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/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[];
|
package/dist/history.js
ADDED
|
@@ -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,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[]>;
|