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/env.js ADDED
@@ -0,0 +1,139 @@
1
+ // This file must be imported FIRST to ensure env is set up before any other modules
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import dotenv from 'dotenv';
5
+ // NUCLEAR OPTION: Redirect console.log to console.error
6
+ // MCP protocol uses stdout for communication. Any library logging to stdout
7
+ // will break the protocol. Redirecting to stderr is safe.
8
+ console.log = console.error;
9
+ /**
10
+ * Get the global DecisionNode store location
11
+ * ~/.decisionnode/.decisions/
12
+ */
13
+ function getGlobalStorePath() {
14
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
15
+ return path.join(homeDir, '.decisionnode', '.decisions');
16
+ }
17
+ // Current project name - can be overridden at runtime
18
+ let currentProjectName = path.basename(process.cwd());
19
+ /**
20
+ * Set the current project name (called by MCP tools)
21
+ */
22
+ export function setCurrentProject(projectName) {
23
+ currentProjectName = projectName;
24
+ }
25
+ /**
26
+ * Get the current project name
27
+ */
28
+ export function getCurrentProject() {
29
+ return currentProjectName;
30
+ }
31
+ /**
32
+ * Get the project-specific storage path
33
+ * ~/.decisionnode/.decisions/{projectname}/
34
+ * Does NOT create the folder - that's done when saving files
35
+ */
36
+ export function getProjectPath(projectName) {
37
+ const name = projectName || currentProjectName;
38
+ return path.join(getGlobalStorePath(), name);
39
+ }
40
+ /**
41
+ * Ensure project folder exists (call before writing files)
42
+ */
43
+ export function ensureProjectFolder(projectName) {
44
+ const projectPath = getProjectPath(projectName);
45
+ if (!fs.existsSync(projectPath)) {
46
+ fs.mkdirSync(projectPath, { recursive: true });
47
+ }
48
+ }
49
+ // Global store path (e.g., ~/.decisionnode/.decisions/)
50
+ export const GLOBAL_STORE = getGlobalStorePath();
51
+ // Reserved folder name for global decisions
52
+ export const GLOBAL_PROJECT_NAME = '_global';
53
+ /**
54
+ * Get the path to the global decisions folder
55
+ * ~/.decisionnode/.decisions/_global/
56
+ */
57
+ export function getGlobalDecisionsPath() {
58
+ return path.join(getGlobalStorePath(), GLOBAL_PROJECT_NAME);
59
+ }
60
+ /**
61
+ * Ensure the global decisions folder exists
62
+ */
63
+ export function ensureGlobalFolder() {
64
+ const globalPath = getGlobalDecisionsPath();
65
+ if (!fs.existsSync(globalPath)) {
66
+ fs.mkdirSync(globalPath, { recursive: true });
67
+ }
68
+ }
69
+ /**
70
+ * Check if a decision ID is a global decision (prefixed with "global:")
71
+ */
72
+ export function isGlobalId(id) {
73
+ return id.startsWith('global:');
74
+ }
75
+ /**
76
+ * Strip the "global:" prefix from a decision ID
77
+ */
78
+ export function stripGlobalPrefix(id) {
79
+ return id.replace(/^global:/, '');
80
+ }
81
+ // Also export a getter for dynamic access
82
+ export function getProjectRoot() {
83
+ return getProjectPath(currentProjectName);
84
+ }
85
+ // Load .env file from home directory ONLY
86
+ // This is the single source of truth for API keys
87
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
88
+ dotenv.config({
89
+ path: path.join(homeDir, '.decisionnode', '.env'),
90
+ quiet: true
91
+ });
92
+ const CONFIG_FILE_PATH = path.join(homeDir, '.decisionnode', 'config.json');
93
+ const DEFAULT_CONFIG = {
94
+ searchSensitivity: 'high'
95
+ };
96
+ /**
97
+ * Load the global DecisionNode config
98
+ */
99
+ function loadConfig() {
100
+ try {
101
+ if (fs.existsSync(CONFIG_FILE_PATH)) {
102
+ const content = fs.readFileSync(CONFIG_FILE_PATH, 'utf-8');
103
+ return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
104
+ }
105
+ }
106
+ catch {
107
+ // Return default if file is corrupted
108
+ }
109
+ return DEFAULT_CONFIG;
110
+ }
111
+ /**
112
+ * Save the global DecisionNode config
113
+ */
114
+ function saveConfig(config) {
115
+ try {
116
+ const dir = path.dirname(CONFIG_FILE_PATH);
117
+ if (!fs.existsSync(dir)) {
118
+ fs.mkdirSync(dir, { recursive: true });
119
+ }
120
+ fs.writeFileSync(CONFIG_FILE_PATH, JSON.stringify(config, null, 2), 'utf-8');
121
+ }
122
+ catch {
123
+ // Silently fail if we can't write config
124
+ }
125
+ }
126
+ /**
127
+ * Get the current search sensitivity level
128
+ */
129
+ export function getSearchSensitivity() {
130
+ return loadConfig().searchSensitivity;
131
+ }
132
+ /**
133
+ * Set the search sensitivity level
134
+ */
135
+ export function setSearchSensitivity(level) {
136
+ const config = loadConfig();
137
+ config.searchSensitivity = level;
138
+ saveConfig(config);
139
+ }
@@ -0,0 +1,34 @@
1
+ import { DecisionCollection, DecisionNode } from './types.js';
2
+ export type ActionType = 'added' | 'updated' | 'deleted' | 'imported' | 'installed' | 'cloud_push' | 'cloud_pull' | 'conflict_resolved';
3
+ export type SourceType = 'cli' | 'mcp' | 'cloud' | 'marketplace';
4
+ export interface ActivityLogEntry {
5
+ id: string;
6
+ action: ActionType;
7
+ decisionId: string;
8
+ description: string;
9
+ timestamp: string;
10
+ source?: SourceType;
11
+ snapshot: Record<string, DecisionCollection>;
12
+ }
13
+ /**
14
+ * Log an action to the activity log
15
+ * This captures the current state as a snapshot
16
+ */
17
+ export declare function logAction(action: ActionType, decisionId: string, description?: string, source?: SourceType): Promise<void>;
18
+ /**
19
+ * Log a batch action (for multiple decisions at once, like cloud sync)
20
+ * Uses a single entry with a summarized description
21
+ */
22
+ export declare function logBatchAction(action: ActionType, decisionIds: string[], source?: SourceType): Promise<void>;
23
+ /**
24
+ * Get recent activity log entries
25
+ */
26
+ export declare function getHistory(limit?: number): Promise<ActivityLogEntry[]>;
27
+ /**
28
+ * Get a specific snapshot by entry ID
29
+ */
30
+ export declare function getSnapshot(entryId: string): Promise<ActivityLogEntry | null>;
31
+ /**
32
+ * Get all decisions from a snapshot
33
+ */
34
+ export declare function getDecisionsFromSnapshot(snapshot: Record<string, DecisionCollection>): DecisionNode[];
@@ -0,0 +1,159 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import crypto from 'crypto';
4
+ import { loadDecisions } from './store.js';
5
+ import { getProjectRoot, ensureProjectFolder } from './env.js';
6
+ const HISTORY_DIR = 'history';
7
+ const LOG_FILE = path.join(HISTORY_DIR, 'activity.json');
8
+ /**
9
+ * Generate a short hash for entry ID
10
+ */
11
+ function generateEntryId() {
12
+ return crypto.randomBytes(4).toString('hex');
13
+ }
14
+ /**
15
+ * Get the log file path
16
+ */
17
+ function getLogPath() {
18
+ return path.join(getProjectRoot(), LOG_FILE);
19
+ }
20
+ /**
21
+ * Ensure history directory exists
22
+ */
23
+ async function ensureHistoryDir() {
24
+ await fs.mkdir(path.join(getProjectRoot(), HISTORY_DIR), { recursive: true });
25
+ }
26
+ /**
27
+ * Load the activity log
28
+ */
29
+ async function loadActivityLog() {
30
+ try {
31
+ const content = await fs.readFile(getLogPath(), 'utf-8');
32
+ return JSON.parse(content);
33
+ }
34
+ catch {
35
+ return { entries: [] };
36
+ }
37
+ }
38
+ /**
39
+ * Save the activity log
40
+ */
41
+ async function saveActivityLog(log) {
42
+ ensureProjectFolder();
43
+ await ensureHistoryDir();
44
+ await fs.writeFile(getLogPath(), JSON.stringify(log, null, 2), 'utf-8');
45
+ }
46
+ /**
47
+ * Get all available scopes by scanning the .decisions directory
48
+ */
49
+ async function getAvailableScopes() {
50
+ try {
51
+ const files = await fs.readdir(getProjectRoot());
52
+ return files
53
+ .filter(f => f.endsWith('.json') && f !== 'vectors.json')
54
+ .map(f => f.replace('.json', ''))
55
+ .map(s => s.charAt(0).toUpperCase() + s.slice(1));
56
+ }
57
+ catch {
58
+ return [];
59
+ }
60
+ }
61
+ /**
62
+ * Take a snapshot of current decisions
63
+ */
64
+ async function takeSnapshot() {
65
+ const scopes = await getAvailableScopes();
66
+ const snapshot = {};
67
+ for (const scope of scopes) {
68
+ const collection = await loadDecisions(scope);
69
+ snapshot[scope.toLowerCase()] = collection;
70
+ }
71
+ return snapshot;
72
+ }
73
+ /**
74
+ * Log an action to the activity log
75
+ * This captures the current state as a snapshot
76
+ */
77
+ export async function logAction(action, decisionId, description, source) {
78
+ const log = await loadActivityLog();
79
+ const snapshot = await takeSnapshot();
80
+ const entry = {
81
+ id: generateEntryId(),
82
+ action,
83
+ decisionId,
84
+ description: description || `${action} ${decisionId}`,
85
+ timestamp: new Date().toISOString(),
86
+ source: source || 'cli',
87
+ snapshot
88
+ };
89
+ // Add to beginning (newest first)
90
+ log.entries.unshift(entry);
91
+ // Keep last 100 entries to prevent unbounded growth
92
+ if (log.entries.length > 100) {
93
+ log.entries = log.entries.slice(0, 100);
94
+ }
95
+ await saveActivityLog(log);
96
+ }
97
+ /**
98
+ * Log a batch action (for multiple decisions at once, like cloud sync)
99
+ * Uses a single entry with a summarized description
100
+ */
101
+ export async function logBatchAction(action, decisionIds, source = 'cloud') {
102
+ if (decisionIds.length === 0)
103
+ return;
104
+ const log = await loadActivityLog();
105
+ const snapshot = await takeSnapshot();
106
+ // Create summarized description
107
+ let description;
108
+ const actionVerb = action === 'cloud_push' ? 'Pushed' : action === 'cloud_pull' ? 'Pulled' : action;
109
+ if (decisionIds.length === 1) {
110
+ description = `${actionVerb} ${decisionIds[0]}`;
111
+ }
112
+ else if (decisionIds.length <= 3) {
113
+ description = `${actionVerb} ${decisionIds.length} decisions (${decisionIds.join(', ')})`;
114
+ }
115
+ else {
116
+ const shown = decisionIds.slice(0, 3).join(', ');
117
+ description = `${actionVerb} ${decisionIds.length} decisions (${shown}... +${decisionIds.length - 3} more)`;
118
+ }
119
+ const entry = {
120
+ id: generateEntryId(),
121
+ action,
122
+ decisionId: decisionIds.join(','), // Store all IDs
123
+ description,
124
+ timestamp: new Date().toISOString(),
125
+ source,
126
+ snapshot
127
+ };
128
+ log.entries.unshift(entry);
129
+ if (log.entries.length > 100) {
130
+ log.entries = log.entries.slice(0, 100);
131
+ }
132
+ await saveActivityLog(log);
133
+ }
134
+ /**
135
+ * Get recent activity log entries
136
+ */
137
+ export async function getHistory(limit = 20) {
138
+ const log = await loadActivityLog();
139
+ return log.entries.slice(0, limit);
140
+ }
141
+ /**
142
+ * Get a specific snapshot by entry ID
143
+ */
144
+ export async function getSnapshot(entryId) {
145
+ const log = await loadActivityLog();
146
+ return log.entries.find(e => e.id === entryId) || null;
147
+ }
148
+ /**
149
+ * Get all decisions from a snapshot
150
+ */
151
+ export function getDecisionsFromSnapshot(snapshot) {
152
+ const decisions = [];
153
+ for (const scope of Object.keys(snapshot)) {
154
+ if (snapshot[scope].decisions && Array.isArray(snapshot[scope].decisions)) {
155
+ decisions.push(...snapshot[scope].decisions);
156
+ }
157
+ }
158
+ return decisions;
159
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Clean orphaned data (vectors and reviews) for decisions that no longer exist
3
+ */
4
+ export declare function cleanOrphanedData(): Promise<{
5
+ vectorsRemoved: number;
6
+ reviewsRemoved: number;
7
+ }>;
@@ -0,0 +1,49 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { getProjectRoot } from './env.js';
4
+ import { listDecisions } from './store.js';
5
+ import { loadVectorCache, saveVectorCache } from './ai/rag.js';
6
+ const REVIEWED_FILE = () => path.join(getProjectRoot(), 'reviewed.json');
7
+ async function loadReviewed() {
8
+ try {
9
+ const content = await fs.readFile(REVIEWED_FILE(), 'utf-8');
10
+ return JSON.parse(content);
11
+ }
12
+ catch {
13
+ return { reviewed: [] };
14
+ }
15
+ }
16
+ async function saveReviewed(data) {
17
+ await fs.writeFile(REVIEWED_FILE(), JSON.stringify(data, null, 2), 'utf-8');
18
+ }
19
+ /**
20
+ * Clean orphaned data (vectors and reviews) for decisions that no longer exist
21
+ */
22
+ export async function cleanOrphanedData() {
23
+ const decisions = await listDecisions();
24
+ const validIds = new Set(decisions.map(d => d.id));
25
+ // Clean Vectors
26
+ const cache = await loadVectorCache();
27
+ let vectorsRemoved = 0;
28
+ for (const id of Object.keys(cache)) {
29
+ if (!validIds.has(id)) {
30
+ delete cache[id];
31
+ vectorsRemoved++;
32
+ }
33
+ }
34
+ if (vectorsRemoved > 0) {
35
+ await saveVectorCache(cache);
36
+ }
37
+ // Clean Reviews
38
+ const reviewData = await loadReviewed();
39
+ // Use optional chaining just in case
40
+ const currentReviews = reviewData.reviewed || [];
41
+ const originalCount = currentReviews.length;
42
+ const cleanReviews = currentReviews.filter(id => validIds.has(id));
43
+ const reviewsRemoved = originalCount - cleanReviews.length;
44
+ if (reviewsRemoved > 0) {
45
+ reviewData.reviewed = cleanReviews;
46
+ await saveReviewed(reviewData);
47
+ }
48
+ return { vectorsRemoved, reviewsRemoved };
49
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * DecisionNode Marketplace
3
+ *
4
+ * Download and install curated decision packs. Embeddings are generated locally.
5
+ */
6
+ import { DecisionNode } from './types.js';
7
+ /**
8
+ * A Decision Pack is a pre-made collection of decisions with vectors
9
+ */
10
+ export interface DecisionPack {
11
+ id: string;
12
+ name: string;
13
+ description: string;
14
+ author: string;
15
+ version: string;
16
+ scope: string;
17
+ decisions: DecisionNode[];
18
+ vectors: Record<string, number[]>;
19
+ }
20
+ /**
21
+ * Marketplace index entry (lightweight metadata)
22
+ */
23
+ export interface MarketplaceEntry {
24
+ id: string;
25
+ name: string;
26
+ description: string;
27
+ author: string;
28
+ scope: string;
29
+ decisionCount: number;
30
+ downloads: number;
31
+ }
32
+ /**
33
+ * Fetch the marketplace index from Supabase
34
+ */
35
+ export declare function getMarketplaceIndex(): Promise<MarketplaceEntry[]>;
36
+ /**
37
+ * Download and install a decision pack
38
+ */
39
+ export declare function installPack(packId: string): Promise<{
40
+ installed: number;
41
+ skipped: number;
42
+ }>;
43
+ /**
44
+ * Search marketplace by query
45
+ */
46
+ export declare function searchMarketplace(query: string): Promise<MarketplaceEntry[]>;