decisionnode 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 DecisionNode
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # DecisionNode
2
+
3
+ > Structured, queryable memory for development decisions. Stores architectural choices as vector embeddings, exposes them to AI agents via MCP.
4
+
5
+ ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
6
+ ![npm](https://img.shields.io/npm/v/decisionnode.svg)
7
+
8
+ ---
9
+
10
+ Your AI keeps forgetting what you agreed on. You say "use Tailwind, no CSS modules" — next session, it writes CSS modules. DecisionNode stores these decisions as vector embeddings so the AI can search them before writing code.
11
+
12
+ Not a markdown file. A queryable memory layer with semantic search.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm install -g decisionnode
18
+ cd your-project
19
+ decide init # creates project store + .mcp.json for AI clients
20
+ decide setup # configure Gemini API key (free tier)
21
+ ```
22
+
23
+ `decide init` creates a `.mcp.json` in your project — AI clients like Claude Code and Cursor connect automatically.
24
+
25
+ ## How it works
26
+
27
+ 1. **A decision is made** — via `decide add` or the AI calls `add_decision` through MCP
28
+ 2. **Embedded as a vector** — using Gemini's `gemini-embedding-001`, stored locally in `vectors.json`
29
+ 3. **AI retrieves it later** — calls `search_decisions` via MCP, gets back relevant decisions ranked by cosine similarity
30
+
31
+ The retrieval is explicit — the AI calls the MCP tool to search. Decisions are not injected into a system prompt.
32
+
33
+ ## Two interfaces
34
+
35
+ | | CLI (`decide`) | MCP Server (`decide-mcp`) |
36
+ |---|---|---|
37
+ | **For** | You | Your AI |
38
+ | **How** | Terminal commands | Structured JSON over MCP |
39
+ | **Does** | Setup, add, search, edit, deprecate, export, import, config | Search, add, update, delete, list, history |
40
+
41
+ Both read and write to the same local store (`~/.decisionnode/`).
42
+
43
+ ## Quick reference
44
+
45
+ ```bash
46
+ decide add # interactive add
47
+ decide add -s UI -d "Use Tailwind" # one-command add
48
+ decide add --global # applies to all projects
49
+ decide search "error handling" # semantic search
50
+ decide list # list all (includes global)
51
+ decide deprecate ui-003 # soft-delete (reversible)
52
+ decide activate ui-003 # bring it back
53
+ decide check # embedding health
54
+ decide embed # fix missing embeddings
55
+ decide export json > decisions.json # export to file
56
+ ```
57
+
58
+ ## Documentation
59
+
60
+ Full docs at [decisionnode.dev/docs](https://decisionnode.dev/docs)
61
+
62
+ - [Quickstart](https://decisionnode.dev/docs/quickstart)
63
+ - [CLI Reference](https://decisionnode.dev/docs/cli) — all commands
64
+ - [MCP Server](https://decisionnode.dev/docs/mcp) — 9 tools, setup for Claude/Cursor/Windsurf
65
+ - [Decision Nodes](https://decisionnode.dev/docs/decisions) — structure, fields, lifecycle
66
+ - [Context Engine](https://decisionnode.dev/docs/context) — embedding, search, conflict detection
67
+ - [Configuration](https://decisionnode.dev/docs/setup) — storage, search sensitivity, global decisions
68
+ - [Workflows](https://decisionnode.dev/docs/workflows) — common patterns
69
+
70
+ For LLM consumption: [decisionnode.dev/decisionnode-docs.md](https://decisionnode.dev/decisionnode-docs.md)
71
+
72
+ ## Contributing
73
+
74
+ The CLI and MCP server are just the start. There's a VS Code extension, a marketplace for shared decision packs, and cloud sync being worked on — contributions to any of these are welcome.
75
+
76
+ Whether it's a bug fix, a new feature, improving the docs, or just starring the repo — all help is appreciated. See [CONTRIBUTING.md](./CONTRIBUTING.md) for how to get started.
77
+
78
+ ## License
79
+
80
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Get embedding for text.
3
+ * Priority:
4
+ * 1. Pro subscriber -> Cloud embedding (server-side)
5
+ * 2. Local API key -> Local embedding
6
+ * 3. None -> Error
7
+ *
8
+ * @param text - Text to embed
9
+ * @param projectName - Optional project name for cloud context
10
+ */
11
+ export declare function getEmbedding(text: string, projectName?: string): Promise<number[]>;
12
+ /**
13
+ * Check if embedding is available (either local or cloud)
14
+ */
15
+ export declare function isEmbeddingAvailable(): Promise<boolean>;
@@ -0,0 +1,56 @@
1
+ import { GoogleGenerativeAI } from '@google/generative-ai';
2
+ import { getCloudEmbedding, isProSubscriber } from '../cloud.js';
3
+ // Get API key from environment (set by parent process or .env file)
4
+ const apiKey = process.env.GEMINI_API_KEY;
5
+ // We don't throw yet because the user might just be listing decisions
6
+ // We'll check again when they try to use an AI feature
7
+ const genAI = apiKey ? new GoogleGenerativeAI(apiKey) : null;
8
+ const model = genAI ? genAI.getGenerativeModel({ model: "gemini-embedding-001" }) : null;
9
+ /**
10
+ * Get embedding for text.
11
+ * Priority:
12
+ * 1. Pro subscriber -> Cloud embedding (server-side)
13
+ * 2. Local API key -> Local embedding
14
+ * 3. None -> Error
15
+ *
16
+ * @param text - Text to embed
17
+ * @param projectName - Optional project name for cloud context
18
+ */
19
+ export async function getEmbedding(text, projectName) {
20
+ // 1. Check if user is Pro - use cloud embedding
21
+ // This allows Pro users to use the tool with ZERO config (no local API key)
22
+ try {
23
+ const isPro = await isProSubscriber();
24
+ if (isPro) {
25
+ const cloudEmbedding = await getCloudEmbedding(text, projectName);
26
+ if (cloudEmbedding) {
27
+ return cloudEmbedding;
28
+ }
29
+ // If cloud fails (e.g. network error), fall back to local if available
30
+ }
31
+ }
32
+ catch {
33
+ // Build/env error, ignore and try local
34
+ }
35
+ // 2. If local API key is available, use it
36
+ if (model) {
37
+ try {
38
+ const result = await model.embedContent(text);
39
+ return result.embedding.values;
40
+ }
41
+ catch (error) {
42
+ throw error;
43
+ }
44
+ }
45
+ // 3. No key available
46
+ throw new Error("No Gemini API key configured.\n" +
47
+ "Run: decide setup");
48
+ }
49
+ /**
50
+ * Check if embedding is available (either local or cloud)
51
+ */
52
+ export async function isEmbeddingAvailable() {
53
+ if (model)
54
+ return true;
55
+ return await isProSubscriber();
56
+ }
@@ -0,0 +1,79 @@
1
+ import { DecisionNode } from '../types.js';
2
+ interface VectorEntry {
3
+ vector: number[];
4
+ embeddedAt: string;
5
+ }
6
+ type VectorCache = Record<string, VectorEntry | number[]>;
7
+ interface ScoredDecision {
8
+ decision: DecisionNode;
9
+ score: number;
10
+ }
11
+ /**
12
+ * Load the vector cache from disk
13
+ */
14
+ export declare function loadVectorCache(): Promise<VectorCache>;
15
+ /**
16
+ * Save the vector cache to disk
17
+ */
18
+ export declare function saveVectorCache(cache: VectorCache): Promise<void>;
19
+ /**
20
+ * Load the global vector cache from disk
21
+ */
22
+ export declare function loadGlobalVectorCache(): Promise<VectorCache>;
23
+ /**
24
+ * Save the global vector cache to disk
25
+ */
26
+ export declare function saveGlobalVectorCache(cache: VectorCache): Promise<void>;
27
+ /**
28
+ * Embed a single global decision
29
+ * Throws on failure so callers can report embedding status.
30
+ */
31
+ export declare function embedGlobalDecision(decision: DecisionNode): Promise<void>;
32
+ /**
33
+ * Clear the embedding for a deleted global decision
34
+ */
35
+ export declare function clearGlobalEmbedding(decisionId: string): Promise<void>;
36
+ /**
37
+ * Embed a single decision immediately.
38
+ * Called automatically when decisions are added or updated.
39
+ * Throws on failure so callers can report embedding status.
40
+ */
41
+ export declare function embedDecision(decision: DecisionNode): Promise<void>;
42
+ /**
43
+ * Clear the embedding for a deleted decision
44
+ */
45
+ export declare function clearEmbedding(decisionId: string): Promise<void>;
46
+ /**
47
+ * Update embedding for a renamed decision ID
48
+ */
49
+ export declare function renameEmbedding(oldId: string, newId: string): Promise<void>;
50
+ /**
51
+ * Batch embed multiple decisions (used for import)
52
+ */
53
+ export declare function embedDecisions(decisions: DecisionNode[]): Promise<{
54
+ success: number;
55
+ failed: number;
56
+ }>;
57
+ /**
58
+ * Find the most relevant decisions for a given query
59
+ * Automatically includes global decisions in results
60
+ */
61
+ export declare function findRelevantDecisions(query: string, limit?: number): Promise<ScoredDecision[]>;
62
+ /**
63
+ * Get decisions that are missing embeddings
64
+ */
65
+ export declare function getUnembeddedDecisions(): Promise<DecisionNode[]>;
66
+ /**
67
+ * Embed all unembedded decisions
68
+ * Returns list of embedded decision IDs
69
+ */
70
+ export declare function embedAllDecisions(): Promise<{
71
+ embedded: string[];
72
+ failed: string[];
73
+ }>;
74
+ /**
75
+ * Find potential conflicts with existing decisions
76
+ * Uses semantic similarity to find decisions that might contradict a new one
77
+ */
78
+ export declare function findPotentialConflicts(newDecisionText: string, threshold?: number): Promise<ScoredDecision[]>;
79
+ export {};
package/dist/ai/rag.js ADDED
@@ -0,0 +1,268 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { listDecisions, listGlobalDecisions } from '../store.js';
4
+ import { getEmbedding } from './gemini.js';
5
+ import { getProjectRoot, ensureProjectFolder, getGlobalDecisionsPath, ensureGlobalFolder } from '../env.js';
6
+ // getProjectRoot() returns ~/.decisionnode/.decisions/{projectname}/
7
+ const VECTORS_FILE = () => path.join(getProjectRoot(), 'vectors.json');
8
+ /**
9
+ * Load the vector cache from disk
10
+ */
11
+ export async function loadVectorCache() {
12
+ try {
13
+ const content = await fs.readFile(VECTORS_FILE(), 'utf-8');
14
+ return JSON.parse(content);
15
+ }
16
+ catch {
17
+ return {};
18
+ }
19
+ }
20
+ /**
21
+ * Save the vector cache to disk
22
+ */
23
+ export async function saveVectorCache(cache) {
24
+ ensureProjectFolder();
25
+ await fs.writeFile(VECTORS_FILE(), JSON.stringify(cache, null, 2), 'utf-8');
26
+ }
27
+ /**
28
+ * Get the vector from a cache entry (handles both legacy and new format)
29
+ */
30
+ function getVectorFromEntry(entry) {
31
+ if (Array.isArray(entry))
32
+ return entry;
33
+ return entry.vector;
34
+ }
35
+ // Global vectors file path
36
+ const GLOBAL_VECTORS_FILE = () => path.join(getGlobalDecisionsPath(), 'vectors.json');
37
+ /**
38
+ * Load the global vector cache from disk
39
+ */
40
+ export async function loadGlobalVectorCache() {
41
+ try {
42
+ const content = await fs.readFile(GLOBAL_VECTORS_FILE(), 'utf-8');
43
+ return JSON.parse(content);
44
+ }
45
+ catch {
46
+ return {};
47
+ }
48
+ }
49
+ /**
50
+ * Save the global vector cache to disk
51
+ */
52
+ export async function saveGlobalVectorCache(cache) {
53
+ ensureGlobalFolder();
54
+ await fs.writeFile(GLOBAL_VECTORS_FILE(), JSON.stringify(cache, null, 2), 'utf-8');
55
+ }
56
+ /**
57
+ * Embed a single global decision
58
+ * Throws on failure so callers can report embedding status.
59
+ */
60
+ export async function embedGlobalDecision(decision) {
61
+ const cache = await loadGlobalVectorCache();
62
+ const text = getDecisionText(decision);
63
+ const embedding = await getEmbedding(text);
64
+ cache[decision.id] = {
65
+ vector: embedding,
66
+ embeddedAt: new Date().toISOString()
67
+ };
68
+ await saveGlobalVectorCache(cache);
69
+ }
70
+ /**
71
+ * Clear the embedding for a deleted global decision
72
+ */
73
+ export async function clearGlobalEmbedding(decisionId) {
74
+ try {
75
+ const cache = await loadGlobalVectorCache();
76
+ delete cache[decisionId];
77
+ await saveGlobalVectorCache(cache);
78
+ }
79
+ catch {
80
+ // Silently fail
81
+ }
82
+ }
83
+ /**
84
+ * Generate the text representation of a decision for embedding
85
+ */
86
+ function getDecisionText(decision) {
87
+ return `${decision.scope}: ${decision.decision}. ${decision.rationale || ''} ${decision.constraints?.join(' ') || ''}`;
88
+ }
89
+ /**
90
+ * Calculate Cosine Similarity between two vectors
91
+ */
92
+ function cosineSimilarity(vecA, vecB) {
93
+ const dotProduct = vecA.reduce((acc, val, i) => acc + val * vecB[i], 0);
94
+ const magnitudeA = Math.sqrt(vecA.reduce((acc, val) => acc + val * val, 0));
95
+ const magnitudeB = Math.sqrt(vecB.reduce((acc, val) => acc + val * val, 0));
96
+ return dotProduct / (magnitudeA * magnitudeB);
97
+ }
98
+ /**
99
+ * Embed a single decision immediately.
100
+ * Called automatically when decisions are added or updated.
101
+ * Throws on failure so callers can report embedding status.
102
+ */
103
+ export async function embedDecision(decision) {
104
+ const cache = await loadVectorCache();
105
+ const text = getDecisionText(decision);
106
+ const embedding = await getEmbedding(text);
107
+ cache[decision.id] = {
108
+ vector: embedding,
109
+ embeddedAt: new Date().toISOString()
110
+ };
111
+ await saveVectorCache(cache);
112
+ }
113
+ /**
114
+ * Clear the embedding for a deleted decision
115
+ */
116
+ export async function clearEmbedding(decisionId) {
117
+ try {
118
+ const cache = await loadVectorCache();
119
+ delete cache[decisionId];
120
+ await saveVectorCache(cache);
121
+ }
122
+ catch {
123
+ // Silently fail
124
+ }
125
+ }
126
+ /**
127
+ * Update embedding for a renamed decision ID
128
+ */
129
+ export async function renameEmbedding(oldId, newId) {
130
+ try {
131
+ const cache = await loadVectorCache();
132
+ if (cache[oldId]) {
133
+ cache[newId] = cache[oldId];
134
+ delete cache[oldId];
135
+ await saveVectorCache(cache);
136
+ }
137
+ }
138
+ catch {
139
+ // Silently fail
140
+ }
141
+ }
142
+ /**
143
+ * Batch embed multiple decisions (used for import)
144
+ */
145
+ export async function embedDecisions(decisions) {
146
+ const cache = await loadVectorCache();
147
+ let success = 0;
148
+ let failed = 0;
149
+ for (const decision of decisions) {
150
+ try {
151
+ // Rate limit protection
152
+ await new Promise(r => setTimeout(r, 500));
153
+ const text = getDecisionText(decision);
154
+ const embedding = await getEmbedding(text);
155
+ cache[decision.id] = {
156
+ vector: embedding,
157
+ embeddedAt: new Date().toISOString()
158
+ };
159
+ success++;
160
+ }
161
+ catch {
162
+ failed++;
163
+ }
164
+ }
165
+ await saveVectorCache(cache);
166
+ return { success, failed };
167
+ }
168
+ /**
169
+ * Find the most relevant decisions for a given query
170
+ * Automatically includes global decisions in results
171
+ */
172
+ export async function findRelevantDecisions(query, limit = 3) {
173
+ const cache = await loadVectorCache();
174
+ const globalCache = await loadGlobalVectorCache();
175
+ const queryEmbedding = await getEmbedding(query);
176
+ // Project decisions
177
+ const allDecisions = await listDecisions();
178
+ const activeDecisions = allDecisions.filter(d => d.status === 'active');
179
+ // Global decisions (already prefixed with "global:")
180
+ const globalDecisions = await listGlobalDecisions();
181
+ const activeGlobalDecisions = globalDecisions.filter(d => d.status === 'active');
182
+ const scores = [];
183
+ // Score project decisions
184
+ for (const decision of activeDecisions) {
185
+ if (cache[decision.id]) {
186
+ const score = cosineSimilarity(queryEmbedding, getVectorFromEntry(cache[decision.id]));
187
+ scores.push({ decision, score });
188
+ }
189
+ }
190
+ // Score global decisions (stored without prefix in cache)
191
+ for (const decision of activeGlobalDecisions) {
192
+ const rawId = decision.id.replace(/^global:/, '');
193
+ if (globalCache[rawId]) {
194
+ const score = cosineSimilarity(queryEmbedding, getVectorFromEntry(globalCache[rawId]));
195
+ scores.push({ decision, score });
196
+ }
197
+ }
198
+ return scores
199
+ .sort((a, b) => b.score - a.score)
200
+ .slice(0, limit);
201
+ }
202
+ /**
203
+ * Get decisions that are missing embeddings
204
+ */
205
+ export async function getUnembeddedDecisions() {
206
+ const cache = await loadVectorCache();
207
+ const allDecisions = await listDecisions();
208
+ return allDecisions.filter(d => !cache[d.id]);
209
+ }
210
+ /**
211
+ * Embed all unembedded decisions
212
+ * Returns list of embedded decision IDs
213
+ */
214
+ export async function embedAllDecisions() {
215
+ const unembedded = await getUnembeddedDecisions();
216
+ const embedded = [];
217
+ const failed = [];
218
+ if (unembedded.length === 0) {
219
+ return { embedded, failed };
220
+ }
221
+ const cache = await loadVectorCache();
222
+ for (const decision of unembedded) {
223
+ try {
224
+ await new Promise(r => setTimeout(r, 500));
225
+ const text = getDecisionText(decision);
226
+ const embedding = await getEmbedding(text);
227
+ cache[decision.id] = {
228
+ vector: embedding,
229
+ embeddedAt: new Date().toISOString()
230
+ };
231
+ embedded.push(decision.id);
232
+ }
233
+ catch {
234
+ failed.push(decision.id);
235
+ }
236
+ }
237
+ if (embedded.length > 0) {
238
+ await saveVectorCache(cache);
239
+ }
240
+ return { embedded, failed };
241
+ }
242
+ /**
243
+ * Find potential conflicts with existing decisions
244
+ * Uses semantic similarity to find decisions that might contradict a new one
245
+ */
246
+ export async function findPotentialConflicts(newDecisionText, threshold = 0.75) {
247
+ try {
248
+ const cache = await loadVectorCache();
249
+ if (Object.keys(cache).length === 0)
250
+ return [];
251
+ const newEmbedding = await getEmbedding(newDecisionText);
252
+ const allDecisions = await listDecisions();
253
+ const activeDecisions = allDecisions.filter(d => d.status === 'active');
254
+ const conflicts = [];
255
+ for (const decision of activeDecisions) {
256
+ if (cache[decision.id]) {
257
+ const score = cosineSimilarity(newEmbedding, getVectorFromEntry(cache[decision.id]));
258
+ if (score >= threshold) {
259
+ conflicts.push({ decision, score });
260
+ }
261
+ }
262
+ }
263
+ return conflicts.sort((a, b) => b.score - a.score);
264
+ }
265
+ catch {
266
+ return []; // API key not set or other error
267
+ }
268
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};