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 +18 -7
- package/dist/ai/rag.d.ts +13 -1
- package/dist/ai/rag.js +2 -0
- package/dist/cli.js +297 -282
- package/dist/mcp/server.js +1 -1
- package/package.json +9 -6
package/README.md
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="website/src/assets/images/DecisionNode-transparent.png" width="150" />
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
>
|
|
5
|
+
<h1 align="center">DecisionNode</h1>
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
29
|
+
decide init # creates project store
|
|
20
30
|
decide setup # configure Gemini API key (free tier)
|
|
21
|
-
```
|
|
22
31
|
|
|
23
|
-
|
|
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(
|
|
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,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
|
-
|
|
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
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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
|
-
|
|
712
|
-
console.log('
|
|
713
|
-
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('');
|
|
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
|
|
767
|
+
console.log(` ${c.gray}Current key:${c.reset} ${c.dim}${masked}${c.reset}`);
|
|
728
768
|
console.log('');
|
|
729
769
|
}
|
|
730
|
-
const
|
|
731
|
-
const
|
|
732
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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(
|
|
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(
|
|
813
|
+
console.log(` ${c.green}✓${c.reset} All decisions are embedded.\n`);
|
|
770
814
|
return;
|
|
771
815
|
}
|
|
772
|
-
console.log(`
|
|
773
|
-
unembedded.forEach(d => console.log(`
|
|
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(
|
|
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(
|
|
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(
|
|
786
|
-
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`);
|
|
787
831
|
process.exit(1);
|
|
788
832
|
}
|
|
789
833
|
}
|
|
790
834
|
async function handleCheck() {
|
|
791
|
-
console.log(
|
|
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
|
-
|
|
798
|
-
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}`}`);
|
|
799
842
|
if (projectMissing.length > 0) {
|
|
800
|
-
console.log(`
|
|
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
|
-
|
|
814
|
-
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}`}`);
|
|
815
856
|
if (globalMissing.length > 0) {
|
|
816
|
-
console.log(`
|
|
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}
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
878
|
+
console.log(` ${c.green}✓${c.reset} Removed ${result.vectorsRemoved} orphaned vectors`);
|
|
839
879
|
}
|
|
840
880
|
if (result.reviewsRemoved > 0) {
|
|
841
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
988
|
-
console.log(
|
|
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 ?
|
|
993
|
-
console.log(
|
|
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(
|
|
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
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
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(
|
|
1692
|
-
console.log(` search-sensitivity
|
|
1693
|
-
console.log(
|
|
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
|
|
1702
|
-
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`);
|
|
1703
1718
|
return;
|
|
1704
1719
|
}
|
|
1705
1720
|
if (value !== 'high' && value !== 'medium') {
|
|
1706
|
-
console.error(
|
|
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
|
|
1725
|
+
console.log(`\n ${c.green}✓${c.reset} Search sensitivity: ${c.cyan}${value}${c.reset}`);
|
|
1711
1726
|
if (value === 'high') {
|
|
1712
|
-
console.log(
|
|
1727
|
+
console.log(` ${c.dim}AI must search before any code change${c.reset}`);
|
|
1713
1728
|
}
|
|
1714
1729
|
else {
|
|
1715
|
-
console.log(
|
|
1730
|
+
console.log(` ${c.dim}AI searches for significant changes only${c.reset}`);
|
|
1716
1731
|
}
|
|
1717
|
-
console.log(
|
|
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(
|
|
1721
|
-
console.log(
|
|
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();
|
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
|
+
}
|