decisionnode 0.2.0 → 0.4.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/README.md CHANGED
@@ -1,9 +1,19 @@
1
- # DecisionNode
1
+ <p align="center">
2
+ <img src="website/src/assets/images/DecisionNode-transparent.png" width="150" />
3
+ </p>
2
4
 
3
- > Structured, queryable memory for development decisions. Stores architectural choices as vector embeddings, exposes them to AI agents via MCP.
5
+ <h1 align="center">DecisionNode</h1>
4
6
 
5
- ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
6
- ![npm](https://img.shields.io/npm/v/decisionnode.svg)
7
+ <p align="center">
8
+ Structured, queryable memory for development decisions.<br/>
9
+ Stores architectural choices as vector embeddings, exposes them to AI agents via MCP.
10
+ </p>
11
+
12
+ <p align="center">
13
+ <img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT" />
14
+ <img src="https://img.shields.io/npm/v/decisionnode.svg" alt="npm version" />
15
+ <img src="https://github.com/decisionnode/DecisionNode/actions/workflows/ci.yml/badge.svg" alt="CI" />
16
+ </p>
7
17
 
8
18
  ---
9
19
 
@@ -16,11 +26,12 @@ Not a markdown file. A queryable memory layer with semantic search.
16
26
  ```bash
17
27
  npm install -g decisionnode
18
28
  cd your-project
19
- decide init # creates project store + .mcp.json for AI clients
29
+ decide init # creates project store
20
30
  decide setup # configure Gemini API key (free tier)
21
- ```
22
31
 
23
- `decide init` creates a `.mcp.json` in your project — AI clients like Claude Code and Cursor connect automatically.
32
+ # Connect to Claude Code (run once)
33
+ claude mcp add decisionnode -s user decide-mcp
34
+ ```
24
35
 
25
36
  ## How it works
26
37
 
package/dist/ai/rag.d.ts CHANGED
@@ -16,6 +16,10 @@ export declare function loadVectorCache(): Promise<VectorCache>;
16
16
  * Save the vector cache to disk
17
17
  */
18
18
  export declare function saveVectorCache(cache: VectorCache): Promise<void>;
19
+ /**
20
+ * Get the vector from a cache entry (handles both legacy and new format)
21
+ */
22
+ declare function getVectorFromEntry(entry: VectorEntry | number[]): number[];
19
23
  /**
20
24
  * Load the global vector cache from disk
21
25
  */
@@ -33,6 +37,14 @@ export declare function embedGlobalDecision(decision: DecisionNode): Promise<voi
33
37
  * Clear the embedding for a deleted global decision
34
38
  */
35
39
  export declare function clearGlobalEmbedding(decisionId: string): Promise<void>;
40
+ /**
41
+ * Generate the text representation of a decision for embedding
42
+ */
43
+ declare function getDecisionText(decision: DecisionNode): string;
44
+ /**
45
+ * Calculate Cosine Similarity between two vectors
46
+ */
47
+ declare function cosineSimilarity(vecA: number[], vecB: number[]): number;
36
48
  /**
37
49
  * Embed a single decision immediately.
38
50
  * Called automatically when decisions are added or updated.
@@ -71,9 +83,9 @@ export declare function embedAllDecisions(): Promise<{
71
83
  embedded: string[];
72
84
  failed: string[];
73
85
  }>;
86
+ export { cosineSimilarity as _cosineSimilarity, getDecisionText as _getDecisionText, getVectorFromEntry as _getVectorFromEntry };
74
87
  /**
75
88
  * Find potential conflicts with existing decisions
76
89
  * Uses semantic similarity to find decisions that might contradict a new one
77
90
  */
78
91
  export declare function findPotentialConflicts(newDecisionText: string, threshold?: number): Promise<ScoredDecision[]>;
79
- export {};
package/dist/ai/rag.js CHANGED
@@ -239,6 +239,8 @@ export async function embedAllDecisions() {
239
239
  }
240
240
  return { embedded, failed };
241
241
  }
242
+ // Exported for testing
243
+ export { cosineSimilarity as _cosineSimilarity, getDecisionText as _getDecisionText, getVectorFromEntry as _getVectorFromEntry };
242
244
  /**
243
245
  * Find potential conflicts with existing decisions
244
246
  * Uses semantic similarity to find decisions that might contradict a new one
package/dist/cli.js CHANGED
@@ -9,6 +9,39 @@ import fs from 'fs/promises';
9
9
  import path from 'path';
10
10
  const args = process.argv.slice(2);
11
11
  const command = args[0];
12
+ // ─── ANSI styling helpers ───────────────────────────────────
13
+ const c = {
14
+ reset: '\x1b[0m',
15
+ bold: '\x1b[1m',
16
+ dim: '\x1b[2m',
17
+ cyan: '\x1b[36m',
18
+ yellow: '\x1b[33m',
19
+ green: '\x1b[32m',
20
+ red: '\x1b[31m',
21
+ gray: '\x1b[90m',
22
+ white: '\x1b[97m',
23
+ bgCyan: '\x1b[46m',
24
+ bgYellow: '\x1b[43m',
25
+ black: '\x1b[30m',
26
+ };
27
+ function banner() {
28
+ console.log('');
29
+ console.log(` ${c.cyan}╔══════════════════════════════════════╗${c.reset}`);
30
+ console.log(` ${c.cyan}║${c.reset} ${c.bold}${c.cyan}◆${c.reset} ${c.bold}${c.white}Decision${c.yellow}Node${c.reset} ${c.cyan}║${c.reset}`);
31
+ console.log(` ${c.cyan}╚══════════════════════════════════════╝${c.reset}`);
32
+ }
33
+ function box(lines, color = c.cyan) {
34
+ const maxLen = Math.max(...lines.map(l => stripAnsi(l).length));
35
+ const pad = (s) => s + ' '.repeat(maxLen - stripAnsi(s).length);
36
+ console.log(` ${color}┌${'─'.repeat(maxLen + 2)}┐${c.reset}`);
37
+ for (const line of lines) {
38
+ console.log(` ${color}│${c.reset} ${pad(line)} ${color}│${c.reset}`);
39
+ }
40
+ console.log(` ${color}└${'─'.repeat(maxLen + 2)}┘${c.reset}`);
41
+ }
42
+ function stripAnsi(s) {
43
+ return s.replace(/\x1b\[[0-9;]*m/g, '');
44
+ }
12
45
  async function main() {
13
46
  try {
14
47
  switch (command) {
@@ -104,7 +137,7 @@ async function main() {
104
137
  }
105
138
  }
106
139
  catch (error) {
107
- console.error('❌ Error:', error.message);
140
+ console.error(`\n ${c.red}✗${c.reset} ${error.message}\n`);
108
141
  process.exit(1);
109
142
  }
110
143
  }
@@ -120,12 +153,12 @@ async function handleList() {
120
153
  : await listGlobalDecisions();
121
154
  const allDecisions = [...projectDecisions, ...globalDecisions];
122
155
  if (allDecisions.length === 0) {
123
- console.log('📭 No decisions found.');
124
- console.log('\nRun: decide add');
156
+ console.log(`\n ${c.dim}No decisions found.${c.reset}`);
157
+ console.log(` Run: ${c.cyan}decide add${c.reset}\n`);
125
158
  return;
126
159
  }
127
160
  const label = globalOnly ? 'Global Decisions' : `Decisions${scope ? ` (${scope})` : ''}`;
128
- console.log(`\n📋 ${label}: \n`);
161
+ console.log(`\n ${c.bold}${c.white}${label}${c.reset}\n`);
129
162
  // Show global decisions first, then project decisions
130
163
  if (globalDecisions.length > 0) {
131
164
  const globalGrouped = {};
@@ -134,13 +167,12 @@ async function handleList() {
134
167
  globalGrouped[d.scope] = [];
135
168
  globalGrouped[d.scope].push(d);
136
169
  }
137
- console.log('🌐 Global');
170
+ console.log(` ${c.yellow}● Global${c.reset}`);
138
171
  for (const [scopeName, scopeDecisions] of Object.entries(globalGrouped)) {
139
- console.log(` 📁 ${scopeName} `);
172
+ console.log(` ${c.dim}${scopeName}${c.reset}`);
140
173
  for (const d of scopeDecisions) {
141
- const statusIcon = d.status === 'active' ? '✅' :
142
- '⚠️';
143
- console.log(` ${statusIcon} [${d.id}] ${d.decision} `);
174
+ const status = d.status === 'active' ? `${c.green}●${c.reset}` : `${c.dim}○${c.reset}`;
175
+ console.log(` ${status} ${c.cyan}${d.id}${c.reset} ${d.decision}`);
144
176
  }
145
177
  }
146
178
  console.log('');
@@ -153,11 +185,10 @@ async function handleList() {
153
185
  grouped[d.scope].push(d);
154
186
  }
155
187
  for (const [scopeName, scopeDecisions] of Object.entries(grouped)) {
156
- console.log(`📁 ${scopeName} `);
188
+ console.log(` ${c.dim}${scopeName}${c.reset}`);
157
189
  for (const d of scopeDecisions) {
158
- const statusIcon = d.status === 'active' ? '✅' :
159
- '⚠️';
160
- console.log(` ${statusIcon} [${d.id}] ${d.decision} `);
190
+ const status = d.status === 'active' ? `${c.green}●${c.reset}` : `${c.dim}○${c.reset}`;
191
+ console.log(` ${status} ${c.cyan}${d.id}${c.reset} ${d.decision}`);
161
192
  }
162
193
  console.log('');
163
194
  }
@@ -167,61 +198,93 @@ async function handleList() {
167
198
  parts.push(`${projectDecisions.length} project`);
168
199
  if (globalDecisions.length > 0)
169
200
  parts.push(`${globalDecisions.length} global`);
170
- console.log(`Total: ${parts.join(' + ')} decisions`);
201
+ console.log(` ${c.dim}${parts.join(' + ')} decisions${c.reset}\n`);
171
202
  }
172
203
  async function handleGet() {
173
204
  const id = args[1];
174
205
  if (!id) {
175
- console.log('Usage: decide get <decision-id>');
206
+ console.log(`\n ${c.dim}Usage:${c.reset} ${c.cyan}decide get${c.reset} ${c.gray}<decision-id>${c.reset}\n`);
176
207
  return;
177
208
  }
178
209
  const decision = isGlobalId(id)
179
210
  ? await getGlobalDecisionById(id)
180
211
  : await getDecisionById(id);
181
212
  if (!decision) {
182
- console.log(`❌ Decision "${id}" not found`);
213
+ console.log(`\n ${c.red}✗${c.reset} Decision "${id}" not found\n`);
183
214
  return;
184
215
  }
185
- console.log('\n' + '─'.repeat(60));
186
- console.log(`📌 ${decision.id} `);
187
- console.log('─'.repeat(60));
188
- console.log(`\n📋 Decision: ${decision.decision} `);
216
+ const statusColor = decision.status === 'active' ? c.green : c.dim;
217
+ console.log('');
218
+ console.log(` ${c.cyan}${c.bold}${decision.id}${c.reset} ${statusColor}${decision.status}${c.reset}`);
219
+ console.log(` ${c.gray}${'─'.repeat(50)}${c.reset}`);
220
+ console.log(` ${c.white}${decision.decision}${c.reset}`);
189
221
  if (decision.rationale) {
190
- console.log(`\n💡 Rationale: ${decision.rationale} `);
222
+ console.log(` ${c.dim}Rationale:${c.reset} ${decision.rationale}`);
191
223
  }
192
- console.log(`\n📁 Scope: ${decision.scope} `);
193
- console.log(`📊 Status: ${decision.status} `);
224
+ console.log(` ${c.dim}Scope:${c.reset} ${decision.scope}`);
194
225
  if (decision.constraints?.length) {
195
- console.log(`⚠️ Constraints: ${decision.constraints.join(', ')} `);
226
+ console.log(` ${c.dim}Constraints:${c.reset} ${decision.constraints.join(', ')}`);
196
227
  }
197
228
  console.log('');
198
229
  }
199
230
  async function handleSearch() {
200
231
  const query = args.slice(1).join(' ');
201
232
  if (!query) {
202
- console.log('Usage: decide search "<your question>"');
233
+ console.log(`\n ${c.dim}Usage:${c.reset} ${c.cyan}decide search${c.reset} ${c.gray}"your question"${c.reset}\n`);
203
234
  return;
204
235
  }
205
236
  try {
206
237
  const { findRelevantDecisions } = await import('./ai/rag.js');
207
238
  const results = await findRelevantDecisions(query, 5);
208
239
  if (results.length === 0) {
209
- console.log('🔍 No relevant decisions found.');
210
- console.log(' (Have you added decisions yet?)');
240
+ console.log(`\n ${c.dim}No relevant decisions found.${c.reset}`);
241
+ console.log(` ${c.dim}Have you added decisions yet?${c.reset}\n`);
211
242
  return;
212
243
  }
213
- console.log(`\n🔍 Results for: "${query}"\n`);
244
+ console.log(`\n ${c.bold}${c.white}Results for:${c.reset} ${c.dim}"${query}"${c.reset}\n`);
214
245
  for (const result of results) {
215
246
  const score = (result.score * 100).toFixed(0);
216
- console.log(`[${score}%] ${result.decision.id}: ${result.decision.decision} `);
247
+ const scoreColor = Number(score) >= 80 ? c.green : Number(score) >= 60 ? c.yellow : c.dim;
248
+ console.log(` ${scoreColor}${score}%${c.reset} ${c.cyan}${result.decision.id}${c.reset} ${result.decision.decision}`);
217
249
  }
218
250
  console.log('');
219
251
  }
220
252
  catch (error) {
221
- console.log('❌ Semantic search requires a Gemini API key.');
222
- console.log(' Run: decide setup');
253
+ console.log(`\n ${c.red}✗${c.reset} Semantic search requires a Gemini API key.`);
254
+ console.log(` Run: ${c.cyan}decide setup${c.reset}\n`);
223
255
  }
224
256
  }
257
+ function readHidden(promptText) {
258
+ return new Promise((resolve) => {
259
+ process.stderr.write(promptText);
260
+ const stdin = process.stdin;
261
+ const wasRaw = stdin.isRaw;
262
+ stdin.setRawMode(true);
263
+ stdin.resume();
264
+ stdin.setEncoding('utf-8');
265
+ let input = '';
266
+ const onData = (ch) => {
267
+ if (ch === '\r' || ch === '\n') {
268
+ stdin.setRawMode(wasRaw ?? false);
269
+ stdin.pause();
270
+ stdin.removeListener('data', onData);
271
+ resolve(input);
272
+ }
273
+ else if (ch === '\u0003') {
274
+ // Ctrl+C
275
+ process.exit(0);
276
+ }
277
+ else if (ch === '\u007f' || ch === '\b') {
278
+ // Backspace
279
+ input = input.slice(0, -1);
280
+ }
281
+ else {
282
+ input += ch;
283
+ }
284
+ };
285
+ stdin.on('data', onData);
286
+ });
287
+ }
225
288
  function prompt(question, defaultValue) {
226
289
  const rl = readline.createInterface({
227
290
  input: process.stdin,
@@ -261,23 +324,23 @@ async function handleAddDecision() {
261
324
  }
262
325
  else {
263
326
  // Interactive mode
264
- console.log(`\n➕ Add New ${isGlobal ? 'Global ' : ''}Decision\n`);
327
+ console.log(`\n ${c.bold}${c.white}New ${isGlobal ? 'Global ' : ''}Decision${c.reset}\n`);
265
328
  // Show existing scopes for consistency
266
329
  const existingScopes = isGlobal ? await getGlobalScopes() : await getAvailableScopes();
267
330
  if (existingScopes.length > 0) {
268
- console.log(`Existing scopes: ${existingScopes.join(', ')} \n`);
331
+ console.log(` ${c.dim}Existing scopes:${c.reset} ${c.cyan}${existingScopes.join(', ')}${c.reset}\n`);
269
332
  }
270
333
  const scopeExamples = existingScopes.length > 0
271
334
  ? existingScopes.slice(0, 3).join(', ')
272
335
  : 'UI, Backend, API';
273
- scope = await prompt(`Scope (e.g., ${scopeExamples} - capitalization doesn't matter): `);
336
+ scope = await prompt(` ${c.yellow}▸${c.reset} Scope (e.g., ${scopeExamples}): `);
274
337
  if (!scope.trim()) {
275
- console.log('❌ Scope is required');
338
+ console.log(` ${c.red}✗${c.reset} Scope is required\n`);
276
339
  return;
277
340
  }
278
- decisionText = await prompt('Decision: ');
341
+ decisionText = await prompt(` ${c.yellow}▸${c.reset} Decision: `);
279
342
  if (!decisionText.trim()) {
280
- console.log('❌ Decision text is required');
343
+ console.log(` ${c.red}✗${c.reset} Decision text is required\n`);
281
344
  return;
282
345
  }
283
346
  // Check for potential conflicts with existing decisions
@@ -285,15 +348,15 @@ async function handleAddDecision() {
285
348
  const { findPotentialConflicts } = await import('./ai/rag.js');
286
349
  const conflicts = await findPotentialConflicts(`${scope}: ${decisionText}`, 0.75);
287
350
  if (conflicts.length > 0) {
288
- console.log('\n⚠️ Similar decisions found:\n');
351
+ console.log(`\n ${c.yellow}!${c.reset} ${c.bold}Similar decisions found:${c.reset}\n`);
289
352
  for (const { decision, score } of conflicts) {
290
353
  const similarity = Math.round(score * 100);
291
- console.log(` ${decision.id}: ${decision.decision.substring(0, 50)}... (${similarity}% similar)`);
354
+ console.log(` ${c.yellow}${similarity}%${c.reset} ${c.cyan}${decision.id}${c.reset} ${decision.decision.substring(0, 50)}...`);
292
355
  }
293
356
  console.log('');
294
- const proceed = await prompt('Continue anyway? (y/N): ');
357
+ const proceed = await prompt(` Continue anyway? ${c.dim}(y/N):${c.reset} `);
295
358
  if (proceed.toLowerCase() !== 'y') {
296
- console.log('Cancelled.');
359
+ console.log(` ${c.dim}Cancelled.${c.reset}\n`);
297
360
  return;
298
361
  }
299
362
  }
@@ -301,8 +364,8 @@ async function handleAddDecision() {
301
364
  catch {
302
365
  // Conflict check failed (API key not set) - continue anyway
303
366
  }
304
- rationale = await prompt('Rationale (optional): ');
305
- constraintsInput = await prompt('Constraints (comma-separated, optional): ');
367
+ rationale = await prompt(` ${c.yellow}▸${c.reset} Rationale ${c.dim}(optional):${c.reset} `);
368
+ constraintsInput = await prompt(` ${c.yellow}▸${c.reset} Constraints ${c.dim}(comma-separated, optional):${c.reset} `);
306
369
  }
307
370
  if (isGlobal) {
308
371
  const rawId = await getNextGlobalDecisionId(scope.trim());
@@ -316,15 +379,13 @@ async function handleAddDecision() {
316
379
  createdAt: new Date().toISOString()
317
380
  };
318
381
  const { embedded } = await addGlobalDecision(newDecision);
319
- console.log(`\n Created global:${rawId}`);
320
- console.log(` This decision applies to all projects`);
382
+ console.log(`\n ${c.green}✓${c.reset} Created ${c.cyan}global:${rawId}${c.reset}`);
383
+ console.log(` ${c.dim}Applies to all projects${c.reset}`);
321
384
  if (embedded) {
322
- console.log(` Auto-embedded for semantic search`);
385
+ console.log(` ${c.dim}Embedded for semantic search${c.reset}`);
323
386
  }
324
387
  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)`);
388
+ console.log(`\n ${c.yellow}!${c.reset} Not embedded — run ${c.cyan}decide setup${c.reset} then ${c.cyan}decide embed${c.reset}`);
328
389
  }
329
390
  }
330
391
  else {
@@ -339,21 +400,19 @@ async function handleAddDecision() {
339
400
  createdAt: new Date().toISOString()
340
401
  };
341
402
  const { embedded } = await addDecision(newDecision);
342
- console.log(`\n Created ${id}`);
403
+ console.log(`\n ${c.green}✓${c.reset} Created ${c.cyan}${id}${c.reset}`);
343
404
  if (embedded) {
344
- console.log(` Auto-embedded for semantic search`);
405
+ console.log(` ${c.dim}Embedded for semantic search${c.reset}`);
345
406
  }
346
407
  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)`);
408
+ console.log(`\n ${c.yellow}!${c.reset} Not embedded — run ${c.cyan}decide setup${c.reset} then ${c.cyan}decide embed${c.reset}`);
350
409
  }
351
410
  }
