decisionnode 0.3.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 +1 -0
- package/dist/ai/rag.d.ts +13 -1
- package/dist/ai/rag.js +2 -0
- package/dist/cli.js +297 -267
- package/dist/mcp/server.js +1 -1
- package/package.json +9 -6
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
<p align="center">
|
|
13
13
|
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT" />
|
|
14
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" />
|
|
15
16
|
</p>
|
|
16
17
|
|
|
17
18
|
---
|
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(
|
|
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(
|
|
124
|
-
console.log(
|
|
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
|
|
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(
|
|
170
|
+
console.log(` ${c.yellow}● Global${c.reset}`);
|
|
138
171
|
for (const [scopeName, scopeDecisions] of Object.entries(globalGrouped)) {
|
|
139
|
-
console.log(`
|
|
172
|
+
console.log(` ${c.dim}${scopeName}${c.reset}`);
|
|
140
173
|
for (const d of scopeDecisions) {
|
|
141
|
-
const
|
|
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(
|
|
188
|
+
console.log(` ${c.dim}${scopeName}${c.reset}`);
|
|
157
189
|
for (const d of scopeDecisions) {
|
|
158
|
-
const
|
|
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(`
|
|
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(
|
|
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(
|
|
213
|
+
console.log(`\n ${c.red}✗${c.reset} Decision "${id}" not found\n`);
|
|
183
214
|
return;
|
|
184
215
|
}
|
|
185
|
-
|
|
186
|
-
console.log(
|
|
187
|
-
console.log(
|
|
188
|
-
console.log(
|
|
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(
|
|
222
|
+
console.log(` ${c.dim}Rationale:${c.reset} ${decision.rationale}`);
|
|
191
223
|
}
|
|
192
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
210
|
-
console.log(
|
|
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
|
|
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
|
-
|
|
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(
|
|
222
|
-
console.log(
|
|
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
|
|
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
|
|
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}
|
|
336
|
+
scope = await prompt(` ${c.yellow}▸${c.reset} Scope (e.g., ${scopeExamples}): `);
|
|
274
337
|
if (!scope.trim()) {
|
|
275
|
-
console.log(
|
|
338
|
+
console.log(` ${c.red}✗${c.reset} Scope is required\n`);
|
|
276
339
|
return;
|
|
277
340
|
}
|
|
278
|
-
decisionText = await prompt(
|
|
341
|
+
decisionText = await prompt(` ${c.yellow}▸${c.reset} Decision: `);
|
|
279
342
|
if (!decisionText.trim()) {
|
|
280
|
-
console.log(
|
|
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(
|
|
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(`
|
|
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(
|
|
357
|
+
const proceed = await prompt(` Continue anyway? ${c.dim}(y/N):${c.reset} `);
|
|
295
358
|
if (proceed.toLowerCase() !== 'y') {
|
|
296
|
-
console.log(
|
|
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(
|
|
305
|
-
constraintsInput = await prompt(
|
|
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
|
|
320
|
-
console.log(`
|
|
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(`
|
|
385
|
+
console.log(` ${c.dim}Embedded for semantic search${c.reset}`);
|
|
323
386
|
}
|
|
324
387
|
else {
|
|
325
|
-
console.log(`\n
|
|
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
|
|
403
|
+
console.log(`\n ${c.green}✓${c.reset} Created ${c.cyan}${id}${c.reset}`);
|
|
343
404
|
if (embedded) {
|
|
344
|
-
console.log(`
|
|
405
|
+
console.log(` ${c.dim}Embedded for semantic search${c.reset}`);
|
|
345
406
|
}
|
|
346
407
|
else {
|
|
347
|
-
console.log(`\n
|
|
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(
|
|
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(
|
|
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
|
|
369
|
-
const confirm = await prompt(
|
|
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(
|
|
430
|
+
console.log(` ${c.dim}Cancelled.${c.reset}\n`);
|
|
372
431
|
return;
|
|
373
432
|
}
|
|
374
433
|
}
|
|
375
|
-
console.log(`\n
|
|
376
|
-
console.log(
|
|
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(
|
|
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
|
|
398
|
-
console.log(`
|
|
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(
|
|
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(
|
|
470
|
+
console.log(`\n ${c.red}✗${c.reset} Decision "${id}" not found\n`);
|
|
412
471
|
return;
|
|
413
472
|
}
|
|
414
|
-
console.log(`\n
|
|
415
|
-
console.log(`
|
|
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(
|
|
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(
|
|
478
|
+
const confirm = await prompt(` Type ${c.bold}"yes"${c.reset} to confirm: `);
|
|
420
479
|
if (confirm.trim().toLowerCase() !== 'yes') {
|
|
421
|
-
console.log(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
517
|
-
console.log(`Available
|
|
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
|
|
522
|
-
decisions.forEach(d => console.log(`
|
|
523
|
-
console.log(
|
|
524
|
-
const confirm = await prompt(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
569
|
-
console.log(`
|
|
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
|
|
575
|
-
console.log(`
|
|
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(
|
|
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
|
|
594
|
-
console.log(`
|
|
595
|
-
console.log(`
|
|
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})
|
|
642
|
+
console.log(` ${c.dim}Decisions at this point (${decisions.length}):${c.reset}\n`);
|
|
599
643
|
for (const d of decisions) {
|
|
600
|
-
console.log(
|
|
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(`
|
|
646
|
+
console.log(` ${c.dim}Rationale:${c.reset} ${d.rationale}`);
|
|
604
647
|
if (d.constraints?.length)
|
|
605
|
-
console.log(`
|
|
606
|
-
console.log(`
|
|
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(
|
|
669
|
+
console.log(`\n ${c.dim}No history found${filter ? ` for "${filter}"` : ''}.${c.reset}\n`);
|
|
627
670
|
return;
|
|
628
671
|
}
|
|
629
|
-
console.log(`\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 ?
|
|
635
|
-
console.log(
|
|
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
|
|
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,36 +708,49 @@ function getTimeAgo(date) {
|
|
|
666
708
|
async function handleInit() {
|
|
667
709
|
const cwd = process.cwd();
|
|
668
710
|
const projectName = path.basename(cwd);
|
|
669
|
-
|
|
670
|
-
console.log(
|
|
671
|
-
console.log(`
|
|
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(
|
|
676
|
-
console.log(
|
|
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
|
-
console.log(
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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('');
|
|
691
744
|
}
|
|
692
745
|
async function handleSetup() {
|
|
693
746
|
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
694
747
|
const envPath = path.join(homeDir, '.decisionnode', '.env');
|
|
695
748
|
const envDir = path.dirname(envPath);
|
|
696
|
-
|
|
697
|
-
console.log('
|
|
698
|
-
console.log(
|
|
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('');
|
|
699
754
|
// Check if key already exists
|
|
700
755
|
let existingKey = process.env.GEMINI_API_KEY || '';
|
|
701
756
|
if (!existingKey) {
|
|
@@ -709,19 +764,20 @@ async function handleSetup() {
|
|
|
709
764
|
}
|
|
710
765
|
if (existingKey) {
|
|
711
766
|
const masked = existingKey.slice(0, 8) + '...' + existingKey.slice(-4);
|
|
712
|
-
console.log(`Current key
|
|
767
|
+
console.log(` ${c.gray}Current key:${c.reset} ${c.dim}${masked}${c.reset}`);
|
|
713
768
|
console.log('');
|
|
714
769
|
}
|
|
715
|
-
const
|
|
716
|
-
const
|
|
717
|
-
|
|
718
|
-
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
|
|
719
773
|
if (!key && existingKey) {
|
|
720
|
-
console.log(
|
|
774
|
+
console.log(` ${c.green}✓${c.reset} Keeping existing key.`);
|
|
775
|
+
console.log('');
|
|
721
776
|
return;
|
|
722
777
|
}
|
|
723
778
|
if (!key) {
|
|
724
|
-
console.log(
|
|
779
|
+
console.log(` ${c.yellow}!${c.reset} No key provided. Run ${c.cyan}decide setup${c.reset} again later.`);
|
|
780
|
+
console.log('');
|
|
725
781
|
return;
|
|
726
782
|
}
|
|
727
783
|
// Write the .env file
|
|
@@ -739,53 +795,53 @@ async function handleSetup() {
|
|
|
739
795
|
}
|
|
740
796
|
await fs.writeFile(envPath, envContent, 'utf-8');
|
|
741
797
|
process.env.GEMINI_API_KEY = key;
|
|
742
|
-
console.log(
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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
|
+
]);
|
|
746
805
|
console.log('');
|
|
747
806
|
}
|
|
748
807
|
async function handleEmbed() {
|
|
749
|
-
console.log(
|
|
808
|
+
console.log(`\n ${c.bold}${c.white}Embedding decisions${c.reset}\n`);
|
|
750
809
|
try {
|
|
751
810
|
const { getUnembeddedDecisions, embedAllDecisions } = await import('./ai/rag.js');
|
|
752
811
|
const unembedded = await getUnembeddedDecisions();
|
|
753
812
|
if (unembedded.length === 0) {
|
|
754
|
-
console.log(
|
|
813
|
+
console.log(` ${c.green}✓${c.reset} All decisions are embedded.\n`);
|
|
755
814
|
return;
|
|
756
815
|
}
|
|
757
|
-
console.log(`
|
|
758
|
-
unembedded.forEach(d => console.log(`
|
|
759
|
-
console.log(
|
|
760
|
-
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}`);
|
|
761
819
|
const result = await embedAllDecisions();
|
|
762
820
|
if (result.embedded.length > 0) {
|
|
763
|
-
console.log(
|
|
821
|
+
console.log(` ${c.green}✓${c.reset} Embedded: ${c.cyan}${result.embedded.join(', ')}${c.reset}`);
|
|
764
822
|
}
|
|
765
823
|
if (result.failed.length > 0) {
|
|
766
|
-
console.log(
|
|
824
|
+
console.log(` ${c.red}✗${c.reset} Failed: ${result.failed.join(', ')}`);
|
|
767
825
|
}
|
|
826
|
+
console.log('');
|
|
768
827
|
}
|
|
769
828
|
catch (error) {
|
|
770
|
-
console.log(
|
|
771
|
-
console.log(
|
|
829
|
+
console.log(` ${c.red}✗${c.reset} Requires a Gemini API key.`);
|
|
830
|
+
console.log(` Run: ${c.cyan}decide setup${c.reset}\n`);
|
|
772
831
|
process.exit(1);
|
|
773
832
|
}
|
|
774
833
|
}
|
|
775
834
|
async function handleCheck() {
|
|
776
|
-
console.log(
|
|
835
|
+
console.log(`\n ${c.bold}${c.white}Health Check${c.reset}\n`);
|
|
777
836
|
const { loadVectorCache, loadGlobalVectorCache } = await import('./ai/rag.js');
|
|
778
|
-
// Project decisions
|
|
779
837
|
const projectDecisions = await listDecisions();
|
|
780
838
|
const projectCache = await loadVectorCache();
|
|
781
839
|
const projectMissing = projectDecisions.filter(d => !projectCache[d.id]);
|
|
782
|
-
|
|
783
|
-
console.log(`
|
|
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}`}`);
|
|
784
842
|
if (projectMissing.length > 0) {
|
|
785
|
-
console.log(`
|
|
786
|
-
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)}`));
|
|
787
844
|
}
|
|
788
|
-
// Global decisions
|
|
789
845
|
const globalDecs = await listGlobalDecisions();
|
|
790
846
|
let globalMissingCount = 0;
|
|
791
847
|
if (globalDecs.length > 0) {
|
|
@@ -795,40 +851,40 @@ async function handleCheck() {
|
|
|
795
851
|
return !globalCache[rawId];
|
|
796
852
|
});
|
|
797
853
|
globalMissingCount = globalMissing.length;
|
|
798
|
-
|
|
799
|
-
console.log(`
|
|
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}`}`);
|
|
800
856
|
if (globalMissing.length > 0) {
|
|
801
|
-
console.log(`
|
|
802
|
-
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)}`));
|
|
803
858
|
}
|
|
804
859
|
}
|
|
805
860
|
const totalMissing = projectMissing.length + globalMissingCount;
|
|
806
861
|
if (totalMissing > 0) {
|
|
807
|
-
console.log(`\n${totalMissing}
|
|
862
|
+
console.log(`\n ${c.yellow}${totalMissing}${c.reset} not searchable. Run: ${c.cyan}decide embed${c.reset}\n`);
|
|
808
863
|
}
|
|
809
864
|
else {
|
|
810
|
-
console.log(`\n
|
|
865
|
+
console.log(`\n ${c.green}✓${c.reset} All decisions embedded and searchable.\n`);
|
|
811
866
|
}
|
|
812
867
|
}
|
|
813
868
|
async function handleClean() {
|
|
814
|
-
console.log(
|
|
869
|
+
console.log(`\n ${c.bold}${c.white}Cleaning${c.reset}\n`);
|
|
815
870
|
try {
|
|
816
871
|
const { cleanOrphanedData } = await import('./maintenance.js');
|
|
817
872
|
const result = await cleanOrphanedData();
|
|
818
873
|
if (result.vectorsRemoved === 0 && result.reviewsRemoved === 0) {
|
|
819
|
-
console.log(
|
|
874
|
+
console.log(` ${c.green}✓${c.reset} Nothing to clean.\n`);
|
|
820
875
|
}
|
|
821
876
|
else {
|
|
822
877
|
if (result.vectorsRemoved > 0) {
|
|
823
|
-
console.log(
|
|
878
|
+
console.log(` ${c.green}✓${c.reset} Removed ${result.vectorsRemoved} orphaned vectors`);
|
|
824
879
|
}
|
|
825
880
|
if (result.reviewsRemoved > 0) {
|
|
826
|
-
console.log(
|
|
881
|
+
console.log(` ${c.green}✓${c.reset} Removed ${result.reviewsRemoved} orphaned reviews`);
|
|
827
882
|
}
|
|
883
|
+
console.log('');
|
|
828
884
|
}
|
|
829
885
|
}
|
|
830
886
|
catch (error) {
|
|
831
|
-
console.error(
|
|
887
|
+
console.error(` ${c.red}✗${c.reset} ${error.message}\n`);
|
|
832
888
|
process.exit(1);
|
|
833
889
|
}
|
|
834
890
|
}
|
|
@@ -958,28 +1014,23 @@ async function handleMarketplace() {
|
|
|
958
1014
|
}
|
|
959
1015
|
}
|
|
960
1016
|
async function handleProjects() {
|
|
961
|
-
console.log(
|
|
962
|
-
// Show global decisions first
|
|
1017
|
+
console.log(`\n ${c.bold}${c.white}Projects${c.reset}\n`);
|
|
963
1018
|
const globalDecisions = await listGlobalDecisions();
|
|
964
1019
|
if (globalDecisions.length > 0) {
|
|
965
1020
|
const globalScopes = await getGlobalScopes();
|
|
966
|
-
console.log(
|
|
967
|
-
console.log(` ${globalDecisions.length} decisions [${globalScopes.join(', ')}]`);
|
|
968
|
-
console.log('');
|
|
1021
|
+
console.log(` ${c.yellow}● Global${c.reset} ${c.dim}${globalDecisions.length} decisions${c.reset} ${c.dim}[${globalScopes.join(', ')}]${c.reset}`);
|
|
969
1022
|
}
|
|
970
1023
|
const projects = await listProjects();
|
|
971
1024
|
if (projects.length === 0 && globalDecisions.length === 0) {
|
|
972
|
-
console.log(
|
|
973
|
-
console.log(
|
|
1025
|
+
console.log(` ${c.dim}No projects found.${c.reset}`);
|
|
1026
|
+
console.log(` Run: ${c.cyan}decide add${c.reset}\n`);
|
|
974
1027
|
return;
|
|
975
1028
|
}
|
|
976
1029
|
for (const project of projects) {
|
|
977
|
-
const scopeStr = project.scopes.length > 0 ?
|
|
978
|
-
console.log(
|
|
979
|
-
console.log(` ${project.decisionCount} decisions ${scopeStr}`);
|
|
980
|
-
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}`);
|
|
981
1032
|
}
|
|
982
|
-
console.log(
|
|
1033
|
+
console.log(`\n ${c.dim}${projects.length} projects${globalDecisions.length > 0 ? ` + global (${globalDecisions.length})` : ''}${c.reset}\n`);
|
|
983
1034
|
}
|
|
984
1035
|
/**
|
|
985
1036
|
* Handle login command - authenticate with DecisionNode
|
|
@@ -1615,54 +1666,36 @@ async function handleFetch() {
|
|
|
1615
1666
|
}
|
|
1616
1667
|
}
|
|
1617
1668
|
function printUsage() {
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
history [entry-id] View activity log or snapshot
|
|
1649
|
-
|
|
1650
|
-
projects List all available projects
|
|
1651
|
-
config View/set configuration
|
|
1652
|
-
delete-scope <scope> Delete all decisions in a scope
|
|
1653
|
-
|
|
1654
|
-
Global decision IDs use the "global:" prefix (e.g., global:ui-001).
|
|
1655
|
-
Use this prefix with get, edit, and delete commands.
|
|
1656
|
-
|
|
1657
|
-
Examples:
|
|
1658
|
-
decide init
|
|
1659
|
-
decide add
|
|
1660
|
-
decide add --global
|
|
1661
|
-
decide search "What font should I use?"
|
|
1662
|
-
decide list --global
|
|
1663
|
-
decide get global:ui-001
|
|
1664
|
-
decide edit global:ui-001
|
|
1665
|
-
`);
|
|
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('');
|
|
1666
1699
|
}
|
|
1667
1700
|
/**
|
|
1668
1701
|
* Handle config command - view or set configuration options
|
|
@@ -1671,39 +1704,36 @@ async function handleConfig() {
|
|
|
1671
1704
|
const subCommand = args[1];
|
|
1672
1705
|
const value = args[2];
|
|
1673
1706
|
if (!subCommand) {
|
|
1674
|
-
// Show current config
|
|
1675
1707
|
const sensitivity = getSearchSensitivity();
|
|
1676
|
-
console.log(
|
|
1677
|
-
console.log(` search-sensitivity
|
|
1678
|
-
console.log(
|
|
1679
|
-
console.log(' search-sensitivity high|medium');
|
|
1680
|
-
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`);
|
|
1681
1711
|
return;
|
|
1682
1712
|
}
|
|
1683
1713
|
if (subCommand === 'search-sensitivity') {
|
|
1684
1714
|
if (!value) {
|
|
1685
1715
|
const current = getSearchSensitivity();
|
|
1686
|
-
console.log(`\n
|
|
1687
|
-
console.log(
|
|
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`);
|
|
1688
1718
|
return;
|
|
1689
1719
|
}
|
|
1690
1720
|
if (value !== 'high' && value !== 'medium') {
|
|
1691
|
-
console.error(
|
|
1721
|
+
console.error(` ${c.red}✗${c.reset} Invalid value. Use ${c.cyan}high${c.reset} or ${c.cyan}medium${c.reset}\n`);
|
|
1692
1722
|
process.exit(1);
|
|
1693
1723
|
}
|
|
1694
1724
|
setSearchSensitivity(value);
|
|
1695
|
-
console.log(`\n
|
|
1725
|
+
console.log(`\n ${c.green}✓${c.reset} Search sensitivity: ${c.cyan}${value}${c.reset}`);
|
|
1696
1726
|
if (value === 'high') {
|
|
1697
|
-
console.log(
|
|
1727
|
+
console.log(` ${c.dim}AI must search before any code change${c.reset}`);
|
|
1698
1728
|
}
|
|
1699
1729
|
else {
|
|
1700
|
-
console.log(
|
|
1730
|
+
console.log(` ${c.dim}AI searches for significant changes only${c.reset}`);
|
|
1701
1731
|
}
|
|
1702
|
-
console.log(
|
|
1732
|
+
console.log(`\n ${c.dim}Restart your MCP server for changes to take effect.${c.reset}\n`);
|
|
1703
1733
|
return;
|
|
1704
1734
|
}
|
|
1705
|
-
console.error(
|
|
1706
|
-
console.log(
|
|
1735
|
+
console.error(` ${c.red}✗${c.reset} Unknown option: ${subCommand}`);
|
|
1736
|
+
console.log(` ${c.dim}Available:${c.reset} search-sensitivity\n`);
|
|
1707
1737
|
process.exit(1);
|
|
1708
1738
|
}
|
|
1709
1739
|
main();
|
package/dist/mcp/server.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "decisionnode",
|
|
3
|
-
"version": "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://
|
|
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
|
-
"
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"test:watch": "vitest",
|
|
45
|
+
"prepublishOnly": "npm run build && npm run test"
|
|
44
46
|
},
|
|
45
47
|
"engines": {
|
|
46
|
-
"node": ">=
|
|
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
|
+
}
|