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 +21 -0
- package/README.md +80 -0
- package/dist/ai/gemini.d.ts +15 -0
- package/dist/ai/gemini.js +56 -0
- package/dist/ai/rag.d.ts +79 -0
- package/dist/ai/rag.js +268 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1724 -0
- package/dist/cloud.d.ts +177 -0
- package/dist/cloud.js +631 -0
- package/dist/env.d.ts +47 -0
- package/dist/env.js +139 -0
- package/dist/history.d.ts +34 -0
- package/dist/history.js +159 -0
- package/dist/maintenance.d.ts +7 -0
- package/dist/maintenance.js +49 -0
- package/dist/marketplace.d.ts +46 -0
- package/dist/marketplace.js +300 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.js +621 -0
- package/dist/setup.d.ts +6 -0
- package/dist/setup.js +132 -0
- package/dist/store.d.ts +126 -0
- package/dist/store.js +555 -0
- package/dist/types.d.ts +60 -0
- package/dist/types.js +9 -0
- package/package.json +57 -0
package/dist/store.js
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { getProjectRoot, GLOBAL_STORE, GLOBAL_PROJECT_NAME, getGlobalDecisionsPath, ensureGlobalFolder, stripGlobalPrefix } from './env.js';
|
|
4
|
+
import { embedDecision, clearEmbedding, renameEmbedding, embedDecisions, embedGlobalDecision, clearGlobalEmbedding } from './ai/rag.js';
|
|
5
|
+
import { logAction } from './history.js';
|
|
6
|
+
import { syncDecisionsToCloud, deleteDecisionFromCloud, getAutoSyncEnabled } from './cloud.js';
|
|
7
|
+
// getProjectRoot() returns ~/.decisionnode/.decisions/{projectname}/
|
|
8
|
+
// Files go directly there: ui.json, backend.json, vectors.json, history/
|
|
9
|
+
/**
|
|
10
|
+
* Get all available scopes by scanning the .decisions directory
|
|
11
|
+
*/
|
|
12
|
+
export async function getAvailableScopes() {
|
|
13
|
+
try {
|
|
14
|
+
const files = await fs.readdir(getProjectRoot());
|
|
15
|
+
return files
|
|
16
|
+
.filter(f => f.endsWith('.json') && f !== 'vectors.json' && f !== 'reviewed.json' && f !== 'sync-metadata.json' && f !== 'incoming.json')
|
|
17
|
+
.map(f => f.replace('.json', ''))
|
|
18
|
+
.map(s => s.charAt(0).toUpperCase() + s.slice(1));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Get all available scopes in the global decisions folder
|
|
26
|
+
*/
|
|
27
|
+
export async function getGlobalScopes() {
|
|
28
|
+
try {
|
|
29
|
+
const globalPath = getGlobalDecisionsPath();
|
|
30
|
+
const files = await fs.readdir(globalPath);
|
|
31
|
+
return files
|
|
32
|
+
.filter(f => f.endsWith('.json') && f !== 'vectors.json' && f !== 'reviewed.json' && f !== 'sync-metadata.json' && f !== 'incoming.json')
|
|
33
|
+
.map(f => f.replace('.json', ''))
|
|
34
|
+
.map(s => s.charAt(0).toUpperCase() + s.slice(1));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Get the path to a decision file in the global folder
|
|
42
|
+
*/
|
|
43
|
+
function getGlobalDecisionFilePath(scope) {
|
|
44
|
+
const cleanScope = scope.toLowerCase()
|
|
45
|
+
.replace('.json', '')
|
|
46
|
+
.replace(/[\/\\]/g, '_')
|
|
47
|
+
.replace(/[^a-z0-9_\-]/g, '_');
|
|
48
|
+
return path.join(getGlobalDecisionsPath(), `${cleanScope}.json`);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Load all decisions for a given scope from the global folder
|
|
52
|
+
*/
|
|
53
|
+
export async function loadGlobalDecisions(scope) {
|
|
54
|
+
const filePath = getGlobalDecisionFilePath(scope);
|
|
55
|
+
try {
|
|
56
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
57
|
+
return JSON.parse(content);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error.code === 'ENOENT') {
|
|
61
|
+
return { scope, decisions: [] };
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Save decisions for a given scope in the global folder
|
|
68
|
+
*/
|
|
69
|
+
export async function saveGlobalDecisions(collection) {
|
|
70
|
+
const filePath = getGlobalDecisionFilePath(collection.scope);
|
|
71
|
+
ensureGlobalFolder();
|
|
72
|
+
await fs.writeFile(filePath, JSON.stringify(collection, null, 2), 'utf-8');
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* List all global decisions, optionally filtered by scope
|
|
76
|
+
* Returns decisions with "global:" prefix on IDs
|
|
77
|
+
*/
|
|
78
|
+
export async function listGlobalDecisions(scope) {
|
|
79
|
+
const scopes = scope
|
|
80
|
+
? [scope]
|
|
81
|
+
: await getGlobalScopes();
|
|
82
|
+
const allDecisions = [];
|
|
83
|
+
for (const s of scopes) {
|
|
84
|
+
const collection = await loadGlobalDecisions(s);
|
|
85
|
+
if (collection.decisions && Array.isArray(collection.decisions)) {
|
|
86
|
+
// Prefix IDs with "global:" for display
|
|
87
|
+
const prefixed = collection.decisions.map(d => ({
|
|
88
|
+
...d,
|
|
89
|
+
id: d.id.startsWith('global:') ? d.id : `global:${d.id}`,
|
|
90
|
+
}));
|
|
91
|
+
allDecisions.push(...prefixed);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return allDecisions;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Get a global decision by ID (with or without "global:" prefix)
|
|
98
|
+
*/
|
|
99
|
+
export async function getGlobalDecisionById(id) {
|
|
100
|
+
const rawId = stripGlobalPrefix(id);
|
|
101
|
+
const scopes = await getGlobalScopes();
|
|
102
|
+
for (const scope of scopes) {
|
|
103
|
+
const collection = await loadGlobalDecisions(scope);
|
|
104
|
+
const decision = collection.decisions.find(d => d.id === rawId);
|
|
105
|
+
if (decision) {
|
|
106
|
+
return { ...decision, id: `global:${decision.id}` };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Generate the next decision ID for a global scope
|
|
113
|
+
* Returns ID without the "global:" prefix (prefix is added on read)
|
|
114
|
+
*/
|
|
115
|
+
export async function getNextGlobalDecisionId(scope) {
|
|
116
|
+
const collection = await loadGlobalDecisions(scope);
|
|
117
|
+
const prefix = scope.toLowerCase().replace(/[^a-z]/g, '').substring(0, 10);
|
|
118
|
+
let maxNum = 0;
|
|
119
|
+
for (const d of collection.decisions) {
|
|
120
|
+
const match = d.id.match(/-([0-9]+)$/);
|
|
121
|
+
if (match) {
|
|
122
|
+
const num = parseInt(match[1], 10);
|
|
123
|
+
if (num > maxNum)
|
|
124
|
+
maxNum = num;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return `${prefix}-${String(maxNum + 1).padStart(3, '0')}`;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Add a new global decision
|
|
131
|
+
*/
|
|
132
|
+
export async function addGlobalDecision(decision, source = 'cli') {
|
|
133
|
+
const normalizedDecision = { ...decision, scope: normalizeScope(decision.scope) };
|
|
134
|
+
// Store without the "global:" prefix in the file
|
|
135
|
+
const rawId = stripGlobalPrefix(normalizedDecision.id);
|
|
136
|
+
const storedDecision = { ...normalizedDecision, id: rawId };
|
|
137
|
+
const collection = await loadGlobalDecisions(normalizedDecision.scope);
|
|
138
|
+
if (collection.decisions.some(d => d.id === rawId)) {
|
|
139
|
+
throw new Error(`Global decision with ID ${rawId} already exists`);
|
|
140
|
+
}
|
|
141
|
+
collection.decisions.push(storedDecision);
|
|
142
|
+
await saveGlobalDecisions(collection);
|
|
143
|
+
// Auto-embed using global vectors
|
|
144
|
+
let embedded = false;
|
|
145
|
+
try {
|
|
146
|
+
await embedGlobalDecision(storedDecision);
|
|
147
|
+
embedded = true;
|
|
148
|
+
}
|
|
149
|
+
catch { }
|
|
150
|
+
// Log the action
|
|
151
|
+
await logAction('added', `global:${rawId}`, `Added global decision global:${rawId}`, source);
|
|
152
|
+
return { embedded };
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Update an existing global decision
|
|
156
|
+
*/
|
|
157
|
+
export async function updateGlobalDecision(id, updates) {
|
|
158
|
+
const rawId = stripGlobalPrefix(id);
|
|
159
|
+
const scopes = await getGlobalScopes();
|
|
160
|
+
for (const scope of scopes) {
|
|
161
|
+
const collection = await loadGlobalDecisions(scope);
|
|
162
|
+
const index = collection.decisions.findIndex(d => d.id === rawId);
|
|
163
|
+
if (index !== -1) {
|
|
164
|
+
const updated = {
|
|
165
|
+
...collection.decisions[index],
|
|
166
|
+
...updates,
|
|
167
|
+
id: rawId,
|
|
168
|
+
updatedAt: new Date().toISOString()
|
|
169
|
+
};
|
|
170
|
+
collection.decisions[index] = updated;
|
|
171
|
+
await saveGlobalDecisions(collection);
|
|
172
|
+
embedGlobalDecision(updated).catch(() => { });
|
|
173
|
+
await logAction('updated', `global:${rawId}`, `Updated global decision global:${rawId}`);
|
|
174
|
+
return { ...updated, id: `global:${rawId}` };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Delete a global decision
|
|
181
|
+
*/
|
|
182
|
+
export async function deleteGlobalDecision(id) {
|
|
183
|
+
const rawId = stripGlobalPrefix(id);
|
|
184
|
+
const scopes = await getGlobalScopes();
|
|
185
|
+
for (const scope of scopes) {
|
|
186
|
+
const collection = await loadGlobalDecisions(scope);
|
|
187
|
+
const index = collection.decisions.findIndex(d => d.id === rawId);
|
|
188
|
+
if (index !== -1) {
|
|
189
|
+
collection.decisions.splice(index, 1);
|
|
190
|
+
if (collection.decisions.length === 0) {
|
|
191
|
+
try {
|
|
192
|
+
const filePath = getGlobalDecisionFilePath(scope);
|
|
193
|
+
await fs.unlink(filePath);
|
|
194
|
+
}
|
|
195
|
+
catch { }
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
await saveGlobalDecisions(collection);
|
|
199
|
+
}
|
|
200
|
+
clearGlobalEmbedding(rawId).catch(() => { });
|
|
201
|
+
await logAction('deleted', `global:${rawId}`, `Deleted global decision global:${rawId}`);
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Normalize scope to consistent capitalized format
|
|
209
|
+
* e.g., 'ui', 'UI', 'Ui', 'uI' all become 'UI'
|
|
210
|
+
* e.g., 'backend', 'Backend', 'BACKEND' all become 'Backend'
|
|
211
|
+
*/
|
|
212
|
+
export function normalizeScope(scope) {
|
|
213
|
+
// Convert to lowercase, then capitalize first letter
|
|
214
|
+
const lower = scope.toLowerCase();
|
|
215
|
+
return lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* List all available projects in the global store
|
|
219
|
+
* Returns project name, decision count, and available scopes
|
|
220
|
+
*/
|
|
221
|
+
export async function listProjects() {
|
|
222
|
+
const projects = [];
|
|
223
|
+
try {
|
|
224
|
+
const dirs = await fs.readdir(GLOBAL_STORE, { withFileTypes: true });
|
|
225
|
+
for (const dir of dirs) {
|
|
226
|
+
if (!dir.isDirectory())
|
|
227
|
+
continue;
|
|
228
|
+
// Skip the _global folder — it's not a project
|
|
229
|
+
if (dir.name === GLOBAL_PROJECT_NAME)
|
|
230
|
+
continue;
|
|
231
|
+
const projectPath = path.join(GLOBAL_STORE, dir.name);
|
|
232
|
+
const projectInfo = {
|
|
233
|
+
name: dir.name,
|
|
234
|
+
decisionCount: 0,
|
|
235
|
+
scopes: [],
|
|
236
|
+
};
|
|
237
|
+
try {
|
|
238
|
+
const files = await fs.readdir(projectPath);
|
|
239
|
+
const jsonFiles = files.filter(f => f.endsWith('.json') && f !== 'vectors.json' && f !== 'reviewed.json' && f !== 'sync-metadata.json' && f !== 'incoming.json');
|
|
240
|
+
for (const jsonFile of jsonFiles) {
|
|
241
|
+
const filePath = path.join(projectPath, jsonFile);
|
|
242
|
+
try {
|
|
243
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
244
|
+
const collection = JSON.parse(content);
|
|
245
|
+
projectInfo.decisionCount += collection.decisions.length;
|
|
246
|
+
projectInfo.scopes.push(collection.scope);
|
|
247
|
+
// Get last modified time
|
|
248
|
+
const stat = await fs.stat(filePath);
|
|
249
|
+
if (!projectInfo.lastModified || stat.mtime.toISOString() > projectInfo.lastModified) {
|
|
250
|
+
projectInfo.lastModified = stat.mtime.toISOString();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
// Skip invalid files
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
// Skip inaccessible projects
|
|
260
|
+
}
|
|
261
|
+
projects.push(projectInfo);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
// Global store doesn't exist yet
|
|
266
|
+
}
|
|
267
|
+
// Sort by decision count (most decisions first)
|
|
268
|
+
return projects.sort((a, b) => b.decisionCount - a.decisionCount);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Get the path to a decision file for a given scope
|
|
272
|
+
*/
|
|
273
|
+
function getDecisionFilePath(scope) {
|
|
274
|
+
const cleanScope = scope.toLowerCase()
|
|
275
|
+
.replace('.json', '')
|
|
276
|
+
.replace(/[\/\\]/g, '_')
|
|
277
|
+
.replace(/[^a-z0-9_\-]/g, '_');
|
|
278
|
+
return path.join(getProjectRoot(), `${cleanScope}.json`);
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Load all decisions for a given scope
|
|
282
|
+
*/
|
|
283
|
+
export async function loadDecisions(scope) {
|
|
284
|
+
const filePath = getDecisionFilePath(scope);
|
|
285
|
+
try {
|
|
286
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
287
|
+
return JSON.parse(content);
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
if (error.code === 'ENOENT') {
|
|
291
|
+
return { scope, decisions: [] };
|
|
292
|
+
}
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Save decisions for a given scope
|
|
298
|
+
*/
|
|
299
|
+
export async function saveDecisions(collection) {
|
|
300
|
+
const filePath = getDecisionFilePath(collection.scope);
|
|
301
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
302
|
+
await fs.writeFile(filePath, JSON.stringify(collection, null, 2), 'utf-8');
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Get a single decision by ID across all scopes
|
|
306
|
+
*/
|
|
307
|
+
export async function getDecisionById(id) {
|
|
308
|
+
const scopes = await getAvailableScopes();
|
|
309
|
+
for (const scope of scopes) {
|
|
310
|
+
const collection = await loadDecisions(scope);
|
|
311
|
+
const decision = collection.decisions.find(d => d.id === id);
|
|
312
|
+
if (decision) {
|
|
313
|
+
return decision;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* List all decisions, optionally filtered by scope
|
|
320
|
+
*/
|
|
321
|
+
export async function listDecisions(scope) {
|
|
322
|
+
const scopes = scope
|
|
323
|
+
? [scope]
|
|
324
|
+
: await getAvailableScopes();
|
|
325
|
+
const allDecisions = [];
|
|
326
|
+
for (const s of scopes) {
|
|
327
|
+
const collection = await loadDecisions(s);
|
|
328
|
+
if (collection.decisions && Array.isArray(collection.decisions)) {
|
|
329
|
+
allDecisions.push(...collection.decisions);
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
console.warn(`⚠️ Warning: Scope '${s}' is corrupted or empty (missing 'decisions' array). Skipping.`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return allDecisions;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Add a new decision
|
|
339
|
+
* Auto-embeds and logs the action
|
|
340
|
+
* @param decision - The decision to add
|
|
341
|
+
* @param source - Where this action originated (default: 'cli')
|
|
342
|
+
*/
|
|
343
|
+
export async function addDecision(decision, source = 'cli') {
|
|
344
|
+
// Normalize scope to consistent capitalization
|
|
345
|
+
const normalizedDecision = { ...decision, scope: normalizeScope(decision.scope) };
|
|
346
|
+
const collection = await loadDecisions(normalizedDecision.scope);
|
|
347
|
+
if (collection.decisions.some(d => d.id === normalizedDecision.id)) {
|
|
348
|
+
throw new Error(`Decision with ID ${normalizedDecision.id} already exists`);
|
|
349
|
+
}
|
|
350
|
+
collection.decisions.push(normalizedDecision);
|
|
351
|
+
await saveDecisions(collection);
|
|
352
|
+
// Auto-embed — await to report success/failure
|
|
353
|
+
let embedded = false;
|
|
354
|
+
try {
|
|
355
|
+
await embedDecision(normalizedDecision);
|
|
356
|
+
embedded = true;
|
|
357
|
+
}
|
|
358
|
+
catch { }
|
|
359
|
+
// Auto-sync to cloud (async, non-blocking, Pro only, if enabled)
|
|
360
|
+
getAutoSyncEnabled().then(enabled => {
|
|
361
|
+
if (enabled)
|
|
362
|
+
syncDecisionsToCloud(path.basename(getProjectRoot()), [normalizedDecision]).catch(() => { });
|
|
363
|
+
});
|
|
364
|
+
// Log the action with source
|
|
365
|
+
await logAction('added', normalizedDecision.id, `Added ${normalizedDecision.id}`, source);
|
|
366
|
+
return { embedded };
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Update an existing decision
|
|
370
|
+
* Auto-embeds and logs the action
|
|
371
|
+
*/
|
|
372
|
+
export async function updateDecision(id, updates) {
|
|
373
|
+
const scopes = await getAvailableScopes();
|
|
374
|
+
for (const scope of scopes) {
|
|
375
|
+
const collection = await loadDecisions(scope);
|
|
376
|
+
const index = collection.decisions.findIndex(d => d.id === id);
|
|
377
|
+
if (index !== -1) {
|
|
378
|
+
const updated = {
|
|
379
|
+
...collection.decisions[index],
|
|
380
|
+
...updates,
|
|
381
|
+
id,
|
|
382
|
+
updatedAt: new Date().toISOString()
|
|
383
|
+
};
|
|
384
|
+
collection.decisions[index] = updated;
|
|
385
|
+
await saveDecisions(collection);
|
|
386
|
+
// Auto-embed (async, non-blocking)
|
|
387
|
+
embedDecision(updated).catch(() => { });
|
|
388
|
+
// Auto-sync to cloud (async, non-blocking, Pro only, if enabled)
|
|
389
|
+
getAutoSyncEnabled().then(enabled => {
|
|
390
|
+
if (enabled)
|
|
391
|
+
syncDecisionsToCloud(path.basename(getProjectRoot()), [updated]).catch(() => { });
|
|
392
|
+
});
|
|
393
|
+
// Log the action
|
|
394
|
+
await logAction('updated', id, `Updated ${id}`);
|
|
395
|
+
return updated;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Delete a decision by ID
|
|
402
|
+
* Clears embedding and logs the action
|
|
403
|
+
*/
|
|
404
|
+
export async function deleteDecision(id) {
|
|
405
|
+
const scopes = await getAvailableScopes();
|
|
406
|
+
for (const scope of scopes) {
|
|
407
|
+
const collection = await loadDecisions(scope);
|
|
408
|
+
const index = collection.decisions.findIndex(d => d.id === id);
|
|
409
|
+
if (index !== -1) {
|
|
410
|
+
const deleted = collection.decisions[index];
|
|
411
|
+
collection.decisions.splice(index, 1);
|
|
412
|
+
if (collection.decisions.length === 0) {
|
|
413
|
+
// Remove empty scope file
|
|
414
|
+
try {
|
|
415
|
+
const filePath = getDecisionFilePath(scope);
|
|
416
|
+
await fs.unlink(filePath);
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
// Ignore delete error (e.g. file already gone)
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
await saveDecisions(collection);
|
|
424
|
+
}
|
|
425
|
+
// Clear embedding
|
|
426
|
+
clearEmbedding(id).catch(() => { });
|
|
427
|
+
// Auto-delete from cloud (async, non-blocking, Pro only, if enabled)
|
|
428
|
+
getAutoSyncEnabled().then(enabled => {
|
|
429
|
+
if (enabled)
|
|
430
|
+
deleteDecisionFromCloud(id).catch(() => { });
|
|
431
|
+
});
|
|
432
|
+
// Log the action
|
|
433
|
+
await logAction('deleted', id, `Deleted ${id}`);
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Delete an entire scope (all decisions within it)
|
|
441
|
+
* Deletes the scope file, embeddings, and optionally from cloud
|
|
442
|
+
*/
|
|
443
|
+
export async function deleteScope(scope) {
|
|
444
|
+
const normalizedScope = normalizeScope(scope);
|
|
445
|
+
const collection = await loadDecisions(normalizedScope);
|
|
446
|
+
if (!collection.decisions || collection.decisions.length === 0) {
|
|
447
|
+
return { deleted: 0, decisionIds: [] };
|
|
448
|
+
}
|
|
449
|
+
const decisionIds = collection.decisions.map(d => d.id);
|
|
450
|
+
const count = decisionIds.length;
|
|
451
|
+
// Delete the scope file
|
|
452
|
+
try {
|
|
453
|
+
const filePath = getDecisionFilePath(normalizedScope);
|
|
454
|
+
await fs.unlink(filePath);
|
|
455
|
+
}
|
|
456
|
+
catch {
|
|
457
|
+
// Ignore if file doesn't exist
|
|
458
|
+
}
|
|
459
|
+
// Clear embeddings for all decisions
|
|
460
|
+
for (const id of decisionIds) {
|
|
461
|
+
clearEmbedding(id).catch(() => { });
|
|
462
|
+
}
|
|
463
|
+
// Auto-delete from cloud (async, non-blocking, Pro only, if enabled)
|
|
464
|
+
getAutoSyncEnabled().then(enabled => {
|
|
465
|
+
if (enabled) {
|
|
466
|
+
for (const id of decisionIds) {
|
|
467
|
+
deleteDecisionFromCloud(id).catch(() => { });
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
// Log the action
|
|
472
|
+
await logAction('deleted', `scope:${normalizedScope}`, `Deleted scope ${normalizedScope} (${count} decisions)`);
|
|
473
|
+
return { deleted: count, decisionIds };
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Generate the next decision ID for a scope
|
|
477
|
+
*/
|
|
478
|
+
export async function getNextDecisionId(scope) {
|
|
479
|
+
const collection = await loadDecisions(scope);
|
|
480
|
+
const prefix = scope.toLowerCase().replace(/[^a-z]/g, '').substring(0, 10);
|
|
481
|
+
let maxNum = 0;
|
|
482
|
+
for (const d of collection.decisions) {
|
|
483
|
+
const match = d.id.match(/-([0-9]+)$/);
|
|
484
|
+
if (match) {
|
|
485
|
+
const num = parseInt(match[1], 10);
|
|
486
|
+
if (num > maxNum)
|
|
487
|
+
maxNum = num;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return `${prefix}-${String(maxNum + 1).padStart(3, '0')}`;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Renumber decisions in a scope after deletion
|
|
494
|
+
* Also updates embeddings for renamed IDs
|
|
495
|
+
*/
|
|
496
|
+
export async function renumberDecisions(scope) {
|
|
497
|
+
const collection = await loadDecisions(scope);
|
|
498
|
+
const prefix = scope.toLowerCase().replace(/[^a-z]/g, '').substring(0, 10);
|
|
499
|
+
const renames = [];
|
|
500
|
+
const sorted = [...collection.decisions].sort((a, b) => {
|
|
501
|
+
const aMatch = a.id.match(/-([0-9]+)$/);
|
|
502
|
+
const bMatch = b.id.match(/-([0-9]+)$/);
|
|
503
|
+
const aNum = aMatch ? parseInt(aMatch[1], 10) : 0;
|
|
504
|
+
const bNum = bMatch ? parseInt(bMatch[1], 10) : 0;
|
|
505
|
+
return aNum - bNum;
|
|
506
|
+
});
|
|
507
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
508
|
+
const newId = `${prefix}-${String(i + 1).padStart(3, '0')}`;
|
|
509
|
+
if (sorted[i].id !== newId) {
|
|
510
|
+
const oldId = sorted[i].id;
|
|
511
|
+
renames.push(`${oldId} → ${newId}`);
|
|
512
|
+
sorted[i].id = newId;
|
|
513
|
+
// Rename embedding
|
|
514
|
+
renameEmbedding(oldId, newId).catch(() => { });
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (renames.length > 0) {
|
|
518
|
+
collection.decisions = sorted;
|
|
519
|
+
await saveDecisions(collection);
|
|
520
|
+
}
|
|
521
|
+
return renames;
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Import decisions from a JSON file or object
|
|
525
|
+
* Auto-embeds all imported decisions
|
|
526
|
+
*/
|
|
527
|
+
export async function importDecisions(decisions, options) {
|
|
528
|
+
let added = 0;
|
|
529
|
+
let skipped = 0;
|
|
530
|
+
const toEmbed = [];
|
|
531
|
+
for (const decision of decisions) {
|
|
532
|
+
const existing = await getDecisionById(decision.id);
|
|
533
|
+
if (existing && !options?.overwrite) {
|
|
534
|
+
skipped++;
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
if (existing && options?.overwrite) {
|
|
538
|
+
await updateDecision(decision.id, decision);
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
const collection = await loadDecisions(decision.scope);
|
|
542
|
+
collection.decisions.push(decision);
|
|
543
|
+
await saveDecisions(collection);
|
|
544
|
+
}
|
|
545
|
+
toEmbed.push(decision);
|
|
546
|
+
added++;
|
|
547
|
+
}
|
|
548
|
+
// Batch embed all imported decisions
|
|
549
|
+
const { success } = await embedDecisions(toEmbed);
|
|
550
|
+
// Log import action
|
|
551
|
+
if (added > 0) {
|
|
552
|
+
await logAction('imported', `${added}-decisions`, `Imported ${added} decisions`);
|
|
553
|
+
}
|
|
554
|
+
return { added, skipped, embedded: success };
|
|
555
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DecisionNode v0 - The atomic unit of decision tracking
|
|
3
|
+
*
|
|
4
|
+
* This is our Data Model
|
|
5
|
+
*
|
|
6
|
+
* This is NOT documentation, note-taking, or chat history.
|
|
7
|
+
* This IS a structured, queryable, enforceable memory layer.
|
|
8
|
+
*/
|
|
9
|
+
export type DecisionScope = string;
|
|
10
|
+
export type DecisionStatus = "active" | "deprecated";
|
|
11
|
+
export interface DecisionNode {
|
|
12
|
+
/** Unique identifier for this decision */
|
|
13
|
+
id: string;
|
|
14
|
+
/** Domain this decision applies to */
|
|
15
|
+
scope: DecisionScope;
|
|
16
|
+
/** What was decided (1 sentence, clear and actionable) */
|
|
17
|
+
decision: string;
|
|
18
|
+
/** Why this decision was made (short, optional but encouraged) */
|
|
19
|
+
rationale?: string;
|
|
20
|
+
/** Things that must hold true - constraints and rules */
|
|
21
|
+
constraints?: string[];
|
|
22
|
+
/** Current status of this decision */
|
|
23
|
+
status: DecisionStatus;
|
|
24
|
+
/** When this decision was created (ISO 8601 format) */
|
|
25
|
+
createdAt: string;
|
|
26
|
+
/** Optional: When this decision was last updated */
|
|
27
|
+
updatedAt?: string;
|
|
28
|
+
/** Arbitrary tags for organization */
|
|
29
|
+
tags?: string[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Collection of decisions for a specific scope
|
|
33
|
+
*/
|
|
34
|
+
export interface DecisionCollection {
|
|
35
|
+
scope: DecisionScope;
|
|
36
|
+
decisions: DecisionNode[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Represents a decision synced to the cloud database
|
|
40
|
+
*/
|
|
41
|
+
export interface SyncedDecision {
|
|
42
|
+
id: string;
|
|
43
|
+
project_name: string;
|
|
44
|
+
decision_id: string;
|
|
45
|
+
scope: string;
|
|
46
|
+
decision: string;
|
|
47
|
+
rationale: string | null;
|
|
48
|
+
constraints: string[] | null;
|
|
49
|
+
status: string;
|
|
50
|
+
synced_at: string;
|
|
51
|
+
created_at: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Represents a group of decisions belonging to a project
|
|
55
|
+
*/
|
|
56
|
+
export interface ProjectGroup {
|
|
57
|
+
name: string;
|
|
58
|
+
decisions: SyncedDecision[];
|
|
59
|
+
lastSynced: string;
|
|
60
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "decisionnode",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Store development decisions as vector embeddings, query them via semantic search. CLI + MCP server for AI agents.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"decisions",
|
|
7
|
+
"mcp",
|
|
8
|
+
"model-context-protocol",
|
|
9
|
+
"ai",
|
|
10
|
+
"claude",
|
|
11
|
+
"cursor",
|
|
12
|
+
"windsurf",
|
|
13
|
+
"antigravity",
|
|
14
|
+
"gemini",
|
|
15
|
+
"embeddings",
|
|
16
|
+
"semantic-search",
|
|
17
|
+
"rag"
|
|
18
|
+
],
|
|
19
|
+
"author": "DecisionNode",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/decisionnode/decisionnode"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/decisionnode/decisionnode#readme",
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/decisionnode/decisionnode/issues"
|
|
28
|
+
},
|
|
29
|
+
"type": "module",
|
|
30
|
+
"bin": {
|
|
31
|
+
"decide": "./dist/cli.js",
|
|
32
|
+
"decisionnode": "./dist/cli.js",
|
|
33
|
+
"decide-mcp": "./dist/mcp/server.js"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc",
|
|
42
|
+
"dev": "tsc --watch",
|
|
43
|
+
"prepublishOnly": "npm run build"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18.0.0"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@google/generative-ai": "^0.24.1",
|
|
50
|
+
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
51
|
+
"dotenv": "^17.2.3"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "^20.19.27",
|
|
55
|
+
"typescript": "^5.3.3"
|
|
56
|
+
}
|
|
57
|
+
}
|