352
411
  }
353
412
  async function handleEdit() {
354
413
  const id = args[1];
355
414
  if (!id) {
356
- console.log('Usage: decide edit <decision-id>');
415
+ console.log(`\n ${c.dim}Usage:${c.reset} ${c.cyan}decide edit${c.reset} ${c.gray}<decision-id>${c.reset}\n`);
357
416
  return;
358
417
  }
359
418
  const global = isGlobalId(id);
@@ -361,19 +420,19 @@ async function handleEdit() {
361
420
  ? await getGlobalDecisionById(id)
362
421
  : await getDecisionById(id);
363
422
  if (!decision) {
364
- console.log(`❌ Decision ${id} not found`);
423
+ console.log(`\n ${c.red}✗${c.reset} Decision "${id}" not found\n`);
365
424
  return;
366
425
  }
367
426
  if (global) {
368
- console.log(`\n⚠️ This is a global decision that affects ALL projects.`);
369
- const confirm = await prompt('Continue editing? (y/N): ');
427
+ console.log(`\n ${c.yellow}!${c.reset} This is a global decision that affects ${c.bold}all projects${c.reset}.`);
428
+ const confirm = await prompt(` Continue editing? ${c.dim}(y/N):${c.reset} `);
370
429
  if (confirm.trim().toLowerCase() !== 'y') {
371
- console.log('Cancelled.');
430
+ console.log(` ${c.dim}Cancelled.${c.reset}\n`);
372
431
  return;
373
432
  }
374
433
  }
375
- console.log(`\n✏️ Editing ${id}`);
376
- console.log('Press Enter to keep current value.\n');
434
+ console.log(`\n ${c.bold}${c.white}Editing${c.reset} ${c.cyan}${id}${c.reset}`);
435
+ console.log(` ${c.dim}Press Enter to keep current value.${c.reset}\n`);
377
436
  const newDecision = await prompt('Decision: ', decision.decision);
378
437
  const newRationale = await prompt('Rationale: ', decision.rationale || '');
379
438
  const newConstraints = await prompt('Constraints: ', (decision.constraints || []).join(', '));
@@ -385,7 +444,7 @@ async function handleEdit() {
385
444
  if (newConstraints.trim())
386
445
  updates.constraints = newConstraints.split(',').map(s => s.trim());
387
446
  if (Object.keys(updates).length === 0) {
388
- console.log('\nNo changes made.');
447
+ console.log(`\n ${c.dim}No changes made.${c.reset}\n`);
389
448
  return;
390
449
  }
391
450
  if (global) {
@@ -394,13 +453,13 @@ async function handleEdit() {
394
453
  else {
395
454
  await updateDecision(id, updates);
396
455
  }
397
- console.log(`\n Updated ${id}`);
398
- console.log(` Auto-embedded for semantic search`);
456
+ console.log(`\n ${c.green}✓${c.reset} Updated ${c.cyan}${id}${c.reset}`);
457
+ console.log(` ${c.dim}Embedded for semantic search${c.reset}\n`);
399
458
  }
400
459
  async function handleDelete() {
401
460
  const id = args[1];
402
461
  if (!id) {
403
- console.log('Usage: decide delete <decision-id>');
462
+ console.log(`\n ${c.dim}Usage:${c.reset} ${c.cyan}decide delete${c.reset} ${c.gray}<decision-id>${c.reset}\n`);
404
463
  return;
405
464
  }
406
465
  const global = isGlobalId(id);
@@ -408,17 +467,17 @@ async function handleDelete() {
408
467
  ? await getGlobalDecisionById(id)
409
468
  : await getDecisionById(id);
410
469
  if (!decision) {
411
- console.log(`❌ Decision ${id} not found`);
470
+ console.log(`\n ${c.red}✗${c.reset} Decision "${id}" not found\n`);
412
471
  return;
413
472
  }
414
- console.log(`\n🗑️ Delete: ${id}`);
415
- console.log(` "${decision.decision}"\n`);
473
+ console.log(`\n ${c.red}${c.bold}Delete${c.reset} ${c.cyan}${id}${c.reset}`);
474
+ console.log(` ${c.dim}"${decision.decision}"${c.reset}\n`);
416
475
  if (global) {
417
- console.log(`⚠️ This is a global decision that affects ALL projects.`);
476
+ console.log(` ${c.yellow}!${c.reset} This is a global decision that affects ${c.bold}all projects${c.reset}.`);
418
477
  }
419
- const confirm = await prompt('Type "yes" to confirm: ');
478
+ const confirm = await prompt(` Type ${c.bold}"yes"${c.reset} to confirm: `);
420
479
  if (confirm.trim().toLowerCase() !== 'yes') {
421
- console.log('Cancelled.');
480
+ console.log(` ${c.dim}Cancelled.${c.reset}\n`);
422
481
  return;
423
482
  }
424
483
  if (global) {
@@ -443,12 +502,12 @@ async function handleDelete() {
443
502
  }
444
503
  }
445
504
  }
446
- console.log(`\n Deleted ${id}`);
505
+ console.log(`\n ${c.green}✓${c.reset} Deleted ${c.cyan}${id}${c.reset}\n`);
447
506
  }
448
507
  async function handleDeprecate() {
449
508
  const id = args[1];
450
509
  if (!id) {
451
- console.log('Usage: decide deprecate <decision-id>');
510
+ console.log(`\n ${c.dim}Usage:${c.reset} ${c.cyan}decide deprecate${c.reset} ${c.gray}<decision-id>${c.reset}\n`);
452
511
  return;
453
512
  }
454
513
  const global = isGlobalId(id);
@@ -456,27 +515,26 @@ async function handleDeprecate() {
456
515
  ? await getGlobalDecisionById(id)
457
516
  : await getDecisionById(id);
458
517
  if (!decision) {
459
- console.log(`❌ Decision ${id} not found`);
518
+ console.log(`\n ${c.red}✗${c.reset} Decision "${id}" not found\n`);
460
519
  return;
461
520
  }
462
521
  if (decision.status === 'deprecated') {
463
- console.log(`⚠️ ${id} is already deprecated.`);
522
+ console.log(`\n ${c.yellow}!${c.reset} ${c.cyan}${id}${c.reset} is already deprecated.\n`);
464
523
  return;
465
524
  }
466
- console.log(`\n📌 ${id}: ${decision.decision}`);
525
+ console.log(`\n ${c.cyan}${id}${c.reset} ${decision.decision}`);
467
526
  if (global) {
468
527
  await updateGlobalDecision(id, { status: 'deprecated' });
469
528
  }
470
529
  else {
471
530
  await updateDecision(id, { status: 'deprecated' });
472
531
  }
473
- console.log(`\n✅ Deprecated ${id}`);
474
- console.log(` This decision will no longer appear in search results.`);
532
+ console.log(` ${c.green}✓${c.reset} Deprecated — hidden from search results\n`);
475
533
  }
476
534
  async function handleActivate() {
477
535
  const id = args[1];
478
536
  if (!id) {
479
- console.log('Usage: decide activate <decision-id>');
537
+ console.log(`\n ${c.dim}Usage:${c.reset} ${c.cyan}decide activate${c.reset} ${c.gray}<decision-id>${c.reset}\n`);
480
538
  return;
481
539
  }
482
540
  const global = isGlobalId(id);
@@ -484,76 +542,66 @@ async function handleActivate() {
484
542
  ? await getGlobalDecisionById(id)
485
543
  : await getDecisionById(id);
486
544
  if (!decision) {
487
- console.log(`❌ Decision ${id} not found`);
545
+ console.log(`\n ${c.red}✗${c.reset} Decision "${id}" not found\n`);
488
546
  return;
489
547
  }
490
548
  if (decision.status === 'active') {
491
- console.log(`⚠️ ${id} is already active.`);
549
+ console.log(`\n ${c.yellow}!${c.reset} ${c.cyan}${id}${c.reset} is already active.\n`);
492
550
  return;
493
551
  }
494
- console.log(`\n📌 ${id}: ${decision.decision}`);
552
+ console.log(`\n ${c.cyan}${id}${c.reset} ${decision.decision}`);
495
553
  if (global) {
496
554
  await updateGlobalDecision(id, { status: 'active' });
497
555
  }
498
556
  else {
499
557
  await updateDecision(id, { status: 'active' });
500
558
  }
501
- console.log(`\n✅ Activated ${id}`);
502
- console.log(` This decision will now appear in search results.`);
559
+ console.log(` ${c.green}✓${c.reset} Activated — now appears in search results\n`);
503
560
  }
504
561
  async function handleDeleteScope() {
505
562
  const scopeArg = args[1];
506
563
  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');
564
+ console.log(`\n ${c.dim}Usage:${c.reset} ${c.cyan}decide delete-scope${c.reset} ${c.gray}<scope>${c.reset}\n`);
510
565
  return;
511
566
  }
512
- // Show what will be deleted
513
567
  const scopes = await getAvailableScopes();
514
568
  const normalizedInput = scopeArg.charAt(0).toUpperCase() + scopeArg.slice(1).toLowerCase();
515
569
  if (!scopes.some(s => s.toLowerCase() === scopeArg.toLowerCase())) {
516
- console.log(`❌ Scope "${scopeArg}" not found.`);
517
- console.log(`Available scopes: ${scopes.join(', ')}`);
570
+ console.log(`\n ${c.red}✗${c.reset} Scope "${scopeArg}" not found.`);
571
+ console.log(` ${c.dim}Available:${c.reset} ${c.cyan}${scopes.join(', ')}${c.reset}\n`);
518
572
  return;
519
573
  }
520
574
  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: ');
575
+ console.log(`\n ${c.red}${c.bold}Delete scope${c.reset} ${c.cyan}${normalizedInput}${c.reset} ${c.dim}(${decisions.length} decisions)${c.reset}\n`);
576
+ decisions.forEach(d => console.log(` ${c.dim}─${c.reset} ${c.cyan}${d.id}${c.reset} ${d.decision.substring(0, 50)}`));
577
+ console.log(`\n ${c.yellow}!${c.reset} This cannot be undone.`);
578
+ const confirm = await prompt(` Type the scope name to confirm: `);
525
579
  if (confirm.toLowerCase() !== scopeArg.toLowerCase() && confirm.toLowerCase() !== normalizedInput.toLowerCase()) {
526
- console.log('❌ Deletion cancelled.');
580
+ console.log(` ${c.dim}Cancelled.${c.reset}\n`);
527
581
  return;
528
582
  }
529
583
  const result = await deleteScope(scopeArg);
530
- console.log(`\n Deleted scope "${normalizedInput}" (${result.deleted} decisions removed)`);
584
+ console.log(`\n ${c.green}✓${c.reset} Deleted scope ${c.cyan}${normalizedInput}${c.reset} ${c.dim}(${result.deleted} decisions)${c.reset}\n`);
531
585
  }
532
586
  async function handleImport() {
533
587
  const globalFlag = args.includes('--global');
534
588
  const filePath = args.find(a => a !== 'import' && a !== '--global' && a !== '--overwrite' && !a.startsWith('-'));
535
589
  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
- ]`);
590
+ console.log(`\n ${c.dim}Usage:${c.reset} ${c.cyan}decide import${c.reset} ${c.gray}<file.json>${c.reset} ${c.dim}[--global] [--overwrite]${c.reset}\n`);
542
591
  return;
543
592
  }
544
- console.log(`\n📥 Importing from ${filePath}${globalFlag ? ' (global)' : ''}...`);
593
+ console.log(`\n ${c.bold}${c.white}Importing${c.reset} ${c.dim}${filePath}${globalFlag ? ' (global)' : ''}${c.reset}`);
545
594
  try {
546
595
  const content = await fs.readFile(filePath, 'utf-8');
547
596
  const data = JSON.parse(content);
548
597
  // Support both array and {decisions: [...]} format
549
598
  const decisions = Array.isArray(data) ? data : data.decisions;
550
599
  if (!decisions || decisions.length === 0) {
551
- console.log('❌ No decisions found in file');
600
+ console.log(` ${c.red}✗${c.reset} No decisions found in file\n`);
552
601
  return;
553
602
  }
554
603
  const overwriteFlag = args.includes('--overwrite');
555
604
  if (globalFlag) {
556
- // Import into global store
557
605
  let added = 0;
558
606
  let skipped = 0;
559
607
  for (const decision of decisions) {
@@ -565,20 +613,17 @@ async function handleImport() {
565
613
  skipped++;
566
614
  }
567
615
  }
568
- console.log(`\n Import complete (global)`);
569
- console.log(` Added: ${added}`);
570
- console.log(` Skipped: ${skipped}`);
616
+ console.log(`\n ${c.green}✓${c.reset} Import complete ${c.dim}(global)${c.reset}`);
617
+ console.log(` ${c.green}${added}${c.reset} added ${c.dim}${skipped} skipped${c.reset}\n`);
571
618
  }
572
619
  else {
573
620
  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}`);
621
+ console.log(`\n ${c.green}✓${c.reset} Import complete`);
622
+ console.log(` ${c.green}${result.added}${c.reset} added ${c.dim}${result.skipped} skipped ${result.embedded} embedded${c.reset}\n`);
578
623
  }
579
624
  }
580
625
  catch (error) {
581
- console.log(`❌ Import failed: ${error.message}`);
626
+ console.log(`\n ${c.red}✗${c.reset} Import failed: ${error.message}\n`);
582
627
  }
583
628
  }
584
629
  async function handleHistory() {
@@ -590,20 +635,18 @@ async function handleHistory() {
590
635
  console.log(`❌ Entry ${entryId} not found`);
591
636
  return;
592
637
  }
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`);
638
+ console.log(`\n ${c.bold}${c.white}Snapshot${c.reset} ${c.cyan}${entry.id}${c.reset}`);
639
+ console.log(` ${c.dim}Action:${c.reset} ${entry.action} ${c.dim}Time:${c.reset} ${new Date(entry.timestamp).toLocaleString()}`);
640
+ console.log(` ${c.dim}${entry.description}${c.reset}\n`);
597
641
  const decisions = getDecisionsFromSnapshot(entry.snapshot);
598
- console.log(`Decisions at this point (${decisions.length}):\n`);
642
+ console.log(` ${c.dim}Decisions at this point (${decisions.length}):${c.reset}\n`);
599
643
  for (const d of decisions) {
600
- console.log(`\n─── ${d.id} ───`);
601
- console.log(` Decision: ${d.decision}`);
644
+ console.log(` ${c.cyan}${d.id}${c.reset} ${d.decision}`);
602
645
  if (d.rationale)
603
- console.log(` Rationale: ${d.rationale}`);
646
+ console.log(` ${c.dim}Rationale:${c.reset} ${d.rationale}`);
604
647
  if (d.constraints?.length)
605
- console.log(` Constraints: ${d.constraints.join(', ')}`);
606
- console.log(` Status: ${d.status}`);
648
+ console.log(` ${c.dim}Constraints:${c.reset} ${d.constraints.join(', ')}`);
649
+ console.log(` ${c.dim}Status:${c.reset} ${d.status}`);
607
650
  }
608
651
  console.log('');
609
652
  return;
@@ -623,30 +666,29 @@ async function handleHistory() {
623
666
  displayedHistory = history.filter(h => h.source === filter);
624
667
  }
625
668
  if (displayedHistory.length === 0) {
626
- console.log(`📭 No history found${filter ? ` for filter "${filter}"` : ''}.`);
669
+ console.log(`\n ${c.dim}No history found${filter ? ` for "${filter}"` : ''}.${c.reset}\n`);
627
670
  return;
628
671
  }
629
- console.log(`\n📜 Activity History${filter ? ` (Filter: ${filter.toUpperCase()})` : ''}\n`);
630
- console.log('━'.repeat(60));
672
+ console.log(`\n ${c.bold}${c.white}Activity History${c.reset}${filter ? ` ${c.dim}(${filter})${c.reset}` : ''}\n`);
631
673
  displayedHistory.forEach(entry => {
632
674
  const date = new Date(entry.timestamp).toLocaleString();
633
675
  const icon = getActionIcon(entry.action);
634
- const source = entry.source ? `[${entry.source.toUpperCase()}]` : '[CLI]';
635
- console.log(`${icon} ${date} ${source.padEnd(13)} ${entry.description}`);
676
+ const source = entry.source ? entry.source.toUpperCase() : 'CLI';
677
+ console.log(` ${icon} ${c.dim}${date}${c.reset} ${c.gray}${source.padEnd(5)}${c.reset} ${entry.description}`);
636
678
  });
637
679
  console.log('');
638
680
  }
639
681
  function getActionIcon(action) {
640
682
  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 '🔹';
683
+ case 'added': return `${c.green}+${c.reset}`;
684
+ case 'updated': return `${c.yellow}~${c.reset}`;
685
+ case 'deleted': return `${c.red}-${c.reset}`;
686
+ case 'imported': return `${c.cyan}↓${c.reset}`;
687
+ case 'installed': return `${c.cyan}■${c.reset}`;
688
+ case 'cloud_push': return `${c.cyan}↑${c.reset}`;
689
+ case 'cloud_pull': return `${c.cyan}↓${c.reset}`;
690
+ case 'conflict_resolved': return `${c.yellow}⇔${c.reset}`;
691
+ default: return `${c.dim}·${c.reset}`;
650
692
  }
651
693
  }
652
694
  function getTimeAgo(date) {
@@ -666,51 +708,49 @@ function getTimeAgo(date) {
666
708
  async function handleInit() {
667
709
  const cwd = process.cwd();
668
710
  const projectName = path.basename(cwd);
669
- console.log('\n🚀 Initializing DecisionNode\n');
670
- console.log(` Project: ${projectName}`);
671
- console.log(` Location: ${cwd}\n`);
711
+ banner();
712
+ console.log('');
713
+ console.log(` ${c.gray}Project:${c.reset} ${c.bold}${projectName}${c.reset}`);
714
+ console.log(` ${c.gray}Path:${c.reset} ${c.dim}${cwd}${c.reset}`);
715
+ console.log('');
672
716
  // Check if already initialized by looking for existing decisions
673
717
  const existingScopes = await getAvailableScopes();
674
718
  if (existingScopes.length > 0) {
675
- console.log(`✅ Already initialized with ${existingScopes.length} scope(s): ${existingScopes.join(', ')}`);
676
- console.log('\n Run: decide list');
719
+ console.log(` ${c.green}✓${c.reset} Already initialized with ${c.bold}${existingScopes.length}${c.reset} scope(s): ${c.cyan}${existingScopes.join(', ')}${c.reset}`);
720
+ console.log(`\n Run: ${c.cyan}decide list${c.reset}`);
721
+ console.log('');
677
722
  return;
678
723
  }
679
724
  // Create the .decisions directory
680
725
  const { getProjectRoot } = await import('./env.js');
681
726
  const projectRoot = getProjectRoot();
682
727
  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');
728
+ console.log(` ${c.green}✓${c.reset} Initialized\n`);
729
+ box([
730
+ `${c.bold}${c.white}Next steps${c.reset}`,
731
+ '',
732
+ `${c.yellow}1.${c.reset} Configure your API key`,
733
+ ` ${c.cyan}decide setup${c.reset}`,
734
+ '',
735
+ `${c.yellow}2.${c.reset} Connect your AI client`,
736
+ ` ${c.dim}Claude Code:${c.reset} ${c.cyan}claude mcp add decisionnode -s user decide-mcp${c.reset}`,
737
+ ` ${c.dim}Cursor:${c.reset} ${c.dim}Add decide-mcp in Settings → MCP${c.reset}`,
738
+ ` ${c.dim}Windsurf:${c.reset} ${c.dim}Add decide-mcp in Settings → MCP${c.reset}`,
739
+ '',
740
+ `${c.yellow}3.${c.reset} Add your first decision`,
741
+ ` ${c.cyan}decide add${c.reset}`,
742
+ ]);
743
+ console.log('');
706
744
  }
707
745
  async function handleSetup() {
708
746
  const homeDir = process.env.HOME || process.env.USERPROFILE || '';
709
747
  const envPath = path.join(homeDir, '.decisionnode', '.env');
710
748
  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');
749
+ banner();
750
+ console.log('');
751
+ console.log(` ${c.gray}Semantic search requires a Gemini API key (free tier).${c.reset}`);
752
+ console.log(` ${c.dim}Get one at:${c.reset} ${c.cyan}https://aistudio.google.com/${c.reset}`);
753
+ console.log('');
714
754
  // Check if key already exists
715
755
  let existingKey = process.env.GEMINI_API_KEY || '';
716
756
  if (!existingKey) {
@@ -724,19 +764,20 @@ async function handleSetup() {
724
764
  }
725
765
  if (existingKey) {
726
766
  const masked = existingKey.slice(0, 8) + '...' + existingKey.slice(-4);
727
- console.log(`Current key: ${masked}`);
767
+ console.log(` ${c.gray}Current key:${c.reset} ${c.dim}${masked}${c.reset}`);
728
768
  console.log('');
729
769
  }
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();
770
+ const promptText = existingKey ? ` ${c.yellow}▸${c.reset} New API key (enter to keep current): ` : ` ${c.yellow}▸${c.reset} Gemini API key: `;
771
+ const key = await readHidden(promptText);
772
+ console.log(''); // newline after hidden input
734
773
  if (!key && existingKey) {
735
- console.log('\n✅ Keeping existing key.');
774
+ console.log(` ${c.green}✓${c.reset} Keeping existing key.`);
775
+ console.log('');
736
776
  return;
737
777
  }
738
778
  if (!key) {
739
- console.log('\n⚠️ No key provided. You can run decide setup again later.');
779
+ console.log(` ${c.yellow}!${c.reset} No key provided. Run ${c.cyan}decide setup${c.reset} again later.`);
780
+ console.log('');
740
781
  return;
741
782
  }
742
783
  // Write the .env file
@@ -754,53 +795,53 @@ async function handleSetup() {
754
795
  }
755
796
  await fs.writeFile(envPath, envContent, 'utf-8');
756
797
  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');
798
+ console.log(` ${c.green}✓${c.reset} API key saved\n`);
799
+ box([
800
+ `${c.bold}${c.white}Ready to go${c.reset}`,
801
+ '',
802
+ `${c.cyan}decide add${c.reset} ${c.dim}Record a decision${c.reset}`,
803
+ `${c.cyan}decide search "query"${c.reset} ${c.dim}Semantic search${c.reset}`,
804
+ ]);
761
805
  console.log('');
762
806
  }
763
807
  async function handleEmbed() {
764
- console.log('\nEmbedding decisions...\n');
808
+ console.log(`\n ${c.bold}${c.white}Embedding decisions${c.reset}\n`);
765
809
  try {
766
810
  const { getUnembeddedDecisions, embedAllDecisions } = await import('./ai/rag.js');
767
811
  const unembedded = await getUnembeddedDecisions();
768
812
  if (unembedded.length === 0) {
769
- console.log('✅ All decisions are embedded!');
813
+ console.log(` ${c.green}✓${c.reset} All decisions are embedded.\n`);
770
814
  return;
771
815
  }
772
- console.log(`Found ${unembedded.length} unembedded decisions:`);
773
- unembedded.forEach(d => console.log(` ⚠️ ${d.id}`));
774
- console.log('');
775
- console.log('Generating embeddings...');
816
+ console.log(` ${c.yellow}${unembedded.length}${c.reset} unembedded decisions:`);
817
+ unembedded.forEach(d => console.log(` ${c.dim}─${c.reset} ${c.cyan}${d.id}${c.reset}`));
818
+ console.log(`\n ${c.dim}Generating embeddings...${c.reset}`);
776
819
  const result = await embedAllDecisions();
777
820
  if (result.embedded.length > 0) {
778
- console.log(`\n✅ Embedded: ${result.embedded.join(', ')}`);
821
+ console.log(` ${c.green}✓${c.reset} Embedded: ${c.cyan}${result.embedded.join(', ')}${c.reset}`);
779
822
  }
780
823
  if (result.failed.length > 0) {
781
- console.log(`❌ Failed: ${result.failed.join(', ')}`);
824
+ console.log(` ${c.red}✗${c.reset} Failed: ${result.failed.join(', ')}`);
782
825
  }
826
+ console.log('');
783
827
  }
784
828
  catch (error) {
785
- console.log('❌ Embedding requires a Gemini API key.');
786
- console.log(' Run: decide setup');
829
+ console.log(` ${c.red}✗${c.reset} Requires a Gemini API key.`);
830
+ console.log(` Run: ${c.cyan}decide setup${c.reset}\n`);
787
831
  process.exit(1);
788
832
  }
789
833
  }
790
834
  async function handleCheck() {
791
- console.log('\n🔍 Decision Health Check\n');
835
+ console.log(`\n ${c.bold}${c.white}Health Check${c.reset}\n`);
792
836
  const { loadVectorCache, loadGlobalVectorCache } = await import('./ai/rag.js');
793
- // Project decisions
794
837
  const projectDecisions = await listDecisions();
795
838
  const projectCache = await loadVectorCache();
796
839
  const projectMissing = projectDecisions.filter(d => !projectCache[d.id]);
797
- console.log(`📦 Project: ${projectDecisions.length} decisions`);
798
- console.log(` Embedded: ${projectDecisions.length - projectMissing.length}`);
840
+ const projectEmbedded = projectDecisions.length - projectMissing.length;
841
+ console.log(` ${c.dim}Project${c.reset} ${c.green}${projectEmbedded}${c.reset} embedded ${projectMissing.length > 0 ? `${c.yellow}${projectMissing.length}${c.reset} missing` : `${c.dim}0 missing${c.reset}`}`);
799
842
  if (projectMissing.length > 0) {
800
- console.log(` ⚠️ Missing vectors: ${projectMissing.length}`);
801
- projectMissing.forEach(d => console.log(` - ${d.id}: ${d.decision.substring(0, 50)}`));
843
+ projectMissing.forEach(d => console.log(` ${c.yellow}!${c.reset} ${c.cyan}${d.id}${c.reset} ${d.decision.substring(0, 50)}`));
802
844
  }
803
- // Global decisions
804
845
  const globalDecs = await listGlobalDecisions();
805
846
  let globalMissingCount = 0;
806
847
  if (globalDecs.length > 0) {
@@ -810,40 +851,40 @@ async function handleCheck() {
810
851
  return !globalCache[rawId];
811
852
  });
812
853
  globalMissingCount = globalMissing.length;
813
- console.log(`\n🌐 Global: ${globalDecs.length} decisions`);
814
- console.log(` Embedded: ${globalDecs.length - globalMissing.length}`);
854
+ const globalEmbedded = globalDecs.length - globalMissing.length;
855
+ console.log(` ${c.dim}Global${c.reset} ${c.green}${globalEmbedded}${c.reset} embedded ${globalMissing.length > 0 ? `${c.yellow}${globalMissing.length}${c.reset} missing` : `${c.dim}0 missing${c.reset}`}`);
815
856
  if (globalMissing.length > 0) {
816
- console.log(` ⚠️ Missing vectors: ${globalMissing.length}`);
817
- globalMissing.forEach(d => console.log(` - ${d.id}: ${d.decision.substring(0, 50)}`));
857
+ globalMissing.forEach(d => console.log(` ${c.yellow}!${c.reset} ${c.cyan}${d.id}${c.reset} ${d.decision.substring(0, 50)}`));
818
858
  }
819
859
  }
820
860
  const totalMissing = projectMissing.length + globalMissingCount;
821
861
  if (totalMissing > 0) {
822
- console.log(`\n${totalMissing} decision(s) not searchable. Run: decide embed`);
862
+ console.log(`\n ${c.yellow}${totalMissing}${c.reset} not searchable. Run: ${c.cyan}decide embed${c.reset}\n`);
823
863
  }
824
864
  else {
825
- console.log(`\n All decisions are embedded and searchable!`);
865
+ console.log(`\n ${c.green}✓${c.reset} All decisions embedded and searchable.\n`);
826
866
  }
827
867
  }
828
868
  async function handleClean() {
829
- console.log('\n🧹 Cleaning orphaned data...\n');
869
+ console.log(`\n ${c.bold}${c.white}Cleaning${c.reset}\n`);
830
870
  try {
831
871
  const { cleanOrphanedData } = await import('./maintenance.js');
832
872
  const result = await cleanOrphanedData();
833
873
  if (result.vectorsRemoved === 0 && result.reviewsRemoved === 0) {
834
- console.log('✅ Nothing to clean. Your data is tidy!');
874
+ console.log(` ${c.green}✓${c.reset} Nothing to clean.\n`);
835
875
  }
836
876
  else {
837
877
  if (result.vectorsRemoved > 0) {
838
- console.log(`✅ Removed ${result.vectorsRemoved} orphaned vectors.`);
878
+ console.log(` ${c.green}✓${c.reset} Removed ${result.vectorsRemoved} orphaned vectors`);
839
879
  }
840
880
  if (result.reviewsRemoved > 0) {
841
- console.log(`✅ Removed ${result.reviewsRemoved} orphaned reviews.`);
881
+ console.log(` ${c.green}✓${c.reset} Removed ${result.reviewsRemoved} orphaned reviews`);
842
882
  }
883
+ console.log('');
843
884
  }
844
885
  }
845
886
  catch (error) {
846
- console.error('❌ Error cleaning data:', error.message);
887
+ console.error(` ${c.red}✗${c.reset} ${error.message}\n`);
847
888
  process.exit(1);
848
889
  }
849
890
  }
@@ -973,28 +1014,23 @@ async function handleMarketplace() {
973
1014
  }
974
1015
  }
