agentcache 0.1.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 +193 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-MMSMDJ4O.js +466 -0
- package/dist/chunk-PYGRUQNL.js +45 -0
- package/dist/chunk-QGG25FWV.js +173 -0
- package/dist/chunk-RUBNVM6W.js +129 -0
- package/dist/chunk-VPEEZXLK.js +68 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +73 -0
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.js +884 -0
- package/dist/paths-TWJ7GAJY.js +21 -0
- package/dist/postinstall.d.ts +2 -0
- package/dist/postinstall.js +38 -0
- package/dist/pre-tool-use-2P5P6JWE.js +30 -0
- package/dist/session-start-ILEPFZZC.js +64 -0
- package/dist/setup-QB36C2TH.js +47 -0
- package/dist/sqlite-5V565IV3.js +7 -0
- package/dist/stop-DRL3LXFQ.js +41 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 a21.ai
|
|
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,193 @@
|
|
|
1
|
+
# AgentCache
|
|
2
|
+
|
|
3
|
+
**Knowledge cache for AI coding agents** — learns how you work, remembers across sessions, works everywhere.
|
|
4
|
+
|
|
5
|
+
AgentCache observes your coding sessions and compiles reusable knowledge (rules, lessons, architectural decisions, context) into a local database. Every future session — regardless of IDE or LLM — gets the benefit of everything you've learned before.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
Every AI coding session starts from zero. The agent doesn't know your team's conventions, past mistakes, or architectural decisions. You repeat yourself. Bugs recur. Context is lost.
|
|
10
|
+
|
|
11
|
+
AgentCache fixes this. It's a persistent knowledge layer that:
|
|
12
|
+
- **Learns** rules, lessons, and decisions from your sessions
|
|
13
|
+
- **Injects** relevant knowledge at the start of every new session
|
|
14
|
+
- **Works everywhere** — any IDE, any LLM, simultaneously
|
|
15
|
+
- **Stays local** — SQLite database on your machine, nothing leaves your disk
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install -g agentcache
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
That's it. No `init`, no per-project setup, no config files.
|
|
24
|
+
|
|
25
|
+
On install, AgentCache automatically:
|
|
26
|
+
1. Creates `~/.loop/loop.db` (your knowledge store)
|
|
27
|
+
2. Detects installed IDEs (Claude Code, Cursor, Roo Code, Windsurf, Continue, Codex)
|
|
28
|
+
3. Registers itself as an MCP server in each
|
|
29
|
+
4. Sets up Claude Code hooks for automatic transcript recovery
|
|
30
|
+
|
|
31
|
+
## How It Works
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
35
|
+
│ Your Machine │
|
|
36
|
+
│ │
|
|
37
|
+
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
|
38
|
+
│ │ Claude │ │ Cursor │ │ Roo │ │ Codex │ ... │
|
|
39
|
+
│ │ Code │ │ │ │ Code │ │ │ │
|
|
40
|
+
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
|
|
41
|
+
│ │ │ │ │ │
|
|
42
|
+
│ └──────────────┴──────────────┴──────────────┘ │
|
|
43
|
+
│ │ │
|
|
44
|
+
│ MCP Protocol (stdio) │
|
|
45
|
+
│ │ │
|
|
46
|
+
│ ┌────────────┴────────────┐ │
|
|
47
|
+
│ │ AgentCache MCP Server │ │
|
|
48
|
+
│ │ (agentcache serve) │ │
|
|
49
|
+
│ └────────────┬────────────┘ │
|
|
50
|
+
│ │ │
|
|
51
|
+
│ ┌─────────┴─────────┐ │
|
|
52
|
+
│ │ ~/.loop/loop.db │ │
|
|
53
|
+
│ │ (SQLite + WAL) │ │
|
|
54
|
+
│ └───────────────────┘ │
|
|
55
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### The Cycle
|
|
59
|
+
|
|
60
|
+
1. **Session starts** — agent calls `loop_inject_context` → gets compiled rules, lessons, decisions
|
|
61
|
+
2. **During session** — agent calls `loop_compile_submit` incrementally as it learns things
|
|
62
|
+
3. **Session ends** — knowledge is already saved. If agent didn't submit (abrupt exit), transcript recovery handles it next session.
|
|
63
|
+
|
|
64
|
+
### Knowledge Types
|
|
65
|
+
|
|
66
|
+
| Type | Scope | Example |
|
|
67
|
+
|------|-------|---------|
|
|
68
|
+
| **Rule** | Global | "Always use snake_case for database columns" |
|
|
69
|
+
| **Lesson** | Global | "Don't mock the database in integration tests — we got burned when mocked tests passed but prod migration failed" |
|
|
70
|
+
| **Decision** | Project | "Using Drizzle ORM over Prisma because we need raw SQL escape hatches" |
|
|
71
|
+
| **Context** | Project | "Currently migrating from REST to GraphQL, both coexist" |
|
|
72
|
+
|
|
73
|
+
Rules and lessons are **global** — they apply to all your projects. Decisions and context are **project-specific**.
|
|
74
|
+
|
|
75
|
+
## MCP Tools
|
|
76
|
+
|
|
77
|
+
AgentCache exposes 8 tools via the Model Context Protocol:
|
|
78
|
+
|
|
79
|
+
| Tool | Purpose |
|
|
80
|
+
|------|---------|
|
|
81
|
+
| `loop_inject_context` | Load compiled knowledge at session start |
|
|
82
|
+
| `loop_compile_submit` | Submit observations incrementally during session |
|
|
83
|
+
| `loop_compile_cluster` | Resolve clustering when observations overlap existing knowledge |
|
|
84
|
+
| `loop_compile_extract` | Process queued transcripts from previous sessions |
|
|
85
|
+
| `loop_enforce` | Check tool calls against enforced policy rules |
|
|
86
|
+
| `loop_save_observation` | Save a permanent observation (USER authority, never auto-deprecated) |
|
|
87
|
+
| `loop_get_knowledge` | Query the knowledge database |
|
|
88
|
+
| `loop_deprecate_knowledge` | Mark knowledge as deprecated when it's no longer valid |
|
|
89
|
+
|
|
90
|
+
## CLI Commands
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
agentcache setup # Re-detect IDEs and register (runs automatically on install)
|
|
94
|
+
agentcache serve # Start MCP server (IDEs spawn this automatically)
|
|
95
|
+
agentcache status # Show knowledge stats for current project
|
|
96
|
+
agentcache compile-session # Queue transcript for compilation (Stop hook)
|
|
97
|
+
agentcache discover # Discover uncompiled transcripts (SessionStart hook)
|
|
98
|
+
agentcache enforce # Policy enforcement (PreToolUse hook)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Design Principles
|
|
102
|
+
|
|
103
|
+
### Zero Config
|
|
104
|
+
|
|
105
|
+
`npm install -g agentcache` is the only step. It detects your IDEs, registers itself, and starts working. No dotfiles in your project. No init commands. No config to maintain.
|
|
106
|
+
|
|
107
|
+
### Universal
|
|
108
|
+
|
|
109
|
+
AgentCache uses MCP (Model Context Protocol) as its only interface. Any IDE that supports MCP works. Any LLM — Claude, GPT, Gemini, Qwen, Llama — can use the tools. No IDE-specific code paths. No LLM-specific logic.
|
|
110
|
+
|
|
111
|
+
### Developer-Scoped
|
|
112
|
+
|
|
113
|
+
One database per developer (`~/.loop/loop.db`), not per project. Rules and lessons learned in one project benefit all your projects. Project-specific decisions stay scoped to their project.
|
|
114
|
+
|
|
115
|
+
### Resilient to Abrupt Exits
|
|
116
|
+
|
|
117
|
+
Sessions can end without warning (crash, ctrl-c, network drop). AgentCache handles this through:
|
|
118
|
+
- **Incremental submission** — observations are saved as they happen, not batched at the end
|
|
119
|
+
- **Transcript recovery** — for Claude Code and Continue, transcripts persist on disk and are compiled next session
|
|
120
|
+
- **Pending queue in SQLite** — concurrent access is safe, nothing lost to race conditions
|
|
121
|
+
|
|
122
|
+
### Anti-Bloat
|
|
123
|
+
|
|
124
|
+
AgentCache prevents knowledge from growing unbounded:
|
|
125
|
+
- **Confidence promotion** — observations need repeated confirmation before becoming high-confidence
|
|
126
|
+
- **Decay** — auto-compiled items not seen in 30 days get archived
|
|
127
|
+
- **Budget caps** — max 20 rules, 10 lessons, 10 decisions, 5 context items injected per session
|
|
128
|
+
- **Priority ranking** — USER authority first, then by confidence and recency
|
|
129
|
+
|
|
130
|
+
## Supported IDEs
|
|
131
|
+
|
|
132
|
+
| IDE | MCP | Transcript Recovery | Hooks |
|
|
133
|
+
|-----|-----|--------------------|----|
|
|
134
|
+
| Claude Code | Yes | Full (JSONL) | Stop, SessionStart, PreToolUse |
|
|
135
|
+
| Cursor | Yes | Incremental only | — |
|
|
136
|
+
| Roo Code | Yes | Incremental only | — |
|
|
137
|
+
| Windsurf | Yes | Incremental only | — |
|
|
138
|
+
| Continue | Yes | Full (JSON) | — |
|
|
139
|
+
| Codex | Yes | Incremental only | — |
|
|
140
|
+
|
|
141
|
+
"Incremental only" means if the agent submits observations during the session, they're saved. If the session terminates before any submission, those observations are lost (no transcript access).
|
|
142
|
+
|
|
143
|
+
## Data Storage
|
|
144
|
+
|
|
145
|
+
All data lives in `~/.loop/loop.db` (SQLite with WAL mode for concurrent access).
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
~/.loop/
|
|
149
|
+
└── loop.db # All knowledge, observations, sessions, pending queue
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
No data leaves your machine. No network calls. No telemetry. No accounts.
|
|
153
|
+
|
|
154
|
+
## How Knowledge Compiles
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
Observations (raw)
|
|
158
|
+
│
|
|
159
|
+
▼
|
|
160
|
+
Extract → Normalize → Canonicalize → Cluster → Detect Contradictions → Compile
|
|
161
|
+
│ │
|
|
162
|
+
│ "Always use ESLint" │
|
|
163
|
+
│ "Always use ESLint" ──→ deduplicated, confidence promoted │
|
|
164
|
+
│ "Use Prettier not ESLint" ──→ contradiction detected │
|
|
165
|
+
│ ▼
|
|
166
|
+
Knowledge Items (compiled)
|
|
167
|
+
- status: active/deprecated/superseded
|
|
168
|
+
- confidence: low/medium/high
|
|
169
|
+
- authority: AUTO/USER
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The compiler is the LLM itself (the agent in your session). AgentCache provides extraction prompts and the agent processes them — no separate API keys or LLM calls needed.
|
|
173
|
+
|
|
174
|
+
## Project Identity
|
|
175
|
+
|
|
176
|
+
Projects are identified by a hash of their full filesystem path, not just the folder name. This means:
|
|
177
|
+
- `/work/api` and `/personal/api` are different projects
|
|
178
|
+
- Renaming a folder creates a new project identity
|
|
179
|
+
- Knowledge doesn't leak between same-named projects
|
|
180
|
+
|
|
181
|
+
## Contributing
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
git clone https://github.com/raghav-a21ai/loop-eng
|
|
185
|
+
cd loop-eng
|
|
186
|
+
npm install
|
|
187
|
+
npm run build
|
|
188
|
+
npm test
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## License
|
|
192
|
+
|
|
193
|
+
MIT
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
// src/storage/sqlite.ts
|
|
2
|
+
import Database from "better-sqlite3";
|
|
3
|
+
var SqliteKnowledgeRepository = class {
|
|
4
|
+
db;
|
|
5
|
+
constructor(dbPath) {
|
|
6
|
+
this.db = new Database(dbPath);
|
|
7
|
+
this.db.pragma("journal_mode = WAL");
|
|
8
|
+
this.db.pragma("foreign_keys = ON");
|
|
9
|
+
this.initSchema();
|
|
10
|
+
}
|
|
11
|
+
initSchema() {
|
|
12
|
+
this.db.exec(`
|
|
13
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
14
|
+
id TEXT PRIMARY KEY,
|
|
15
|
+
project TEXT NOT NULL,
|
|
16
|
+
started_at INTEGER NOT NULL,
|
|
17
|
+
ended_at INTEGER NOT NULL,
|
|
18
|
+
git_branch TEXT,
|
|
19
|
+
git_commit TEXT,
|
|
20
|
+
provider TEXT NOT NULL DEFAULT 'claude',
|
|
21
|
+
model TEXT NOT NULL,
|
|
22
|
+
transcript_path TEXT NOT NULL,
|
|
23
|
+
observation_count INTEGER NOT NULL DEFAULT 0
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
CREATE TABLE IF NOT EXISTS observations (
|
|
27
|
+
id TEXT PRIMARY KEY,
|
|
28
|
+
session_id TEXT NOT NULL REFERENCES sessions(id),
|
|
29
|
+
timestamp INTEGER NOT NULL,
|
|
30
|
+
type TEXT NOT NULL,
|
|
31
|
+
content TEXT NOT NULL,
|
|
32
|
+
source_quote TEXT NOT NULL,
|
|
33
|
+
confidence TEXT NOT NULL,
|
|
34
|
+
project TEXT NOT NULL,
|
|
35
|
+
scope TEXT NOT NULL DEFAULT 'project'
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
CREATE TABLE IF NOT EXISTS knowledge_items (
|
|
39
|
+
id TEXT PRIMARY KEY,
|
|
40
|
+
canonical_hash TEXT NOT NULL UNIQUE,
|
|
41
|
+
type TEXT NOT NULL,
|
|
42
|
+
title TEXT NOT NULL,
|
|
43
|
+
content TEXT NOT NULL,
|
|
44
|
+
confidence TEXT NOT NULL DEFAULT 'low',
|
|
45
|
+
observation_count INTEGER NOT NULL DEFAULT 1,
|
|
46
|
+
authority TEXT NOT NULL DEFAULT 'AUTO',
|
|
47
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
48
|
+
superseded_by_id TEXT,
|
|
49
|
+
enforce INTEGER NOT NULL DEFAULT 0,
|
|
50
|
+
project TEXT NOT NULL,
|
|
51
|
+
scope TEXT NOT NULL DEFAULT 'project',
|
|
52
|
+
created_at INTEGER NOT NULL,
|
|
53
|
+
updated_at INTEGER NOT NULL,
|
|
54
|
+
last_seen_at INTEGER NOT NULL,
|
|
55
|
+
metadata TEXT NOT NULL DEFAULT '{}'
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
CREATE TABLE IF NOT EXISTS compile_runs (
|
|
59
|
+
id TEXT PRIMARY KEY,
|
|
60
|
+
project TEXT NOT NULL,
|
|
61
|
+
session_id TEXT REFERENCES sessions(id),
|
|
62
|
+
compiler_version TEXT NOT NULL,
|
|
63
|
+
prompt_versions TEXT NOT NULL DEFAULT '{}',
|
|
64
|
+
started_at INTEGER NOT NULL,
|
|
65
|
+
ended_at INTEGER NOT NULL,
|
|
66
|
+
duration_ms INTEGER NOT NULL,
|
|
67
|
+
observations_processed INTEGER NOT NULL DEFAULT 0,
|
|
68
|
+
knowledge_created INTEGER NOT NULL DEFAULT 0,
|
|
69
|
+
knowledge_reinforced INTEGER NOT NULL DEFAULT 0,
|
|
70
|
+
knowledge_deprecated INTEGER NOT NULL DEFAULT 0,
|
|
71
|
+
knowledge_superseded INTEGER NOT NULL DEFAULT 0,
|
|
72
|
+
knowledge_ignored INTEGER NOT NULL DEFAULT 0,
|
|
73
|
+
contradictions_detected INTEGER NOT NULL DEFAULT 0,
|
|
74
|
+
diagnostics TEXT
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
CREATE TABLE IF NOT EXISTS contradictions (
|
|
78
|
+
id TEXT PRIMARY KEY,
|
|
79
|
+
project TEXT NOT NULL,
|
|
80
|
+
item_a_id TEXT NOT NULL,
|
|
81
|
+
item_b_id TEXT NOT NULL,
|
|
82
|
+
topic TEXT NOT NULL,
|
|
83
|
+
description TEXT NOT NULL,
|
|
84
|
+
recommendation TEXT NOT NULL,
|
|
85
|
+
resolved INTEGER NOT NULL DEFAULT 0,
|
|
86
|
+
created_at INTEGER NOT NULL
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
CREATE TABLE IF NOT EXISTS pending_transcripts (
|
|
90
|
+
id TEXT PRIMARY KEY,
|
|
91
|
+
transcript_path TEXT NOT NULL UNIQUE,
|
|
92
|
+
project TEXT NOT NULL,
|
|
93
|
+
project_root TEXT NOT NULL,
|
|
94
|
+
provider TEXT NOT NULL,
|
|
95
|
+
queued_at INTEGER NOT NULL
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project);
|
|
99
|
+
CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
|
|
100
|
+
CREATE INDEX IF NOT EXISTS idx_knowledge_project ON knowledge_items(project);
|
|
101
|
+
CREATE INDEX IF NOT EXISTS idx_knowledge_enforce ON knowledge_items(enforce);
|
|
102
|
+
CREATE INDEX IF NOT EXISTS idx_knowledge_status ON knowledge_items(status);
|
|
103
|
+
CREATE INDEX IF NOT EXISTS idx_ki_scope_status ON knowledge_items(scope, status);
|
|
104
|
+
CREATE INDEX IF NOT EXISTS idx_ki_project_status ON knowledge_items(project, status);
|
|
105
|
+
CREATE INDEX IF NOT EXISTS idx_compile_runs_project ON compile_runs(project);
|
|
106
|
+
CREATE INDEX IF NOT EXISTS idx_contradictions_project ON contradictions(project, resolved);
|
|
107
|
+
`);
|
|
108
|
+
}
|
|
109
|
+
saveSession(session) {
|
|
110
|
+
this.db.prepare(
|
|
111
|
+
`INSERT INTO sessions (id, project, started_at, ended_at, git_branch, git_commit, provider, model, transcript_path, observation_count)
|
|
112
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
113
|
+
).run(
|
|
114
|
+
session.id,
|
|
115
|
+
session.project,
|
|
116
|
+
session.startedAt,
|
|
117
|
+
session.endedAt,
|
|
118
|
+
session.gitBranch,
|
|
119
|
+
session.gitCommit,
|
|
120
|
+
session.provider,
|
|
121
|
+
session.model,
|
|
122
|
+
session.transcriptPath,
|
|
123
|
+
session.observationCount
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
getSession(id) {
|
|
127
|
+
const row = this.db.prepare("SELECT * FROM sessions WHERE id = ?").get(id);
|
|
128
|
+
if (!row) return null;
|
|
129
|
+
return this.mapSession(row);
|
|
130
|
+
}
|
|
131
|
+
getCompiledTranscriptPaths(project) {
|
|
132
|
+
const rows = this.db.prepare("SELECT transcript_path FROM sessions WHERE project = ? AND transcript_path != ''").all(project);
|
|
133
|
+
return rows.map((r) => r.transcript_path);
|
|
134
|
+
}
|
|
135
|
+
getAllCompiledTranscriptPaths() {
|
|
136
|
+
const rows = this.db.prepare("SELECT transcript_path FROM sessions WHERE transcript_path != ''").all();
|
|
137
|
+
return rows.map((r) => r.transcript_path);
|
|
138
|
+
}
|
|
139
|
+
saveObservation(obs) {
|
|
140
|
+
this.db.prepare(
|
|
141
|
+
`INSERT INTO observations (id, session_id, timestamp, type, content, source_quote, confidence, project, scope)
|
|
142
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
143
|
+
).run(
|
|
144
|
+
obs.id,
|
|
145
|
+
obs.sessionId,
|
|
146
|
+
obs.timestamp,
|
|
147
|
+
obs.type,
|
|
148
|
+
obs.content,
|
|
149
|
+
obs.sourceQuote,
|
|
150
|
+
obs.confidence,
|
|
151
|
+
obs.project,
|
|
152
|
+
obs.scope
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
saveObservations(obs) {
|
|
156
|
+
const insert = this.db.prepare(
|
|
157
|
+
`INSERT INTO observations (id, session_id, timestamp, type, content, source_quote, confidence, project, scope)
|
|
158
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
159
|
+
);
|
|
160
|
+
const transaction = this.db.transaction((observations) => {
|
|
161
|
+
for (const o of observations) {
|
|
162
|
+
insert.run(
|
|
163
|
+
o.id,
|
|
164
|
+
o.sessionId,
|
|
165
|
+
o.timestamp,
|
|
166
|
+
o.type,
|
|
167
|
+
o.content,
|
|
168
|
+
o.sourceQuote,
|
|
169
|
+
o.confidence,
|
|
170
|
+
o.project,
|
|
171
|
+
o.scope
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
transaction(obs);
|
|
176
|
+
}
|
|
177
|
+
getObservations(project, since) {
|
|
178
|
+
let sql = "SELECT * FROM observations WHERE project = ?";
|
|
179
|
+
const params = [project];
|
|
180
|
+
if (since !== void 0) {
|
|
181
|
+
sql += " AND timestamp >= ?";
|
|
182
|
+
params.push(since);
|
|
183
|
+
}
|
|
184
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
185
|
+
return rows.map((row) => this.mapObservation(row));
|
|
186
|
+
}
|
|
187
|
+
saveKnowledgeItem(item) {
|
|
188
|
+
this.db.prepare(
|
|
189
|
+
`INSERT INTO knowledge_items (id, canonical_hash, type, title, content, confidence, observation_count, authority, status, superseded_by_id, enforce, project, scope, created_at, updated_at, last_seen_at, metadata)
|
|
190
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
191
|
+
).run(
|
|
192
|
+
item.id,
|
|
193
|
+
item.canonicalHash,
|
|
194
|
+
item.type,
|
|
195
|
+
item.title,
|
|
196
|
+
item.content,
|
|
197
|
+
item.confidence,
|
|
198
|
+
item.observationCount,
|
|
199
|
+
item.authority,
|
|
200
|
+
item.status,
|
|
201
|
+
item.supersededById ?? null,
|
|
202
|
+
item.enforce ? 1 : 0,
|
|
203
|
+
item.project,
|
|
204
|
+
item.scope,
|
|
205
|
+
item.createdAt,
|
|
206
|
+
item.updatedAt,
|
|
207
|
+
item.lastSeenAt,
|
|
208
|
+
JSON.stringify(item.metadata)
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
updateKnowledgeItem(id, patch) {
|
|
212
|
+
const fieldMap = {
|
|
213
|
+
canonicalHash: "canonical_hash",
|
|
214
|
+
type: "type",
|
|
215
|
+
title: "title",
|
|
216
|
+
content: "content",
|
|
217
|
+
confidence: "confidence",
|
|
218
|
+
observationCount: "observation_count",
|
|
219
|
+
authority: "authority",
|
|
220
|
+
status: "status",
|
|
221
|
+
supersededById: "superseded_by_id",
|
|
222
|
+
enforce: "enforce",
|
|
223
|
+
project: "project",
|
|
224
|
+
scope: "scope",
|
|
225
|
+
createdAt: "created_at",
|
|
226
|
+
updatedAt: "updated_at",
|
|
227
|
+
lastSeenAt: "last_seen_at",
|
|
228
|
+
metadata: "metadata"
|
|
229
|
+
};
|
|
230
|
+
const setClauses = [];
|
|
231
|
+
const values = [];
|
|
232
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
233
|
+
const column = fieldMap[key];
|
|
234
|
+
if (!column) continue;
|
|
235
|
+
setClauses.push(`${column} = ?`);
|
|
236
|
+
if (key === "enforce") {
|
|
237
|
+
values.push(value ? 1 : 0);
|
|
238
|
+
} else if (key === "metadata") {
|
|
239
|
+
values.push(JSON.stringify(value));
|
|
240
|
+
} else {
|
|
241
|
+
values.push(value);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (setClauses.length === 0) return;
|
|
245
|
+
values.push(id);
|
|
246
|
+
this.db.prepare(`UPDATE knowledge_items SET ${setClauses.join(", ")} WHERE id = ?`).run(...values);
|
|
247
|
+
}
|
|
248
|
+
getKnowledgeItems(project, filter) {
|
|
249
|
+
let sql = "SELECT * FROM knowledge_items WHERE project = ?";
|
|
250
|
+
const params = [project];
|
|
251
|
+
if (filter) {
|
|
252
|
+
if (filter.type !== void 0) {
|
|
253
|
+
sql += " AND type = ?";
|
|
254
|
+
params.push(filter.type);
|
|
255
|
+
}
|
|
256
|
+
if (filter.status !== void 0) {
|
|
257
|
+
sql += " AND status = ?";
|
|
258
|
+
params.push(filter.status);
|
|
259
|
+
}
|
|
260
|
+
if (filter.enforce !== void 0) {
|
|
261
|
+
sql += " AND enforce = ?";
|
|
262
|
+
params.push(filter.enforce ? 1 : 0);
|
|
263
|
+
}
|
|
264
|
+
if (filter.authority !== void 0) {
|
|
265
|
+
sql += " AND authority = ?";
|
|
266
|
+
params.push(filter.authority);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
270
|
+
return rows.map((row) => this.mapKnowledgeItem(row));
|
|
271
|
+
}
|
|
272
|
+
getKnowledgeItem(id) {
|
|
273
|
+
const row = this.db.prepare("SELECT * FROM knowledge_items WHERE id = ?").get(id);
|
|
274
|
+
if (!row) return null;
|
|
275
|
+
return this.mapKnowledgeItem(row);
|
|
276
|
+
}
|
|
277
|
+
getKnowledgeForContext(project) {
|
|
278
|
+
const decayThreshold = Date.now() - 30 * 24 * 60 * 60 * 1e3;
|
|
279
|
+
this.db.prepare(
|
|
280
|
+
`UPDATE knowledge_items SET status = 'archived', updated_at = ?
|
|
281
|
+
WHERE status = 'active' AND authority = 'AUTO' AND last_seen_at < ?`
|
|
282
|
+
).run(Date.now(), decayThreshold);
|
|
283
|
+
const rows = this.db.prepare(
|
|
284
|
+
`SELECT * FROM knowledge_items WHERE status = 'active' AND (scope = 'global' OR project = ?)
|
|
285
|
+
ORDER BY
|
|
286
|
+
CASE authority WHEN 'USER' THEN 0 ELSE 1 END,
|
|
287
|
+
CASE confidence WHEN 'high' THEN 3 WHEN 'medium' THEN 2 ELSE 1 END DESC,
|
|
288
|
+
last_seen_at DESC
|
|
289
|
+
LIMIT 50`
|
|
290
|
+
).all(project);
|
|
291
|
+
return rows.map((row) => this.mapKnowledgeItem(row));
|
|
292
|
+
}
|
|
293
|
+
getEnforcedRules(project) {
|
|
294
|
+
const rows = this.db.prepare(
|
|
295
|
+
`SELECT * FROM knowledge_items WHERE enforce = 1 AND status = 'active' AND (scope = 'global' OR project = ?)`
|
|
296
|
+
).all(project);
|
|
297
|
+
return rows.map((row) => this.mapKnowledgeItem(row));
|
|
298
|
+
}
|
|
299
|
+
saveCompileRun(run) {
|
|
300
|
+
this.db.prepare(
|
|
301
|
+
`INSERT INTO compile_runs (id, project, session_id, compiler_version, prompt_versions, started_at, ended_at, duration_ms, observations_processed, knowledge_created, knowledge_reinforced, knowledge_deprecated, knowledge_superseded, knowledge_ignored, contradictions_detected, diagnostics)
|
|
302
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
303
|
+
).run(
|
|
304
|
+
run.id,
|
|
305
|
+
run.project,
|
|
306
|
+
run.sessionId,
|
|
307
|
+
run.compilerVersion,
|
|
308
|
+
JSON.stringify(run.promptVersions),
|
|
309
|
+
run.startedAt,
|
|
310
|
+
run.endedAt,
|
|
311
|
+
run.durationMs,
|
|
312
|
+
run.observationsProcessed,
|
|
313
|
+
run.knowledgeCreated,
|
|
314
|
+
run.knowledgeReinforced,
|
|
315
|
+
run.knowledgeDeprecated,
|
|
316
|
+
run.knowledgeSuperseded,
|
|
317
|
+
run.knowledgeIgnored,
|
|
318
|
+
run.contradictionsDetected,
|
|
319
|
+
run.diagnostics
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
getCompileRuns(project, limit) {
|
|
323
|
+
let sql = "SELECT * FROM compile_runs WHERE project = ? ORDER BY started_at DESC";
|
|
324
|
+
const params = [project];
|
|
325
|
+
if (limit !== void 0) {
|
|
326
|
+
sql += " LIMIT ?";
|
|
327
|
+
params.push(limit);
|
|
328
|
+
}
|
|
329
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
330
|
+
return rows.map((row) => this.mapCompileRun(row));
|
|
331
|
+
}
|
|
332
|
+
saveContradiction(report) {
|
|
333
|
+
this.db.prepare(
|
|
334
|
+
`INSERT INTO contradictions (id, project, item_a_id, item_b_id, topic, description, recommendation, resolved, created_at)
|
|
335
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
336
|
+
).run(
|
|
337
|
+
report.id,
|
|
338
|
+
report.project,
|
|
339
|
+
report.itemAId,
|
|
340
|
+
report.itemBId,
|
|
341
|
+
report.topic,
|
|
342
|
+
report.description,
|
|
343
|
+
report.recommendation,
|
|
344
|
+
report.resolved ? 1 : 0,
|
|
345
|
+
report.createdAt
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
getUnresolvedContradictions(project) {
|
|
349
|
+
const rows = this.db.prepare("SELECT * FROM contradictions WHERE project = ? AND resolved = 0").all(project);
|
|
350
|
+
return rows.map((row) => this.mapContradiction(row));
|
|
351
|
+
}
|
|
352
|
+
resolveContradiction(id) {
|
|
353
|
+
this.db.prepare("UPDATE contradictions SET resolved = 1 WHERE id = ?").run(id);
|
|
354
|
+
}
|
|
355
|
+
queueTranscript(entry) {
|
|
356
|
+
this.db.prepare(
|
|
357
|
+
`INSERT OR IGNORE INTO pending_transcripts (id, transcript_path, project, project_root, provider, queued_at)
|
|
358
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
359
|
+
).run(entry.id, entry.transcriptPath, entry.project, entry.projectRoot, entry.provider, entry.queuedAt);
|
|
360
|
+
}
|
|
361
|
+
popPendingTranscript() {
|
|
362
|
+
const row = this.db.prepare("SELECT * FROM pending_transcripts ORDER BY queued_at ASC LIMIT 1").get();
|
|
363
|
+
if (!row) return null;
|
|
364
|
+
this.db.prepare("DELETE FROM pending_transcripts WHERE id = ?").run(row.id);
|
|
365
|
+
return {
|
|
366
|
+
id: row.id,
|
|
367
|
+
transcriptPath: row.transcript_path,
|
|
368
|
+
project: row.project,
|
|
369
|
+
projectRoot: row.project_root,
|
|
370
|
+
provider: row.provider,
|
|
371
|
+
queuedAt: row.queued_at
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
getPendingCount() {
|
|
375
|
+
const row = this.db.prepare("SELECT COUNT(*) as count FROM pending_transcripts").get();
|
|
376
|
+
return row.count;
|
|
377
|
+
}
|
|
378
|
+
close() {
|
|
379
|
+
this.db.close();
|
|
380
|
+
}
|
|
381
|
+
mapSession(row) {
|
|
382
|
+
return {
|
|
383
|
+
id: row.id,
|
|
384
|
+
project: row.project,
|
|
385
|
+
startedAt: row.started_at,
|
|
386
|
+
endedAt: row.ended_at,
|
|
387
|
+
gitBranch: row.git_branch,
|
|
388
|
+
gitCommit: row.git_commit,
|
|
389
|
+
provider: row.provider,
|
|
390
|
+
model: row.model,
|
|
391
|
+
transcriptPath: row.transcript_path,
|
|
392
|
+
observationCount: row.observation_count
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
mapObservation(row) {
|
|
396
|
+
return {
|
|
397
|
+
id: row.id,
|
|
398
|
+
sessionId: row.session_id,
|
|
399
|
+
timestamp: row.timestamp,
|
|
400
|
+
type: row.type,
|
|
401
|
+
content: row.content,
|
|
402
|
+
sourceQuote: row.source_quote,
|
|
403
|
+
confidence: row.confidence,
|
|
404
|
+
project: row.project,
|
|
405
|
+
scope: row.scope || "project"
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
mapKnowledgeItem(row) {
|
|
409
|
+
return {
|
|
410
|
+
id: row.id,
|
|
411
|
+
canonicalHash: row.canonical_hash,
|
|
412
|
+
type: row.type,
|
|
413
|
+
title: row.title,
|
|
414
|
+
content: row.content,
|
|
415
|
+
confidence: row.confidence,
|
|
416
|
+
observationCount: row.observation_count,
|
|
417
|
+
authority: row.authority,
|
|
418
|
+
status: row.status,
|
|
419
|
+
supersededById: row.superseded_by_id ?? void 0,
|
|
420
|
+
enforce: row.enforce === 1,
|
|
421
|
+
project: row.project,
|
|
422
|
+
scope: row.scope || "project",
|
|
423
|
+
createdAt: row.created_at,
|
|
424
|
+
updatedAt: row.updated_at,
|
|
425
|
+
lastSeenAt: row.last_seen_at,
|
|
426
|
+
metadata: JSON.parse(row.metadata)
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
mapCompileRun(row) {
|
|
430
|
+
return {
|
|
431
|
+
id: row.id,
|
|
432
|
+
project: row.project,
|
|
433
|
+
sessionId: row.session_id,
|
|
434
|
+
compilerVersion: row.compiler_version,
|
|
435
|
+
promptVersions: JSON.parse(row.prompt_versions),
|
|
436
|
+
startedAt: row.started_at,
|
|
437
|
+
endedAt: row.ended_at,
|
|
438
|
+
durationMs: row.duration_ms,
|
|
439
|
+
observationsProcessed: row.observations_processed,
|
|
440
|
+
knowledgeCreated: row.knowledge_created,
|
|
441
|
+
knowledgeReinforced: row.knowledge_reinforced,
|
|
442
|
+
knowledgeDeprecated: row.knowledge_deprecated,
|
|
443
|
+
knowledgeSuperseded: row.knowledge_superseded,
|
|
444
|
+
knowledgeIgnored: row.knowledge_ignored,
|
|
445
|
+
contradictionsDetected: row.contradictions_detected,
|
|
446
|
+
diagnostics: row.diagnostics
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
mapContradiction(row) {
|
|
450
|
+
return {
|
|
451
|
+
id: row.id,
|
|
452
|
+
project: row.project,
|
|
453
|
+
itemAId: row.item_a_id,
|
|
454
|
+
itemBId: row.item_b_id,
|
|
455
|
+
topic: row.topic,
|
|
456
|
+
description: row.description,
|
|
457
|
+
recommendation: row.recommendation,
|
|
458
|
+
resolved: row.resolved === 1,
|
|
459
|
+
createdAt: row.created_at
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
export {
|
|
465
|
+
SqliteKnowledgeRepository
|
|
466
|
+
};
|