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/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
|
+

|
|
6
|
+

|
|
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
|
+
}
|
package/dist/ai/rag.d.ts
ADDED
|
@@ -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