975
1016
  async function handleProjects() {
976
- console.log('\n📂 Available Projects\n');
977
- // Show global decisions first
1017
+ console.log(`\n ${c.bold}${c.white}Projects${c.reset}\n`);
978
1018
  const globalDecisions = await listGlobalDecisions();
979
1019
  if (globalDecisions.length > 0) {
980
1020
  const globalScopes = await getGlobalScopes();
981
- console.log(`🌐 Global (shared across all projects)`);
982
- console.log(` ${globalDecisions.length} decisions [${globalScopes.join(', ')}]`);
983
- console.log('');
1021
+ console.log(` ${c.yellow}● Global${c.reset} ${c.dim}${globalDecisions.length} decisions${c.reset} ${c.dim}[${globalScopes.join(', ')}]${c.reset}`);
984
1022
  }
985
1023
  const projects = await listProjects();
986
1024
  if (projects.length === 0 && globalDecisions.length === 0) {
987
- console.log('No projects found.');
988
- console.log('\nCreate decisions with: decide add');
1025
+ console.log(` ${c.dim}No projects found.${c.reset}`);
1026
+ console.log(` Run: ${c.cyan}decide add${c.reset}\n`);
989
1027
  return;
990
1028
  }
991
1029
  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('');
1030
+ const scopeStr = project.scopes.length > 0 ? `${c.dim}[${project.scopes.join(', ')}]${c.reset}` : '';
1031
+ console.log(` ${c.cyan}■${c.reset} ${c.bold}${project.name}${c.reset} ${c.dim}${project.decisionCount} decisions${c.reset} ${scopeStr}`);
996
1032
  }
