k0ntext 3.0.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 +623 -0
- package/bin/k0ntext.js +12 -0
- package/dist/agents/cleanup-agent.d.ts +39 -0
- package/dist/agents/cleanup-agent.d.ts.map +1 -0
- package/dist/agents/cleanup-agent.js +56 -0
- package/dist/agents/cleanup-agent.js.map +1 -0
- package/dist/agents/performance-agent.d.ts +37 -0
- package/dist/agents/performance-agent.d.ts.map +1 -0
- package/dist/agents/performance-agent.js +91 -0
- package/dist/agents/performance-agent.js.map +1 -0
- package/dist/analyzer/index.d.ts +5 -0
- package/dist/analyzer/index.d.ts.map +1 -0
- package/dist/analyzer/index.js +5 -0
- package/dist/analyzer/index.js.map +1 -0
- package/dist/analyzer/intelligent-analyzer.d.ts +111 -0
- package/dist/analyzer/intelligent-analyzer.d.ts.map +1 -0
- package/dist/analyzer/intelligent-analyzer.js +537 -0
- package/dist/analyzer/intelligent-analyzer.js.map +1 -0
- package/dist/cli/commands/cleanup.d.ts +3 -0
- package/dist/cli/commands/cleanup.d.ts.map +1 -0
- package/dist/cli/commands/cleanup.js +24 -0
- package/dist/cli/commands/cleanup.js.map +1 -0
- package/dist/cli/commands/export.d.ts +9 -0
- package/dist/cli/commands/export.d.ts.map +1 -0
- package/dist/cli/commands/export.js +72 -0
- package/dist/cli/commands/export.js.map +1 -0
- package/dist/cli/commands/import.d.ts +9 -0
- package/dist/cli/commands/import.d.ts.map +1 -0
- package/dist/cli/commands/import.js +62 -0
- package/dist/cli/commands/import.js.map +1 -0
- package/dist/cli/commands/performance.d.ts +9 -0
- package/dist/cli/commands/performance.d.ts.map +1 -0
- package/dist/cli/commands/performance.js +36 -0
- package/dist/cli/commands/performance.js.map +1 -0
- package/dist/cli/commands/validate.d.ts +9 -0
- package/dist/cli/commands/validate.d.ts.map +1 -0
- package/dist/cli/commands/validate.js +82 -0
- package/dist/cli/commands/validate.js.map +1 -0
- package/dist/cli/commands/watch.d.ts +9 -0
- package/dist/cli/commands/watch.d.ts.map +1 -0
- package/dist/cli/commands/watch.js +72 -0
- package/dist/cli/commands/watch.js.map +1 -0
- package/dist/cli/generate.d.ts +3 -0
- package/dist/cli/generate.d.ts.map +1 -0
- package/dist/cli/generate.js +194 -0
- package/dist/cli/generate.js.map +1 -0
- package/dist/cli/index.d.ts +9 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +448 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/sync.d.ts +26 -0
- package/dist/cli/sync.d.ts.map +1 -0
- package/dist/cli/sync.js +163 -0
- package/dist/cli/sync.js.map +1 -0
- package/dist/config/cleanup-config.d.ts +26 -0
- package/dist/config/cleanup-config.d.ts.map +1 -0
- package/dist/config/cleanup-config.js +21 -0
- package/dist/config/cleanup-config.js.map +1 -0
- package/dist/db/client.d.ts +284 -0
- package/dist/db/client.d.ts.map +1 -0
- package/dist/db/client.js +688 -0
- package/dist/db/client.js.map +1 -0
- package/dist/db/index.d.ts +6 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +6 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/schema.d.ts +41 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +226 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/embeddings/index.d.ts +5 -0
- package/dist/embeddings/index.d.ts.map +1 -0
- package/dist/embeddings/index.js +5 -0
- package/dist/embeddings/index.js.map +1 -0
- package/dist/embeddings/openrouter.d.ts +133 -0
- package/dist/embeddings/openrouter.d.ts.map +1 -0
- package/dist/embeddings/openrouter.js +455 -0
- package/dist/embeddings/openrouter.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp.d.ts +29 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +257 -0
- package/dist/mcp.js.map +1 -0
- package/docs/ARCHIVE/MIGRATE_TO_NEW_REPO.md +222 -0
- package/docs/ARCHIVE/MIGRATE_TO_UNIFIED.md +220 -0
- package/docs/CLEANUP.md +76 -0
- package/docs/MCP_QUICKSTART.md +219 -0
- package/docs/QUICKSTART.md +119 -0
- package/docs/TROUBLESHOOTING.md +611 -0
- package/package.json +100 -0
- package/skills/context-optimize/SKILL.md +86 -0
- package/skills/implement/SKILL.md +150 -0
- package/skills/plan/SKILL.md +143 -0
- package/skills/research/SKILL.md +103 -0
- package/skills/validate/SKILL.md +62 -0
- package/skills/verify-docs/SKILL.md +77 -0
- package/src/agents/cleanup-agent.ts +96 -0
- package/src/agents/performance-agent.ts +117 -0
- package/src/analyzer/index.ts +10 -0
- package/src/analyzer/intelligent-analyzer.ts +640 -0
- package/src/cli/commands/cleanup.ts +26 -0
- package/src/cli/commands/export.ts +82 -0
- package/src/cli/commands/import.ts +73 -0
- package/src/cli/commands/performance.ts +40 -0
- package/src/cli/commands/validate.ts +98 -0
- package/src/cli/commands/watch.ts +83 -0
- package/src/cli/generate.ts +219 -0
- package/src/cli/index.ts +510 -0
- package/src/cli/sync.ts +194 -0
- package/src/config/cleanup-config.ts +42 -0
- package/src/db/client.ts +949 -0
- package/src/db/index.ts +19 -0
- package/src/db/schema.ts +241 -0
- package/src/embeddings/index.ts +11 -0
- package/src/embeddings/openrouter.ts +592 -0
- package/src/index.ts +57 -0
- package/src/mcp.ts +354 -0
- package/templates/AI_CONTEXT.md.template +245 -0
- package/templates/base/README.md +260 -0
- package/templates/base/RPI_WORKFLOW_PLAN.md +325 -0
- package/templates/base/agents/api-developer.md +76 -0
- package/templates/base/agents/context-engineer.md +525 -0
- package/templates/base/agents/core-architect.md +76 -0
- package/templates/base/agents/database-ops.md +76 -0
- package/templates/base/agents/deployment-ops.md +76 -0
- package/templates/base/agents/integration-hub.md +76 -0
- package/templates/base/analytics/README.md +114 -0
- package/templates/base/automation/config.json +58 -0
- package/templates/base/automation/generators/code-mapper.js +308 -0
- package/templates/base/automation/generators/index-builder.js +321 -0
- package/templates/base/automation/hooks/post-commit.sh +83 -0
- package/templates/base/automation/hooks/pre-commit.sh +103 -0
- package/templates/base/ci-templates/README.md +108 -0
- package/templates/base/ci-templates/github-actions/context-check.yml +144 -0
- package/templates/base/ci-templates/github-actions/validate-docs.yml +105 -0
- package/templates/base/commands/analytics.md +238 -0
- package/templates/base/commands/auto-sync.md +172 -0
- package/templates/base/commands/collab.md +194 -0
- package/templates/base/commands/context-optimize.md +226 -0
- package/templates/base/commands/help.md +485 -0
- package/templates/base/commands/rpi-implement.md +164 -0
- package/templates/base/commands/rpi-plan.md +147 -0
- package/templates/base/commands/rpi-research.md +145 -0
- package/templates/base/commands/session-resume.md +144 -0
- package/templates/base/commands/session-save.md +112 -0
- package/templates/base/commands/validate-all.md +77 -0
- package/templates/base/commands/verify-docs-current.md +86 -0
- package/templates/base/config/base.json +57 -0
- package/templates/base/config/environments/development.json +13 -0
- package/templates/base/config/environments/production.json +17 -0
- package/templates/base/config/environments/staging.json +13 -0
- package/templates/base/config/local.json.example +21 -0
- package/templates/base/context/.meta/generated-at.json +18 -0
- package/templates/base/context/ARCHITECTURE_SNAPSHOT.md +156 -0
- package/templates/base/context/CODE_TO_WORKFLOW_MAP.md +94 -0
- package/templates/base/context/FILE_OWNERSHIP.md +57 -0
- package/templates/base/context/INTEGRATION_POINTS.md +92 -0
- package/templates/base/context/KNOWN_GOTCHAS.md +195 -0
- package/templates/base/context/TESTING_MAP.md +95 -0
- package/templates/base/context/WORKFLOW_INDEX.md +129 -0
- package/templates/base/context/workflows/WORKFLOW_TEMPLATE.md +294 -0
- package/templates/base/indexes/agents/CAPABILITY_MATRIX.md +255 -0
- package/templates/base/indexes/agents/CATEGORY_INDEX.md +44 -0
- package/templates/base/indexes/code/CATEGORY_INDEX.md +38 -0
- package/templates/base/indexes/routing/CATEGORY_INDEX.md +39 -0
- package/templates/base/indexes/search/CATEGORY_INDEX.md +39 -0
- package/templates/base/indexes/workflows/CATEGORY_INDEX.md +38 -0
- package/templates/base/knowledge/README.md +98 -0
- package/templates/base/knowledge/sessions/README.md +88 -0
- package/templates/base/knowledge/sessions/TEMPLATE.md +150 -0
- package/templates/base/knowledge/shared/decisions/0001-adopt-context-engineering.md +144 -0
- package/templates/base/knowledge/shared/decisions/README.md +49 -0
- package/templates/base/knowledge/shared/decisions/TEMPLATE.md +123 -0
- package/templates/base/knowledge/shared/patterns/README.md +62 -0
- package/templates/base/knowledge/shared/patterns/TEMPLATE.md +120 -0
- package/templates/base/plans/PLAN_TEMPLATE.md +316 -0
- package/templates/base/plans/active/.gitkeep +0 -0
- package/templates/base/plans/completed/.gitkeep +0 -0
- package/templates/base/research/RESEARCH_TEMPLATE.md +245 -0
- package/templates/base/research/active/.gitkeep +0 -0
- package/templates/base/research/completed/.gitkeep +0 -0
- package/templates/base/schemas/agent.schema.json +141 -0
- package/templates/base/schemas/anchors.schema.json +54 -0
- package/templates/base/schemas/automation.schema.json +93 -0
- package/templates/base/schemas/command.schema.json +134 -0
- package/templates/base/schemas/hashes.schema.json +40 -0
- package/templates/base/schemas/manifest.schema.json +117 -0
- package/templates/base/schemas/plan.schema.json +136 -0
- package/templates/base/schemas/research.schema.json +115 -0
- package/templates/base/schemas/roles.schema.json +34 -0
- package/templates/base/schemas/session.schema.json +77 -0
- package/templates/base/schemas/settings.schema.json +244 -0
- package/templates/base/schemas/staleness.schema.json +53 -0
- package/templates/base/schemas/team-config.schema.json +42 -0
- package/templates/base/schemas/workflow.schema.json +126 -0
- package/templates/base/session/checkpoints/.gitkeep +2 -0
- package/templates/base/session/current/state.json +20 -0
- package/templates/base/session/history/.gitkeep +2 -0
- package/templates/base/settings.json +3 -0
- package/templates/base/standards/COMPATIBILITY.md +219 -0
- package/templates/base/standards/EXTENSION_GUIDELINES.md +280 -0
- package/templates/base/standards/QUALITY_CHECKLIST.md +211 -0
- package/templates/base/standards/README.md +66 -0
- package/templates/base/sync/anchors.json +6 -0
- package/templates/base/sync/hashes.json +6 -0
- package/templates/base/sync/staleness.json +10 -0
- package/templates/base/team/README.md +168 -0
- package/templates/base/team/config.json +79 -0
- package/templates/base/team/roles.json +145 -0
- package/templates/base/tools/bin/claude-context.js +151 -0
- package/templates/base/tools/lib/anchor-resolver.js +276 -0
- package/templates/base/tools/lib/config-loader.js +363 -0
- package/templates/base/tools/lib/detector.js +350 -0
- package/templates/base/tools/lib/diagnose.js +206 -0
- package/templates/base/tools/lib/drift-detector.js +373 -0
- package/templates/base/tools/lib/errors.js +199 -0
- package/templates/base/tools/lib/index.js +36 -0
- package/templates/base/tools/lib/init.js +192 -0
- package/templates/base/tools/lib/logger.js +230 -0
- package/templates/base/tools/lib/placeholder.js +201 -0
- package/templates/base/tools/lib/session-manager.js +354 -0
- package/templates/base/tools/lib/validate.js +521 -0
- package/templates/base/tools/package.json +49 -0
- package/templates/handlebars/aider-config.hbs +146 -0
- package/templates/handlebars/antigravity.hbs +377 -0
- package/templates/handlebars/claude.hbs +183 -0
- package/templates/handlebars/cline.hbs +62 -0
- package/templates/handlebars/continue-config.hbs +116 -0
- package/templates/handlebars/copilot.hbs +130 -0
- package/templates/handlebars/partials/gotcha-list.hbs +11 -0
- package/templates/handlebars/partials/header.hbs +3 -0
- package/templates/handlebars/partials/workflow-summary.hbs +16 -0
- package/templates/handlebars/windsurf-rules.hbs +69 -0
- package/templates/hooks/post-commit.hbs +28 -0
- package/templates/hooks/pre-commit.hbs +46 -0
package/src/db/client.ts
ADDED
|
@@ -0,0 +1,949 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Client
|
|
3
|
+
*
|
|
4
|
+
* SQLite database operations for AI context storage.
|
|
5
|
+
* Handles CRUD operations, queries, and vector search.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import Database from 'better-sqlite3';
|
|
9
|
+
import * as sqliteVec from 'sqlite-vec';
|
|
10
|
+
import { createHash } from 'crypto';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import {
|
|
14
|
+
SCHEMA_SQL,
|
|
15
|
+
VECTOR_SCHEMA_SQL,
|
|
16
|
+
SCHEMA_VERSION,
|
|
17
|
+
type ContextType,
|
|
18
|
+
type RelationType,
|
|
19
|
+
type SyncStatus,
|
|
20
|
+
type AITool
|
|
21
|
+
} from './schema.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Context item structure
|
|
25
|
+
*/
|
|
26
|
+
export interface ContextItem {
|
|
27
|
+
id: string;
|
|
28
|
+
type: ContextType;
|
|
29
|
+
name: string;
|
|
30
|
+
content: string;
|
|
31
|
+
metadata?: Record<string, unknown>;
|
|
32
|
+
filePath?: string;
|
|
33
|
+
contentHash?: string;
|
|
34
|
+
createdAt?: string;
|
|
35
|
+
updatedAt?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Knowledge graph edge
|
|
40
|
+
*/
|
|
41
|
+
export interface GraphEdge {
|
|
42
|
+
id?: number;
|
|
43
|
+
sourceId: string;
|
|
44
|
+
targetId: string;
|
|
45
|
+
relationType: RelationType;
|
|
46
|
+
weight?: number;
|
|
47
|
+
metadata?: Record<string, unknown>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Git commit record
|
|
52
|
+
*/
|
|
53
|
+
export interface GitCommit {
|
|
54
|
+
sha: string;
|
|
55
|
+
message: string;
|
|
56
|
+
authorName?: string;
|
|
57
|
+
authorEmail?: string;
|
|
58
|
+
timestamp: string;
|
|
59
|
+
filesChanged?: string[];
|
|
60
|
+
stats?: { additions: number; deletions: number };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Sync state record
|
|
65
|
+
*/
|
|
66
|
+
export interface SyncState {
|
|
67
|
+
id: string;
|
|
68
|
+
tool: string;
|
|
69
|
+
contentHash?: string;
|
|
70
|
+
lastSync: string;
|
|
71
|
+
filePath?: string;
|
|
72
|
+
status: SyncStatus;
|
|
73
|
+
metadata?: Record<string, unknown>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* AI tool configuration record
|
|
78
|
+
*/
|
|
79
|
+
export interface AIToolConfig {
|
|
80
|
+
id: string;
|
|
81
|
+
toolName: AITool;
|
|
82
|
+
configPath: string;
|
|
83
|
+
content: string;
|
|
84
|
+
contentHash?: string;
|
|
85
|
+
lastSync: string;
|
|
86
|
+
status: SyncStatus;
|
|
87
|
+
metadata?: Record<string, unknown>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Search result with similarity score
|
|
92
|
+
*/
|
|
93
|
+
export interface SearchResult {
|
|
94
|
+
item: ContextItem;
|
|
95
|
+
similarity: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Database client for AI context storage
|
|
100
|
+
*/
|
|
101
|
+
export class DatabaseClient {
|
|
102
|
+
private db: Database.Database;
|
|
103
|
+
private dbPath: string;
|
|
104
|
+
|
|
105
|
+
constructor(projectRoot: string, dbFileName = '.k0ntext.db') {
|
|
106
|
+
this.dbPath = path.join(projectRoot, dbFileName);
|
|
107
|
+
|
|
108
|
+
// Ensure directory exists
|
|
109
|
+
const dbDir = path.dirname(this.dbPath);
|
|
110
|
+
if (!fs.existsSync(dbDir)) {
|
|
111
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
this.db = new Database(this.dbPath);
|
|
115
|
+
|
|
116
|
+
// Enable foreign keys
|
|
117
|
+
this.db.pragma('foreign_keys = ON');
|
|
118
|
+
|
|
119
|
+
// Load sqlite-vec extension
|
|
120
|
+
sqliteVec.load(this.db);
|
|
121
|
+
|
|
122
|
+
// Initialize schema
|
|
123
|
+
this.initSchema();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Migrate legacy database
|
|
128
|
+
*/
|
|
129
|
+
private migrateLegacyDatabase(): void {
|
|
130
|
+
const legacyPath = path.join(process.cwd(), '.ai-context.db');
|
|
131
|
+
const newPath = this.dbPath;
|
|
132
|
+
|
|
133
|
+
if (fs.existsSync(legacyPath) && !fs.existsSync(newPath)) {
|
|
134
|
+
fs.copyFileSync(legacyPath, newPath);
|
|
135
|
+
console.log(`✓ Migrated .ai-context.db to .k0ntext.db`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Initialize database schema
|
|
141
|
+
*/
|
|
142
|
+
private initSchema(): void {
|
|
143
|
+
// Migrate legacy database first
|
|
144
|
+
this.migrateLegacyDatabase();
|
|
145
|
+
|
|
146
|
+
// Create core tables
|
|
147
|
+
this.db.exec(SCHEMA_SQL);
|
|
148
|
+
|
|
149
|
+
// Create vector table
|
|
150
|
+
this.db.exec(VECTOR_SCHEMA_SQL);
|
|
151
|
+
|
|
152
|
+
// Record schema version
|
|
153
|
+
const stmt = this.db.prepare(`
|
|
154
|
+
INSERT OR REPLACE INTO schema_version (version, applied_at)
|
|
155
|
+
VALUES (?, datetime('now'))
|
|
156
|
+
`);
|
|
157
|
+
stmt.run(SCHEMA_VERSION);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Execute callback within a transaction (sync)
|
|
162
|
+
*/
|
|
163
|
+
transaction<T>(callback: () => T): T;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Execute callback within a transaction (async)
|
|
167
|
+
*/
|
|
168
|
+
transaction<T>(callback: () => Promise<T>): Promise<T>;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Execute callback within a transaction (implementation)
|
|
172
|
+
*/
|
|
173
|
+
transaction<T>(callback: () => T | Promise<T>): T | Promise<T> {
|
|
174
|
+
// Detect if callback is async by checking if it returns a Promise
|
|
175
|
+
const result = callback();
|
|
176
|
+
|
|
177
|
+
if (result instanceof Promise) {
|
|
178
|
+
// For async, use manual transaction control
|
|
179
|
+
return (async () => {
|
|
180
|
+
this.db.exec('BEGIN TRANSACTION');
|
|
181
|
+
try {
|
|
182
|
+
const value = await result;
|
|
183
|
+
this.db.exec('COMMIT');
|
|
184
|
+
return value;
|
|
185
|
+
} catch (error) {
|
|
186
|
+
this.db.exec('ROLLBACK');
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
})();
|
|
190
|
+
} else {
|
|
191
|
+
// For sync, use better-sqlite3 transaction helper
|
|
192
|
+
const txn = this.db.transaction(callback as () => T);
|
|
193
|
+
return txn();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Begin a manual transaction (returns rollback/commit functions)
|
|
199
|
+
*/
|
|
200
|
+
beginTransaction(): { rollback: () => void; commit: () => void } {
|
|
201
|
+
this.db.exec('BEGIN TRANSACTION');
|
|
202
|
+
return {
|
|
203
|
+
rollback: () => this.db.exec('ROLLBACK'),
|
|
204
|
+
commit: () => this.db.exec('COMMIT')
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Check database connection health
|
|
210
|
+
*/
|
|
211
|
+
healthCheck(): { healthy: boolean; error?: string } {
|
|
212
|
+
try {
|
|
213
|
+
this.db.prepare('SELECT 1').get();
|
|
214
|
+
return { healthy: true };
|
|
215
|
+
} catch (error) {
|
|
216
|
+
return {
|
|
217
|
+
healthy: false,
|
|
218
|
+
error: error instanceof Error ? error.message : String(error)
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Generate content hash for deduplication
|
|
225
|
+
*/
|
|
226
|
+
private hashContent(content: string): string {
|
|
227
|
+
return createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Generate a unique ID for a context item
|
|
232
|
+
*/
|
|
233
|
+
private generateId(type: ContextType, name: string): string {
|
|
234
|
+
return `${type}:${name.toLowerCase().replace(/[^a-z0-9]/g, '-')}`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ==================== Context Items ====================
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Insert or update a context item
|
|
241
|
+
*/
|
|
242
|
+
upsertItem(item: Omit<ContextItem, 'id' | 'contentHash' | 'createdAt' | 'updatedAt'>): ContextItem {
|
|
243
|
+
const id = this.generateId(item.type, item.name);
|
|
244
|
+
const contentHash = this.hashContent(item.content);
|
|
245
|
+
|
|
246
|
+
const stmt = this.db.prepare(`
|
|
247
|
+
INSERT INTO context_items (id, type, name, content, metadata, file_path, content_hash, updated_at)
|
|
248
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
|
249
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
250
|
+
content = excluded.content,
|
|
251
|
+
metadata = excluded.metadata,
|
|
252
|
+
file_path = excluded.file_path,
|
|
253
|
+
content_hash = excluded.content_hash,
|
|
254
|
+
updated_at = datetime('now')
|
|
255
|
+
RETURNING *
|
|
256
|
+
`);
|
|
257
|
+
|
|
258
|
+
const row = stmt.get(
|
|
259
|
+
id,
|
|
260
|
+
item.type,
|
|
261
|
+
item.name,
|
|
262
|
+
item.content,
|
|
263
|
+
item.metadata ? JSON.stringify(item.metadata) : null,
|
|
264
|
+
item.filePath || null,
|
|
265
|
+
contentHash
|
|
266
|
+
) as Record<string, unknown>;
|
|
267
|
+
|
|
268
|
+
return this.rowToItem(row);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Get a context item by ID
|
|
273
|
+
*/
|
|
274
|
+
getItem(id: string): ContextItem | null {
|
|
275
|
+
const stmt = this.db.prepare('SELECT * FROM context_items WHERE id = ?');
|
|
276
|
+
const row = stmt.get(id) as Record<string, unknown> | undefined;
|
|
277
|
+
return row ? this.rowToItem(row) : null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Get items by type
|
|
282
|
+
*/
|
|
283
|
+
getItemsByType(type: ContextType): ContextItem[] {
|
|
284
|
+
const stmt = this.db.prepare('SELECT * FROM context_items WHERE type = ? ORDER BY name');
|
|
285
|
+
const rows = stmt.all(type) as Record<string, unknown>[];
|
|
286
|
+
return rows.map(row => this.rowToItem(row));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Get all items
|
|
291
|
+
*/
|
|
292
|
+
getAllItems(): ContextItem[] {
|
|
293
|
+
const stmt = this.db.prepare('SELECT * FROM context_items ORDER BY type, name');
|
|
294
|
+
const rows = stmt.all() as Record<string, unknown>[];
|
|
295
|
+
return rows.map(row => this.rowToItem(row));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Delete a context item
|
|
300
|
+
*/
|
|
301
|
+
deleteItem(id: string): boolean {
|
|
302
|
+
const stmt = this.db.prepare('DELETE FROM context_items WHERE id = ?');
|
|
303
|
+
const result = stmt.run(id);
|
|
304
|
+
return result.changes > 0;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Delete items older than specified days
|
|
309
|
+
*/
|
|
310
|
+
deleteStaleItems(daysOld: number, type?: ContextType): number {
|
|
311
|
+
const stmt = this.db.prepare(`
|
|
312
|
+
DELETE FROM context_items
|
|
313
|
+
WHERE datetime(updated_at) < datetime('now', '-' || ? || ' days')
|
|
314
|
+
${type ? 'AND type = ?' : ''}
|
|
315
|
+
`);
|
|
316
|
+
const result = stmt.run(...(type ? [daysOld, type] : [daysOld]));
|
|
317
|
+
return result.changes;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Search items by text (full-text grep-style)
|
|
322
|
+
*/
|
|
323
|
+
searchText(query: string, type?: ContextType): ContextItem[] {
|
|
324
|
+
const pattern = `%${query}%`;
|
|
325
|
+
let sql = 'SELECT * FROM context_items WHERE (content LIKE ? OR name LIKE ?)';
|
|
326
|
+
const params: unknown[] = [pattern, pattern];
|
|
327
|
+
|
|
328
|
+
if (type) {
|
|
329
|
+
sql += ' AND type = ?';
|
|
330
|
+
params.push(type);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
sql += ' ORDER BY name LIMIT 50';
|
|
334
|
+
|
|
335
|
+
const stmt = this.db.prepare(sql);
|
|
336
|
+
const rows = stmt.all(...params) as Record<string, unknown>[];
|
|
337
|
+
return rows.map(row => this.rowToItem(row));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Convert database row to ContextItem
|
|
342
|
+
*/
|
|
343
|
+
private rowToItem(row: Record<string, unknown>): ContextItem {
|
|
344
|
+
return {
|
|
345
|
+
id: row.id as string,
|
|
346
|
+
type: row.type as ContextType,
|
|
347
|
+
name: row.name as string,
|
|
348
|
+
content: row.content as string,
|
|
349
|
+
metadata: row.metadata ? JSON.parse(row.metadata as string) : undefined,
|
|
350
|
+
filePath: row.file_path as string | undefined,
|
|
351
|
+
contentHash: row.content_hash as string | undefined,
|
|
352
|
+
createdAt: row.created_at as string | undefined,
|
|
353
|
+
updatedAt: row.updated_at as string | undefined
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Calculate relevance score for a search result
|
|
359
|
+
*/
|
|
360
|
+
private calculateRelevance(
|
|
361
|
+
item: ContextItem,
|
|
362
|
+
query: string,
|
|
363
|
+
baseScore: number
|
|
364
|
+
): number {
|
|
365
|
+
let score = baseScore;
|
|
366
|
+
|
|
367
|
+
// Boost score for exact name matches
|
|
368
|
+
if (item.name.toLowerCase().includes(query.toLowerCase())) {
|
|
369
|
+
score *= 1.5;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Boost score for recently updated items
|
|
373
|
+
if (item.updatedAt) {
|
|
374
|
+
const daysSinceUpdate = (Date.now() - new Date(item.updatedAt).getTime()) / (1000 * 60 * 60 * 24);
|
|
375
|
+
if (daysSinceUpdate < 7) {
|
|
376
|
+
score *= 1.2; // 20% boost for items updated within a week
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Boost score for certain types
|
|
381
|
+
if (item.type === 'workflow' || item.type === 'agent') {
|
|
382
|
+
score *= 1.1;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return score;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ==================== AI Tool Configs ====================
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Upsert an AI tool configuration
|
|
392
|
+
*/
|
|
393
|
+
upsertToolConfig(config: Omit<AIToolConfig, 'contentHash' | 'lastSync'>): AIToolConfig {
|
|
394
|
+
const contentHash = this.hashContent(config.content);
|
|
395
|
+
|
|
396
|
+
const stmt = this.db.prepare(`
|
|
397
|
+
INSERT INTO ai_tool_configs (id, tool_name, config_path, content, content_hash, last_sync, status, metadata)
|
|
398
|
+
VALUES (?, ?, ?, ?, ?, datetime('now'), ?, ?)
|
|
399
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
400
|
+
content = excluded.content,
|
|
401
|
+
content_hash = excluded.content_hash,
|
|
402
|
+
last_sync = datetime('now'),
|
|
403
|
+
status = excluded.status,
|
|
404
|
+
metadata = excluded.metadata
|
|
405
|
+
RETURNING *
|
|
406
|
+
`);
|
|
407
|
+
|
|
408
|
+
const row = stmt.get(
|
|
409
|
+
config.id,
|
|
410
|
+
config.toolName,
|
|
411
|
+
config.configPath,
|
|
412
|
+
config.content,
|
|
413
|
+
contentHash,
|
|
414
|
+
config.status,
|
|
415
|
+
config.metadata ? JSON.stringify(config.metadata) : null
|
|
416
|
+
) as Record<string, unknown>;
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
id: row.id as string,
|
|
420
|
+
toolName: row.tool_name as AITool,
|
|
421
|
+
configPath: row.config_path as string,
|
|
422
|
+
content: row.content as string,
|
|
423
|
+
contentHash: row.content_hash as string,
|
|
424
|
+
lastSync: row.last_sync as string,
|
|
425
|
+
status: row.status as SyncStatus,
|
|
426
|
+
metadata: row.metadata ? JSON.parse(row.metadata as string) : undefined
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Get tool configs by tool name
|
|
432
|
+
*/
|
|
433
|
+
getToolConfigs(toolName: AITool): AIToolConfig[] {
|
|
434
|
+
const stmt = this.db.prepare('SELECT * FROM ai_tool_configs WHERE tool_name = ?');
|
|
435
|
+
const rows = stmt.all(toolName) as Record<string, unknown>[];
|
|
436
|
+
|
|
437
|
+
return rows.map(row => ({
|
|
438
|
+
id: row.id as string,
|
|
439
|
+
toolName: row.tool_name as AITool,
|
|
440
|
+
configPath: row.config_path as string,
|
|
441
|
+
content: row.content as string,
|
|
442
|
+
contentHash: row.content_hash as string,
|
|
443
|
+
lastSync: row.last_sync as string,
|
|
444
|
+
status: row.status as SyncStatus,
|
|
445
|
+
metadata: row.metadata ? JSON.parse(row.metadata as string) : undefined
|
|
446
|
+
}));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Get all tool configs
|
|
451
|
+
*/
|
|
452
|
+
getAllToolConfigs(): AIToolConfig[] {
|
|
453
|
+
const stmt = this.db.prepare('SELECT * FROM ai_tool_configs ORDER BY tool_name');
|
|
454
|
+
const rows = stmt.all() as Record<string, unknown>[];
|
|
455
|
+
|
|
456
|
+
return rows.map(row => ({
|
|
457
|
+
id: row.id as string,
|
|
458
|
+
toolName: row.tool_name as AITool,
|
|
459
|
+
configPath: row.config_path as string,
|
|
460
|
+
content: row.content as string,
|
|
461
|
+
contentHash: row.content_hash as string,
|
|
462
|
+
lastSync: row.last_sync as string,
|
|
463
|
+
status: row.status as SyncStatus,
|
|
464
|
+
metadata: row.metadata ? JSON.parse(row.metadata as string) : undefined
|
|
465
|
+
}));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ==================== Knowledge Graph ====================
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Add a relationship to the knowledge graph
|
|
472
|
+
*/
|
|
473
|
+
addRelation(edge: Omit<GraphEdge, 'id'>): GraphEdge {
|
|
474
|
+
const stmt = this.db.prepare(`
|
|
475
|
+
INSERT INTO knowledge_graph (source_id, target_id, relation_type, weight, metadata)
|
|
476
|
+
VALUES (?, ?, ?, ?, ?)
|
|
477
|
+
ON CONFLICT(source_id, target_id, relation_type) DO UPDATE SET
|
|
478
|
+
weight = excluded.weight,
|
|
479
|
+
metadata = excluded.metadata
|
|
480
|
+
RETURNING *
|
|
481
|
+
`);
|
|
482
|
+
|
|
483
|
+
const row = stmt.get(
|
|
484
|
+
edge.sourceId,
|
|
485
|
+
edge.targetId,
|
|
486
|
+
edge.relationType,
|
|
487
|
+
edge.weight ?? 1.0,
|
|
488
|
+
edge.metadata ? JSON.stringify(edge.metadata) : null
|
|
489
|
+
) as Record<string, unknown>;
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
id: row.id as number,
|
|
493
|
+
sourceId: row.source_id as string,
|
|
494
|
+
targetId: row.target_id as string,
|
|
495
|
+
relationType: row.relation_type as RelationType,
|
|
496
|
+
weight: row.weight as number,
|
|
497
|
+
metadata: row.metadata ? JSON.parse(row.metadata as string) : undefined
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Get relations from a source item
|
|
503
|
+
*/
|
|
504
|
+
getRelationsFrom(sourceId: string, relationType?: RelationType): GraphEdge[] {
|
|
505
|
+
let sql = `
|
|
506
|
+
SELECT kg.*, ci.name as target_name
|
|
507
|
+
FROM knowledge_graph kg
|
|
508
|
+
JOIN context_items ci ON kg.target_id = ci.id
|
|
509
|
+
WHERE kg.source_id = ?
|
|
510
|
+
`;
|
|
511
|
+
const params: unknown[] = [sourceId];
|
|
512
|
+
|
|
513
|
+
if (relationType) {
|
|
514
|
+
sql += ' AND kg.relation_type = ?';
|
|
515
|
+
params.push(relationType);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
sql += ' ORDER BY kg.weight DESC';
|
|
519
|
+
|
|
520
|
+
const stmt = this.db.prepare(sql);
|
|
521
|
+
const rows = stmt.all(...params) as Record<string, unknown>[];
|
|
522
|
+
|
|
523
|
+
return rows.map(row => ({
|
|
524
|
+
id: row.id as number,
|
|
525
|
+
sourceId: row.source_id as string,
|
|
526
|
+
targetId: row.target_id as string,
|
|
527
|
+
relationType: row.relation_type as RelationType,
|
|
528
|
+
weight: row.weight as number,
|
|
529
|
+
metadata: row.metadata ? JSON.parse(row.metadata as string) : undefined
|
|
530
|
+
}));
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Get relations to a target item
|
|
535
|
+
*/
|
|
536
|
+
getRelationsTo(targetId: string, relationType?: RelationType): GraphEdge[] {
|
|
537
|
+
let sql = `
|
|
538
|
+
SELECT kg.*, ci.name as source_name
|
|
539
|
+
FROM knowledge_graph kg
|
|
540
|
+
JOIN context_items ci ON kg.source_id = ci.id
|
|
541
|
+
WHERE kg.target_id = ?
|
|
542
|
+
`;
|
|
543
|
+
const params: unknown[] = [targetId];
|
|
544
|
+
|
|
545
|
+
if (relationType) {
|
|
546
|
+
sql += ' AND kg.relation_type = ?';
|
|
547
|
+
params.push(relationType);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
sql += ' ORDER BY kg.weight DESC';
|
|
551
|
+
|
|
552
|
+
const stmt = this.db.prepare(sql);
|
|
553
|
+
const rows = stmt.all(...params) as Record<string, unknown>[];
|
|
554
|
+
|
|
555
|
+
return rows.map(row => ({
|
|
556
|
+
id: row.id as number,
|
|
557
|
+
sourceId: row.source_id as string,
|
|
558
|
+
targetId: row.target_id as string,
|
|
559
|
+
relationType: row.relation_type as RelationType,
|
|
560
|
+
weight: row.weight as number,
|
|
561
|
+
metadata: row.metadata ? JSON.parse(row.metadata as string) : undefined
|
|
562
|
+
}));
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Traverse the graph from a starting point
|
|
567
|
+
*/
|
|
568
|
+
traverseGraph(startId: string, maxDepth = 3): Map<string, { item: ContextItem; depth: number }> {
|
|
569
|
+
const visited = new Map<string, { item: ContextItem; depth: number }>();
|
|
570
|
+
const queue: Array<{ id: string; depth: number }> = [{ id: startId, depth: 0 }];
|
|
571
|
+
|
|
572
|
+
while (queue.length > 0) {
|
|
573
|
+
const { id, depth } = queue.shift()!;
|
|
574
|
+
|
|
575
|
+
if (visited.has(id) || depth > maxDepth) continue;
|
|
576
|
+
|
|
577
|
+
const item = this.getItem(id);
|
|
578
|
+
if (!item) continue;
|
|
579
|
+
|
|
580
|
+
visited.set(id, { item, depth });
|
|
581
|
+
|
|
582
|
+
// Get all outgoing relations
|
|
583
|
+
const relations = this.getRelationsFrom(id);
|
|
584
|
+
for (const rel of relations) {
|
|
585
|
+
if (!visited.has(rel.targetId)) {
|
|
586
|
+
queue.push({ id: rel.targetId, depth: depth + 1 });
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return visited;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// ==================== Git Commits ====================
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Insert or update a git commit
|
|
598
|
+
*/
|
|
599
|
+
upsertCommit(commit: GitCommit): void {
|
|
600
|
+
const stmt = this.db.prepare(`
|
|
601
|
+
INSERT INTO git_commits (sha, message, author_name, author_email, timestamp, files_changed, stats)
|
|
602
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
603
|
+
ON CONFLICT(sha) DO UPDATE SET
|
|
604
|
+
message = excluded.message,
|
|
605
|
+
files_changed = excluded.files_changed,
|
|
606
|
+
stats = excluded.stats
|
|
607
|
+
`);
|
|
608
|
+
|
|
609
|
+
stmt.run(
|
|
610
|
+
commit.sha,
|
|
611
|
+
commit.message,
|
|
612
|
+
commit.authorName || null,
|
|
613
|
+
commit.authorEmail || null,
|
|
614
|
+
commit.timestamp,
|
|
615
|
+
commit.filesChanged ? JSON.stringify(commit.filesChanged) : null,
|
|
616
|
+
commit.stats ? JSON.stringify(commit.stats) : null
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Get recent commits
|
|
622
|
+
*/
|
|
623
|
+
getRecentCommits(limit = 50): GitCommit[] {
|
|
624
|
+
const stmt = this.db.prepare(`
|
|
625
|
+
SELECT * FROM git_commits
|
|
626
|
+
ORDER BY timestamp DESC
|
|
627
|
+
LIMIT ?
|
|
628
|
+
`);
|
|
629
|
+
|
|
630
|
+
const rows = stmt.all(limit) as Record<string, unknown>[];
|
|
631
|
+
|
|
632
|
+
return rows.map(row => ({
|
|
633
|
+
sha: row.sha as string,
|
|
634
|
+
message: row.message as string,
|
|
635
|
+
authorName: row.author_name as string | undefined,
|
|
636
|
+
authorEmail: row.author_email as string | undefined,
|
|
637
|
+
timestamp: row.timestamp as string,
|
|
638
|
+
filesChanged: row.files_changed ? JSON.parse(row.files_changed as string) : undefined,
|
|
639
|
+
stats: row.stats ? JSON.parse(row.stats as string) : undefined
|
|
640
|
+
}));
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// ==================== Sync State ====================
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Update sync state for a tool
|
|
647
|
+
*/
|
|
648
|
+
updateSyncState(state: SyncState): void {
|
|
649
|
+
const stmt = this.db.prepare(`
|
|
650
|
+
INSERT INTO sync_state (id, tool, content_hash, last_sync, file_path, status, metadata)
|
|
651
|
+
VALUES (?, ?, ?, datetime('now'), ?, ?, ?)
|
|
652
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
653
|
+
content_hash = excluded.content_hash,
|
|
654
|
+
last_sync = datetime('now'),
|
|
655
|
+
status = excluded.status,
|
|
656
|
+
metadata = excluded.metadata
|
|
657
|
+
`);
|
|
658
|
+
|
|
659
|
+
stmt.run(
|
|
660
|
+
state.id,
|
|
661
|
+
state.tool,
|
|
662
|
+
state.contentHash || null,
|
|
663
|
+
state.filePath || null,
|
|
664
|
+
state.status,
|
|
665
|
+
state.metadata ? JSON.stringify(state.metadata) : null
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Get sync state for a tool
|
|
671
|
+
*/
|
|
672
|
+
getSyncState(tool: string): SyncState[] {
|
|
673
|
+
const stmt = this.db.prepare('SELECT * FROM sync_state WHERE tool = ?');
|
|
674
|
+
const rows = stmt.all(tool) as Record<string, unknown>[];
|
|
675
|
+
|
|
676
|
+
return rows.map(row => ({
|
|
677
|
+
id: row.id as string,
|
|
678
|
+
tool: row.tool as string,
|
|
679
|
+
contentHash: row.content_hash as string | undefined,
|
|
680
|
+
lastSync: row.last_sync as string,
|
|
681
|
+
filePath: row.file_path as string | undefined,
|
|
682
|
+
status: row.status as SyncStatus,
|
|
683
|
+
metadata: row.metadata ? JSON.parse(row.metadata as string) : undefined
|
|
684
|
+
}));
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// ==================== Embeddings ====================
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Store an embedding
|
|
691
|
+
*/
|
|
692
|
+
storeEmbedding(contextId: string, embedding: number[]): void {
|
|
693
|
+
const stmt = this.db.prepare(`
|
|
694
|
+
INSERT INTO embeddings (context_id, embedding)
|
|
695
|
+
VALUES (?, ?)
|
|
696
|
+
ON CONFLICT(context_id) DO UPDATE SET
|
|
697
|
+
embedding = excluded.embedding
|
|
698
|
+
`);
|
|
699
|
+
|
|
700
|
+
// Convert to blob for sqlite-vec
|
|
701
|
+
const buffer = new Float32Array(embedding);
|
|
702
|
+
stmt.run(contextId, Buffer.from(buffer.buffer));
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Search by embedding similarity
|
|
707
|
+
*/
|
|
708
|
+
searchByEmbedding(queryEmbedding: number[], limit = 10): SearchResult[] {
|
|
709
|
+
const buffer = new Float32Array(queryEmbedding);
|
|
710
|
+
|
|
711
|
+
const stmt = this.db.prepare(`
|
|
712
|
+
SELECT
|
|
713
|
+
e.context_id,
|
|
714
|
+
e.embedding,
|
|
715
|
+
ci.*,
|
|
716
|
+
vec_distance_cosine(e.embedding, ?) as distance
|
|
717
|
+
FROM embeddings e
|
|
718
|
+
JOIN context_items ci ON e.context_id = ci.id
|
|
719
|
+
ORDER BY distance
|
|
720
|
+
LIMIT ?
|
|
721
|
+
`);
|
|
722
|
+
|
|
723
|
+
const rows = stmt.all(Buffer.from(buffer.buffer), limit) as Record<string, unknown>[];
|
|
724
|
+
|
|
725
|
+
return rows.map(row => ({
|
|
726
|
+
item: this.rowToItem(row),
|
|
727
|
+
similarity: 1 - (row.distance as number || 0) // Convert distance to similarity
|
|
728
|
+
}));
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Hybrid search combining vector and text search
|
|
733
|
+
*/
|
|
734
|
+
hybridSearch(
|
|
735
|
+
query: string,
|
|
736
|
+
queryEmbedding: number[] | null,
|
|
737
|
+
options: {
|
|
738
|
+
limit?: number;
|
|
739
|
+
type?: ContextType;
|
|
740
|
+
vectorWeight?: number; // 0-1, higher = more weight on semantic
|
|
741
|
+
} = {}
|
|
742
|
+
): SearchResult[] {
|
|
743
|
+
const {
|
|
744
|
+
limit = 10,
|
|
745
|
+
type,
|
|
746
|
+
vectorWeight = 0.7
|
|
747
|
+
} = options;
|
|
748
|
+
|
|
749
|
+
const textResults = this.searchText(query, type);
|
|
750
|
+
const semanticResults = queryEmbedding ? this.searchByEmbedding(queryEmbedding, limit * 2) : [];
|
|
751
|
+
|
|
752
|
+
// Combine and score
|
|
753
|
+
const combinedScores = new Map<string, number>();
|
|
754
|
+
|
|
755
|
+
// Score text results (inverse of position)
|
|
756
|
+
for (let i = 0; i < textResults.length; i++) {
|
|
757
|
+
const score = (1 - i / textResults.length) * (1 - vectorWeight);
|
|
758
|
+
combinedScores.set(textResults[i].id, (combinedScores.get(textResults[i].id) || 0) + score);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Score semantic results
|
|
762
|
+
for (const result of semanticResults) {
|
|
763
|
+
const score = result.similarity * vectorWeight;
|
|
764
|
+
combinedScores.set(result.item.id, (combinedScores.get(result.item.id) || 0) + score);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Sort by combined score
|
|
768
|
+
const results = Array.from(combinedScores.entries())
|
|
769
|
+
.sort((a, b) => b[1] - a[1])
|
|
770
|
+
.slice(0, limit)
|
|
771
|
+
.map(([id]) => this.getItem(id)!)
|
|
772
|
+
.filter(item => item !== null);
|
|
773
|
+
|
|
774
|
+
return results.map(item => ({
|
|
775
|
+
item,
|
|
776
|
+
similarity: combinedScores.get(item.id) || 0
|
|
777
|
+
}));
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Delete an embedding
|
|
782
|
+
*/
|
|
783
|
+
deleteEmbedding(contextId: string): boolean {
|
|
784
|
+
const stmt = this.db.prepare('DELETE FROM embeddings WHERE context_id = ?');
|
|
785
|
+
const result = stmt.run(contextId);
|
|
786
|
+
return result.changes > 0;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Insert or update an embedding for a file by path
|
|
791
|
+
*/
|
|
792
|
+
insertEmbedding(filePath: string, embedding: number[]): void {
|
|
793
|
+
const itemId = this.getItemIdByPath(filePath);
|
|
794
|
+
|
|
795
|
+
if (!itemId) {
|
|
796
|
+
throw new Error(`Cannot insert embedding: no item found for path ${filePath}`);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
this.storeEmbedding(itemId, embedding);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Get item ID by file path
|
|
804
|
+
*/
|
|
805
|
+
private getItemIdByPath(filePath: string): string | null {
|
|
806
|
+
const stmt = this.db.prepare('SELECT id FROM context_items WHERE file_path = ? LIMIT 1');
|
|
807
|
+
const row = stmt.get(filePath) as { id: string } | undefined;
|
|
808
|
+
return row?.id || null;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// ==================== Analytics ====================
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Log a usage event
|
|
815
|
+
*/
|
|
816
|
+
logUsage(toolName: string, query?: string, resultCount?: number, latencyMs?: number): void {
|
|
817
|
+
const stmt = this.db.prepare(`
|
|
818
|
+
INSERT INTO usage_analytics (tool_name, query, result_count, latency_ms)
|
|
819
|
+
VALUES (?, ?, ?, ?)
|
|
820
|
+
`);
|
|
821
|
+
|
|
822
|
+
stmt.run(toolName, query || null, resultCount ?? null, latencyMs ?? null);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Get usage statistics
|
|
827
|
+
*/
|
|
828
|
+
getUsageStats(days = 30): { toolName: string; count: number; avgLatency: number }[] {
|
|
829
|
+
const stmt = this.db.prepare(`
|
|
830
|
+
SELECT
|
|
831
|
+
tool_name,
|
|
832
|
+
COUNT(*) as count,
|
|
833
|
+
AVG(latency_ms) as avg_latency
|
|
834
|
+
FROM usage_analytics
|
|
835
|
+
WHERE timestamp > datetime('now', '-' || ? || ' days')
|
|
836
|
+
GROUP BY tool_name
|
|
837
|
+
ORDER BY count DESC
|
|
838
|
+
`);
|
|
839
|
+
|
|
840
|
+
const rows = stmt.all(days) as Record<string, unknown>[];
|
|
841
|
+
|
|
842
|
+
return rows.map(row => ({
|
|
843
|
+
toolName: row.tool_name as string,
|
|
844
|
+
count: row.count as number,
|
|
845
|
+
avgLatency: row.avg_latency as number
|
|
846
|
+
}));
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// ==================== Utility ====================
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Get database path
|
|
853
|
+
*/
|
|
854
|
+
getPath(): string {
|
|
855
|
+
return this.dbPath;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Get database statistics
|
|
860
|
+
*/
|
|
861
|
+
getStats(): {
|
|
862
|
+
items: number;
|
|
863
|
+
relations: number;
|
|
864
|
+
commits: number;
|
|
865
|
+
embeddings: number;
|
|
866
|
+
toolConfigs: number;
|
|
867
|
+
} {
|
|
868
|
+
const itemCount = (this.db.prepare('SELECT COUNT(*) as count FROM context_items').get() as { count: number }).count;
|
|
869
|
+
const relationCount = (this.db.prepare('SELECT COUNT(*) as count FROM knowledge_graph').get() as { count: number }).count;
|
|
870
|
+
const commitCount = (this.db.prepare('SELECT COUNT(*) as count FROM git_commits').get() as { count: number }).count;
|
|
871
|
+
const toolConfigCount = (this.db.prepare('SELECT COUNT(*) as count FROM ai_tool_configs').get() as { count: number }).count;
|
|
872
|
+
|
|
873
|
+
let embeddingCount = 0;
|
|
874
|
+
try {
|
|
875
|
+
embeddingCount = (this.db.prepare('SELECT COUNT(*) as count FROM embeddings').get() as { count: number }).count;
|
|
876
|
+
} catch {
|
|
877
|
+
// Vector table might not exist yet
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
return {
|
|
881
|
+
items: itemCount,
|
|
882
|
+
relations: relationCount,
|
|
883
|
+
commits: commitCount,
|
|
884
|
+
embeddings: embeddingCount,
|
|
885
|
+
toolConfigs: toolConfigCount
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Get raw database instance (for advanced operations)
|
|
891
|
+
*/
|
|
892
|
+
getRawDb(): Database.Database {
|
|
893
|
+
return this.db;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Vacuum database to reclaim space
|
|
898
|
+
*/
|
|
899
|
+
vacuum(): void {
|
|
900
|
+
this.db.exec('VACUUM');
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Reindex database for optimization
|
|
905
|
+
*/
|
|
906
|
+
reindex(): void {
|
|
907
|
+
this.db.exec('REINDEX');
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Backup database to specified path
|
|
912
|
+
*/
|
|
913
|
+
backup(backupPath: string): void {
|
|
914
|
+
try {
|
|
915
|
+
// Ensure backup directory exists
|
|
916
|
+
const backupDir = path.dirname(backupPath);
|
|
917
|
+
if (!fs.existsSync(backupDir)) {
|
|
918
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Close the database before copying to ensure consistency
|
|
922
|
+
this.db.close();
|
|
923
|
+
fs.copyFileSync(this.dbPath, backupPath);
|
|
924
|
+
// Reopen the database
|
|
925
|
+
this.db = new Database(this.dbPath);
|
|
926
|
+
this.db.pragma('foreign_keys = ON');
|
|
927
|
+
sqliteVec.load(this.db);
|
|
928
|
+
|
|
929
|
+
console.log(`Database backed up to: ${backupPath}`);
|
|
930
|
+
} catch (error) {
|
|
931
|
+
// Try to reopen database if copy failed
|
|
932
|
+
try {
|
|
933
|
+
this.db = new Database(this.dbPath);
|
|
934
|
+
this.db.pragma('foreign_keys = ON');
|
|
935
|
+
sqliteVec.load(this.db);
|
|
936
|
+
} catch {
|
|
937
|
+
// Ignore reopen errors
|
|
938
|
+
}
|
|
939
|
+
throw new Error(`Failed to backup database: ${error instanceof Error ? error.message : error}`);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* Close database connection
|
|
945
|
+
*/
|
|
946
|
+
close(): void {
|
|
947
|
+
this.db.close();
|
|
948
|
+
}
|
|
949
|
+
}
|