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/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
+ }
@@ -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
@@ -0,0 +1,9 @@
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 {};
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
+ }