997
- console.log(`Total: ${projects.length} projects${globalDecisions.length > 0 ? ` + global (${globalDecisions.length} decisions)` : ''}`);
1033
+ console.log(`\n ${c.dim}${projects.length} projects${globalDecisions.length > 0 ? ` + global (${globalDecisions.length})` : ''}${c.reset}\n`);
998
1034
  }
999
1035
  /**
1000
1036
  * Handle login command - authenticate with DecisionNode
@@ -1630,54 +1666,36 @@ async function handleFetch() {
1630
1666
  }
1631
1667
  }
1632
1668
  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
- `);
1669
+ banner();
1670
+ console.log('');
1671
+ console.log(` ${c.dim}Usage:${c.reset} ${c.cyan}decide${c.reset} ${c.white}<command>${c.reset} ${c.dim}[options]${c.reset}`);
1672
+ console.log('');
1673
+ console.log(` ${c.bold}${c.white}Getting Started${c.reset}`);
1674
+ console.log(` ${c.cyan}init${c.reset} ${c.dim}Initialize DecisionNode in current project${c.reset}`);
1675
+ console.log(` ${c.cyan}setup${c.reset} ${c.dim}Configure Gemini API key${c.reset}`);
1676
+ console.log('');
1677
+ console.log(` ${c.bold}${c.white}Decisions${c.reset}`);
1678
+ console.log(` ${c.cyan}add${c.reset} ${c.dim}Add a new decision (interactive or inline)${c.reset}`);
1679
+ console.log(` ${c.cyan}list${c.reset} ${c.dim}List all decisions (includes global)${c.reset}`);
1680
+ console.log(` ${c.cyan}get${c.reset} ${c.gray}<id>${c.reset} ${c.dim}View a decision${c.reset}`);
1681
+ console.log(` ${c.cyan}search${c.reset} ${c.gray}"query"${c.reset} ${c.dim}Semantic search${c.reset}`);
1682
+ console.log(` ${c.cyan}edit${c.reset} ${c.gray}<id>${c.reset} ${c.dim}Edit a decision${c.reset}`);
1683
+ console.log(` ${c.cyan}deprecate${c.reset} ${c.gray}<id>${c.reset} ${c.dim}Hide from search (reversible)${c.reset}`);
1684
+ console.log(` ${c.cyan}activate${c.reset} ${c.gray}<id>${c.reset} ${c.dim}Re-activate a deprecated decision${c.reset}`);
1685
+ console.log(` ${c.cyan}delete${c.reset} ${c.gray}<id>${c.reset} ${c.dim}Permanently delete${c.reset}`);
1686
+ console.log('');
1687
+ console.log(` ${c.bold}${c.white}Data${c.reset}`);
1688
+ console.log(` ${c.cyan}export${c.reset} ${c.gray}[format]${c.reset} ${c.dim}Export (md, json, csv)${c.reset}`);
1689
+ console.log(` ${c.cyan}import${c.reset} ${c.gray}<file>${c.reset} ${c.dim}Import from JSON${c.reset}`);
1690
+ console.log(` ${c.cyan}check${c.reset} ${c.dim}Show unembedded decisions${c.reset}`);
1691
+ console.log(` ${c.cyan}embed${c.reset} ${c.dim}Embed any unembedded decisions${c.reset}`);
1692
+ console.log(` ${c.cyan}history${c.reset} ${c.dim}View activity log${c.reset}`);
1693
+ console.log(` ${c.cyan}projects${c.reset} ${c.dim}List all projects${c.reset}`);
1694
+ console.log(` ${c.cyan}config${c.reset} ${c.dim}View/set configuration${c.reset}`);
1695
+ console.log('');
1696
+ console.log(` ${c.dim}Global decisions use the ${c.reset}${c.yellow}global:${c.reset}${c.dim} prefix (e.g., ${c.reset}${c.yellow}global:ui-001${c.reset}${c.dim})${c.reset}`);
1697
+ console.log(` ${c.dim}Docs: ${c.cyan}https://decisionnode.dev/docs${c.reset}`);
1698
+ console.log('');
1681
1699
  }
1682
1700
  /**
1683
1701
  * Handle config command - view or set configuration options
@@ -1686,39 +1704,36 @@ async function handleConfig() {
1686
1704
  const subCommand = args[1];
1687
1705
  const value = args[2];
1688
1706
  if (!subCommand) {
1689
- // Show current config
1690
1707
  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>');
1708
+ console.log(`\n ${c.bold}${c.white}Configuration${c.reset}\n`);
1709
+ console.log(` ${c.dim}search-sensitivity${c.reset} ${c.cyan}${sensitivity}${c.reset}`);
1710
+ console.log(`\n ${c.dim}Usage:${c.reset} ${c.cyan}decide config${c.reset} ${c.gray}<option> <value>${c.reset}\n`);
1696
1711
  return;
1697
1712
  }
1698
1713
  if (subCommand === 'search-sensitivity') {
1699
1714
  if (!value) {
1700
1715
  const current = getSearchSensitivity();
1701
- console.log(`\n🔍 Current search-sensitivity: ${current}`);
1702
- console.log('\nUsage: decide config search-sensitivity <high|medium>');
1716
+ console.log(`\n ${c.dim}search-sensitivity:${c.reset} ${c.cyan}${current}${c.reset}`);
1717
+ console.log(` ${c.dim}Usage:${c.reset} ${c.cyan}decide config search-sensitivity${c.reset} ${c.gray}<high|medium>${c.reset}\n`);
1703
1718
  return;
1704
1719
  }
1705
1720
  if (value !== 'high' && value !== 'medium') {
1706
- console.error('❌ Invalid value. Use "high" or "medium"');
1721
+ console.error(` ${c.red}✗${c.reset} Invalid value. Use ${c.cyan}high${c.reset} or ${c.cyan}medium${c.reset}\n`);
1707
1722
  process.exit(1);
1708
1723
  }
1709
1724
  setSearchSensitivity(value);
1710
- console.log(`\n Search sensitivity set to: ${value}`);
1725
+ console.log(`\n ${c.green}✓${c.reset} Search sensitivity: ${c.cyan}${value}${c.reset}`);
1711
1726
  if (value === 'high') {
1712
- console.log(' AI will be REQUIRED to search decisions before any code changes.');
1727
+ console.log(` ${c.dim}AI must search before any code change${c.reset}`);
1713
1728
  }
1714
1729
  else {
1715
- console.log(' AI will check decisions for significant changes only.');
1730
+ console.log(` ${c.dim}AI searches for significant changes only${c.reset}`);
1716
1731
  }
1717
- console.log('\n💡 Refresh your MCP Server Config for changes to take effect.');
1732
+ console.log(`\n ${c.dim}Restart your MCP server for changes to take effect.${c.reset}\n`);
1718
1733
  return;
1719
1734
  }
1720
- console.error(`❌ Unknown config option: ${subCommand}`);
1721
- console.log('Available options: search-sensitivity, auto-sync');
1735
+ console.error(` ${c.red}✗${c.reset} Unknown option: ${subCommand}`);
1736
+ console.log(` ${c.dim}Available:${c.reset} search-sensitivity\n`);
1722
1737
  process.exit(1);
1723
1738
  }
1724
1739
  main();
@@ -12,7 +12,7 @@ import { getProjectRoot, getCurrentProject, setCurrentProject, isGlobalId } from
12
12
  // Create MCP server
13
13
  const server = new Server({
14
14
  name: 'decisionnode',
15
- version: '0.3.0',
15
+ version: '0.4.0',
16
16
  }, {
17
17
  capabilities: {
18
18
  tools: {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "decisionnode",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Store development decisions as vector embeddings, query them via semantic search. CLI + MCP server for AI agents.",
5
5
  "keywords": [
6
6
  "decisions",
@@ -22,7 +22,7 @@
22
22
  "type": "git",
23
23
  "url": "https://github.com/decisionnode/decisionnode"
24
24
  },
25
- "homepage": "https://github.com/decisionnode/decisionnode#readme",
25
+ "homepage": "https://decisionnode.dev",
26
26
  "bugs": {
27
27
  "url": "https://github.com/decisionnode/decisionnode/issues"
28
28
  },
@@ -40,10 +40,12 @@
40
40
  "scripts": {
41
41
  "build": "tsc",
42
42
  "dev": "tsc --watch",
43
- "prepublishOnly": "npm run build"
43
+ "test": "vitest run",
44
+ "test:watch": "vitest",
45
+ "prepublishOnly": "npm run build && npm run test"
44
46
  },
45
47
  "engines": {
46
- "node": ">=18.0.0"
48
+ "node": ">=20.0.0"
47
49
  },
48
50
  "dependencies": {
49
51
  "@google/generative-ai": "^0.24.1",
@@ -52,6 +54,7 @@
52
54
  },
53
55
  "devDependencies": {
54
56
  "@types/node": "^20.19.27",
55
- "typescript": "^5.3.3"
57
+ "typescript": "^5.3.3",
58
+ "vitest": "^4.1.2"
56
59
  }
57
- }
60
+ }