@devflow-tools/database 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.
@@ -0,0 +1,147 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { DevFlowDatabase } from '../src/database';
3
+ import { rmSync, existsSync } from 'fs';
4
+ import { join } from 'path';
5
+
6
+ describe('DevFlowDatabase', () => {
7
+ const testDir = join(process.cwd(), '.devflow-test');
8
+ let db: DevFlowDatabase;
9
+
10
+ beforeEach(() => {
11
+ // 清理测试目录
12
+ if (existsSync(testDir)) {
13
+ rmSync(testDir, { recursive: true });
14
+ }
15
+ db = new DevFlowDatabase(testDir);
16
+ });
17
+
18
+ afterEach(() => {
19
+ db.close();
20
+ if (existsSync(testDir)) {
21
+ rmSync(testDir, { recursive: true });
22
+ }
23
+ });
24
+
25
+ it('should create .devflow directory and database file', () => {
26
+ expect(existsSync(join(testDir, '.devflow', 'devflow.db'))).toBe(true);
27
+ });
28
+
29
+ it('should initialize schema with runs table', () => {
30
+ const runs = db.getRuns();
31
+ expect(runs).toBeDefined();
32
+ expect(Array.isArray(runs)).toBe(true);
33
+ });
34
+
35
+ it('should insert and retrieve a run', () => {
36
+ const run = {
37
+ id: 'run-123',
38
+ source: 'mcp',
39
+ tool: 'test_tool',
40
+ input: { query: 'test' },
41
+ status: 'completed',
42
+ startedAt: Date.now(),
43
+ finishedAt: Date.now(),
44
+ tokenUsed: 100,
45
+ metadata: { projectRoot: testDir }
46
+ };
47
+
48
+ db.insertRun(run);
49
+ const retrieved = db.getRun('run-123');
50
+
51
+ expect(retrieved).toBeDefined();
52
+ expect(retrieved?.id).toBe('run-123');
53
+ expect(retrieved?.tool).toBe('test_tool');
54
+ });
55
+
56
+ it('should update a run', () => {
57
+ const run = {
58
+ id: 'run-456',
59
+ source: 'cli',
60
+ tool: 'test',
61
+ status: 'active',
62
+ startedAt: Date.now(),
63
+ tokenUsed: 0
64
+ };
65
+
66
+ db.insertRun(run);
67
+ db.updateRun('run-456', { status: 'completed', finishedAt: Date.now(), tokenUsed: 50 });
68
+
69
+ const retrieved = db.getRun('run-456');
70
+ expect(retrieved?.status).toBe('completed');
71
+ expect(retrieved?.token_used).toBe(50);
72
+ });
73
+
74
+ it('should insert and retrieve run steps', () => {
75
+ const run = {
76
+ id: 'run-789',
77
+ source: 'test',
78
+ tool: 'test',
79
+ status: 'active',
80
+ startedAt: Date.now(),
81
+ tokenUsed: 0
82
+ };
83
+ db.insertRun(run);
84
+
85
+ const step = {
86
+ runId: 'run-789',
87
+ name: 'step-1',
88
+ startTime: Date.now(),
89
+ endTime: Date.now(),
90
+ duration: 100,
91
+ status: 'completed',
92
+ metadata: { tokensUsed: 50 }
93
+ };
94
+
95
+ db.insertRunStep(step);
96
+ const steps = db.getRunSteps('run-789');
97
+
98
+ expect(steps).toHaveLength(1);
99
+ expect(steps[0].name).toBe('step-1');
100
+ });
101
+
102
+ it('should insert and retrieve telemetry events', () => {
103
+ // Insert a parent run first (FK constraint)
104
+ db.insertRun({
105
+ id: 'run-123',
106
+ source: 'test',
107
+ tool: 'test',
108
+ status: 'active',
109
+ startedAt: Date.now(),
110
+ tokenUsed: 0
111
+ });
112
+
113
+ const event = {
114
+ runId: 'run-123',
115
+ kind: 'tool_use',
116
+ timestamp: Date.now(),
117
+ duration: 100,
118
+ success: 1,
119
+ toolName: 'test',
120
+ pluginName: 'react',
121
+ metadata: { test: true }
122
+ };
123
+
124
+ db.insertEvent(event);
125
+ const events = db.getEvents('run-123');
126
+
127
+ expect(events).toHaveLength(1);
128
+ expect(events[0].kind).toBe('tool_use');
129
+ });
130
+
131
+ it('should upsert project stats', () => {
132
+ const stats = {
133
+ projectRoot: testDir,
134
+ totalRuns: 10,
135
+ successfulRuns: 8,
136
+ totalTokens: 1000,
137
+ lastActiveAt: Date.now(),
138
+ topTools: [{ toolName: 'test', count: 5 }]
139
+ };
140
+
141
+ db.upsertProjectStats(stats);
142
+ const retrieved = db.getProjectStats(testDir);
143
+
144
+ expect(retrieved).toBeDefined();
145
+ expect(retrieved?.totalRuns).toBe(10);
146
+ });
147
+ });
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { NodeSqliteDatabase } from '../src/node-sqlite';
3
+ import { unlinkSync, existsSync } from 'fs';
4
+
5
+ describe('NodeSqliteDatabase', () => {
6
+ let db: NodeSqliteDatabase;
7
+ const testDbPath = '/tmp/test-node-sqlite.db';
8
+
9
+ beforeEach(() => {
10
+ if (existsSync(testDbPath)) {
11
+ unlinkSync(testDbPath);
12
+ }
13
+ db = new NodeSqliteDatabase(testDbPath);
14
+ });
15
+
16
+ afterEach(() => {
17
+ db.close();
18
+ if (existsSync(testDbPath)) {
19
+ unlinkSync(testDbPath);
20
+ }
21
+ });
22
+
23
+ it('should create database and execute SQL', () => {
24
+ db.exec(`
25
+ CREATE TABLE test (
26
+ id INTEGER PRIMARY KEY,
27
+ name TEXT
28
+ )
29
+ `);
30
+
31
+ const stmt = db.prepare('INSERT INTO test (name) VALUES (?)');
32
+ const result = stmt.run('test');
33
+
34
+ expect(result.changes).toBe(1);
35
+ });
36
+
37
+ it('should query data', () => {
38
+ db.exec('CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)');
39
+ db.prepare('INSERT INTO test (name) VALUES (?)').run('Alice');
40
+ db.prepare('INSERT INTO test (name) VALUES (?)').run('Bob');
41
+
42
+ const row = db.prepare('SELECT * FROM test WHERE name = ?').get('Alice');
43
+ expect(row.name).toBe('Alice');
44
+
45
+ const rows = db.prepare('SELECT * FROM test').all();
46
+ expect(rows).toHaveLength(2);
47
+ });
48
+
49
+ it('should support transactions', () => {
50
+ db.exec('CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)');
51
+
52
+ db.transaction(() => {
53
+ db.prepare('INSERT INTO test (name) VALUES (?)').run('Alice');
54
+ db.prepare('INSERT INTO test (name) VALUES (?)').run('Bob');
55
+ });
56
+
57
+ const rows = db.prepare('SELECT * FROM test').all();
58
+ expect(rows).toHaveLength(2);
59
+ });
60
+ });
@@ -0,0 +1,16 @@
1
+ export declare class DevFlowDatabase {
2
+ private db;
3
+ constructor(projectRoot: string);
4
+ private initializeSchema;
5
+ insertRun(run: any): void;
6
+ getRun(id: string): any | null;
7
+ updateRun(id: string, updates: any): void;
8
+ insertRunStep(step: any): void;
9
+ getRunSteps(runId: string): any[];
10
+ insertEvent(event: any): void;
11
+ getEvents(runId: string): any[];
12
+ getProjectStats(projectRoot: string): any | null;
13
+ upsertProjectStats(stats: any): void;
14
+ getRuns(): any[];
15
+ close(): void;
16
+ }
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DevFlowDatabase = void 0;
4
+ const node_sqlite_1 = require("./node-sqlite");
5
+ const path_1 = require("path");
6
+ const fs_1 = require("fs");
7
+ class DevFlowDatabase {
8
+ constructor(projectRoot) {
9
+ const devflowDir = (0, path_1.join)(projectRoot, '.devflow');
10
+ if (!(0, fs_1.existsSync)(devflowDir)) {
11
+ (0, fs_1.mkdirSync)(devflowDir, { recursive: true });
12
+ }
13
+ const dbPath = (0, path_1.join)(devflowDir, 'devflow.db');
14
+ this.db = new node_sqlite_1.NodeSqliteDatabase(dbPath);
15
+ // 启用 WAL 模式
16
+ this.db.exec('PRAGMA journal_mode = WAL');
17
+ this.db.exec('PRAGMA busy_timeout = 5000');
18
+ this.initializeSchema();
19
+ }
20
+ initializeSchema() {
21
+ this.db.exec(`
22
+ CREATE TABLE IF NOT EXISTS runs (
23
+ id TEXT PRIMARY KEY,
24
+ source TEXT NOT NULL,
25
+ tool TEXT NOT NULL,
26
+ input TEXT,
27
+ status TEXT NOT NULL,
28
+ started_at INTEGER NOT NULL,
29
+ finished_at INTEGER,
30
+ token_used INTEGER DEFAULT 0,
31
+ metadata TEXT,
32
+ created_at INTEGER NOT NULL
33
+ );
34
+
35
+ CREATE TABLE IF NOT EXISTS run_steps (
36
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
37
+ run_id TEXT NOT NULL,
38
+ name TEXT NOT NULL,
39
+ start_time INTEGER NOT NULL,
40
+ end_time INTEGER,
41
+ duration INTEGER,
42
+ status TEXT,
43
+ metadata TEXT,
44
+ FOREIGN KEY (run_id) REFERENCES runs(id)
45
+ );
46
+
47
+ CREATE TABLE IF NOT EXISTS telemetry_events (
48
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
49
+ run_id TEXT,
50
+ kind TEXT NOT NULL,
51
+ timestamp INTEGER NOT NULL,
52
+ duration INTEGER,
53
+ success INTEGER,
54
+ tool_name TEXT,
55
+ plugin_name TEXT,
56
+ metadata TEXT,
57
+ FOREIGN KEY (run_id) REFERENCES runs(id)
58
+ );
59
+
60
+ CREATE TABLE IF NOT EXISTS project_stats (
61
+ project_root TEXT PRIMARY KEY,
62
+ total_runs INTEGER DEFAULT 0,
63
+ successful_runs INTEGER DEFAULT 0,
64
+ total_tokens INTEGER DEFAULT 0,
65
+ last_active_at INTEGER,
66
+ top_tools TEXT,
67
+ updated_at INTEGER NOT NULL
68
+ );
69
+
70
+ CREATE INDEX IF NOT EXISTS idx_runs_started_at ON runs(started_at);
71
+ CREATE INDEX IF NOT EXISTS idx_runs_status ON runs(status);
72
+ CREATE INDEX IF NOT EXISTS idx_run_steps_run_id ON run_steps(run_id);
73
+ CREATE INDEX IF NOT EXISTS idx_telemetry_events_run_id ON telemetry_events(run_id);
74
+ `);
75
+ }
76
+ insertRun(run) {
77
+ this.db.prepare(`
78
+ INSERT INTO runs (id, source, tool, input, status, started_at, finished_at, token_used, metadata, created_at)
79
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
80
+ `).run(run.id, run.source, run.tool, JSON.stringify(run.input) ?? null, run.status, run.startedAt, run.finishedAt ?? null, run.tokenUsed ?? 0, JSON.stringify(run.metadata) ?? null, Date.now());
81
+ }
82
+ getRun(id) {
83
+ const row = this.db.prepare('SELECT * FROM runs WHERE id = ?').get(id);
84
+ if (!row)
85
+ return null;
86
+ return {
87
+ ...row,
88
+ input: row.input ? JSON.parse(row.input) : null,
89
+ metadata: row.metadata ? JSON.parse(row.metadata) : null
90
+ };
91
+ }
92
+ updateRun(id, updates) {
93
+ const sets = [];
94
+ const values = [];
95
+ if (updates.status !== undefined) {
96
+ sets.push('status = ?');
97
+ values.push(updates.status);
98
+ }
99
+ if (updates.finishedAt !== undefined) {
100
+ sets.push('finished_at = ?');
101
+ values.push(updates.finishedAt);
102
+ }
103
+ if (updates.tokenUsed !== undefined) {
104
+ sets.push('token_used = ?');
105
+ values.push(updates.tokenUsed);
106
+ }
107
+ if (sets.length > 0) {
108
+ values.push(id);
109
+ this.db.prepare(`UPDATE runs SET ${sets.join(', ')} WHERE id = ?`).run(...values);
110
+ }
111
+ }
112
+ insertRunStep(step) {
113
+ this.db.prepare(`
114
+ INSERT INTO run_steps (run_id, name, start_time, end_time, duration, status, metadata)
115
+ VALUES (?, ?, ?, ?, ?, ?, ?)
116
+ `).run(step.runId, step.name, step.startTime, step.endTime, step.duration, step.status, JSON.stringify(step.metadata) ?? null);
117
+ }
118
+ getRunSteps(runId) {
119
+ const rows = this.db.prepare('SELECT * FROM run_steps WHERE run_id = ?').all(runId);
120
+ return rows.map(row => ({
121
+ ...row,
122
+ metadata: row.metadata ? JSON.parse(row.metadata) : null
123
+ }));
124
+ }
125
+ insertEvent(event) {
126
+ this.db.prepare(`
127
+ INSERT INTO telemetry_events (run_id, kind, timestamp, duration, success, tool_name, plugin_name, metadata)
128
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
129
+ `).run(event.runId, event.kind, event.timestamp, event.duration, event.success, event.toolName, event.pluginName, JSON.stringify(event.metadata) ?? null);
130
+ }
131
+ getEvents(runId) {
132
+ const rows = this.db.prepare('SELECT * FROM telemetry_events WHERE run_id = ?').all(runId);
133
+ return rows.map(row => ({
134
+ ...row,
135
+ metadata: row.metadata ? JSON.parse(row.metadata) : null
136
+ }));
137
+ }
138
+ getProjectStats(projectRoot) {
139
+ const row = this.db.prepare('SELECT * FROM project_stats WHERE project_root = ?').get(projectRoot);
140
+ if (!row)
141
+ return null;
142
+ return {
143
+ projectRoot: row.project_root,
144
+ totalRuns: row.total_runs,
145
+ successfulRuns: row.successful_runs,
146
+ totalTokens: row.total_tokens,
147
+ lastActiveAt: row.last_active_at,
148
+ topTools: row.top_tools ? JSON.parse(row.top_tools) : null,
149
+ updatedAt: row.updated_at
150
+ };
151
+ }
152
+ upsertProjectStats(stats) {
153
+ this.db.prepare(`
154
+ INSERT INTO project_stats (project_root, total_runs, successful_runs, total_tokens, last_active_at, top_tools, updated_at)
155
+ VALUES (?, ?, ?, ?, ?, ?, ?)
156
+ ON CONFLICT(project_root) DO UPDATE SET
157
+ total_runs = excluded.total_runs,
158
+ successful_runs = excluded.successful_runs,
159
+ total_tokens = excluded.total_tokens,
160
+ last_active_at = excluded.last_active_at,
161
+ top_tools = excluded.top_tools,
162
+ updated_at = excluded.updated_at
163
+ `).run(stats.projectRoot, stats.totalRuns, stats.successfulRuns, stats.totalTokens, stats.lastActiveAt, JSON.stringify(stats.topTools) ?? null, Date.now());
164
+ }
165
+ getRuns() {
166
+ return this.db.prepare('SELECT * FROM runs').all();
167
+ }
168
+ close() {
169
+ this.db.close();
170
+ }
171
+ }
172
+ exports.DevFlowDatabase = DevFlowDatabase;
@@ -0,0 +1 @@
1
+ export { DevFlowDatabase } from './database';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DevFlowDatabase = void 0;
4
+ var database_1 = require("./database");
5
+ Object.defineProperty(exports, "DevFlowDatabase", { enumerable: true, get: function () { return database_1.DevFlowDatabase; } });
@@ -0,0 +1,9 @@
1
+ import type { Database, Statement } from './types';
2
+ export declare class NodeSqliteDatabase implements Database {
3
+ private db;
4
+ constructor(path: string);
5
+ exec(sql: string): void;
6
+ prepare(sql: string): Statement;
7
+ transaction<T>(fn: () => T): T;
8
+ close(): void;
9
+ }
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NodeSqliteDatabase = void 0;
4
+ // node:sqlite is a Node.js 22+ experimental built-in. TypeScript does not
5
+ // resolve `node:` prefixed modules unless @types/node includes them (which
6
+ // requires @types/node >= 22.5). We suppress the TS error here and rely on
7
+ // the ambient types defined below until the project upgrades @types/node.
8
+ // @ts-expect-error - node:sqlite types not available in @types/node@20
9
+ const node_sqlite_1 = require("node:sqlite");
10
+ class NodeSqliteDatabase {
11
+ constructor(path) {
12
+ this.db = new node_sqlite_1.DatabaseSync(path);
13
+ }
14
+ exec(sql) {
15
+ this.db.exec(sql);
16
+ }
17
+ prepare(sql) {
18
+ const stmt = this.db.prepare(sql);
19
+ return {
20
+ run: (...params) => {
21
+ const result = stmt.run(...params);
22
+ return {
23
+ changes: result.changes,
24
+ lastInsertRowid: result.lastInsertRowid,
25
+ };
26
+ },
27
+ get: (...params) => {
28
+ return stmt.get(...params);
29
+ },
30
+ all: (...params) => {
31
+ return stmt.all(...params);
32
+ },
33
+ };
34
+ }
35
+ transaction(fn) {
36
+ this.db.exec('BEGIN');
37
+ try {
38
+ const result = fn();
39
+ this.db.exec('COMMIT');
40
+ return result;
41
+ }
42
+ catch (err) {
43
+ this.db.exec('ROLLBACK');
44
+ throw err;
45
+ }
46
+ }
47
+ close() {
48
+ this.db.close();
49
+ }
50
+ }
51
+ exports.NodeSqliteDatabase = NodeSqliteDatabase;
@@ -0,0 +1,15 @@
1
+ export interface Database {
2
+ exec(sql: string): void;
3
+ prepare(sql: string): Statement;
4
+ transaction<T>(fn: () => T): T;
5
+ close(): void;
6
+ }
7
+ export interface Statement {
8
+ run(...params: any[]): RunResult;
9
+ get(...params: any[]): any;
10
+ all(...params: any[]): any[];
11
+ }
12
+ export interface RunResult {
13
+ changes: number;
14
+ lastInsertRowid: number | bigint;
15
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@devflow-tools/database",
3
+ "version": "0.1.0",
4
+ "description": "DevFlow SQLite database package",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "test": "vitest run"
10
+ },
11
+ "dependencies": {},
12
+ "devDependencies": {
13
+ "@types/node": "^20.0.0",
14
+ "typescript": "^5.5.0",
15
+ "vitest": "^2.0.0"
16
+ }
17
+ }
@@ -0,0 +1,224 @@
1
+ import { NodeSqliteDatabase } from './node-sqlite';
2
+ import { join } from 'path';
3
+ import { existsSync, mkdirSync } from 'fs';
4
+
5
+ export class DevFlowDatabase {
6
+ private db: NodeSqliteDatabase;
7
+
8
+ constructor(projectRoot: string) {
9
+ const devflowDir = join(projectRoot, '.devflow');
10
+ if (!existsSync(devflowDir)) {
11
+ mkdirSync(devflowDir, { recursive: true });
12
+ }
13
+
14
+ const dbPath = join(devflowDir, 'devflow.db');
15
+ this.db = new NodeSqliteDatabase(dbPath);
16
+
17
+ // 启用 WAL 模式
18
+ this.db.exec('PRAGMA journal_mode = WAL');
19
+ this.db.exec('PRAGMA busy_timeout = 5000');
20
+
21
+ this.initializeSchema();
22
+ }
23
+
24
+ private initializeSchema() {
25
+ this.db.exec(`
26
+ CREATE TABLE IF NOT EXISTS runs (
27
+ id TEXT PRIMARY KEY,
28
+ source TEXT NOT NULL,
29
+ tool TEXT NOT NULL,
30
+ input TEXT,
31
+ status TEXT NOT NULL,
32
+ started_at INTEGER NOT NULL,
33
+ finished_at INTEGER,
34
+ token_used INTEGER DEFAULT 0,
35
+ metadata TEXT,
36
+ created_at INTEGER NOT NULL
37
+ );
38
+
39
+ CREATE TABLE IF NOT EXISTS run_steps (
40
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
41
+ run_id TEXT NOT NULL,
42
+ name TEXT NOT NULL,
43
+ start_time INTEGER NOT NULL,
44
+ end_time INTEGER,
45
+ duration INTEGER,
46
+ status TEXT,
47
+ metadata TEXT,
48
+ FOREIGN KEY (run_id) REFERENCES runs(id)
49
+ );
50
+
51
+ CREATE TABLE IF NOT EXISTS telemetry_events (
52
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
53
+ run_id TEXT,
54
+ kind TEXT NOT NULL,
55
+ timestamp INTEGER NOT NULL,
56
+ duration INTEGER,
57
+ success INTEGER,
58
+ tool_name TEXT,
59
+ plugin_name TEXT,
60
+ metadata TEXT,
61
+ FOREIGN KEY (run_id) REFERENCES runs(id)
62
+ );
63
+
64
+ CREATE TABLE IF NOT EXISTS project_stats (
65
+ project_root TEXT PRIMARY KEY,
66
+ total_runs INTEGER DEFAULT 0,
67
+ successful_runs INTEGER DEFAULT 0,
68
+ total_tokens INTEGER DEFAULT 0,
69
+ last_active_at INTEGER,
70
+ top_tools TEXT,
71
+ updated_at INTEGER NOT NULL
72
+ );
73
+
74
+ CREATE INDEX IF NOT EXISTS idx_runs_started_at ON runs(started_at);
75
+ CREATE INDEX IF NOT EXISTS idx_runs_status ON runs(status);
76
+ CREATE INDEX IF NOT EXISTS idx_run_steps_run_id ON run_steps(run_id);
77
+ CREATE INDEX IF NOT EXISTS idx_telemetry_events_run_id ON telemetry_events(run_id);
78
+ `);
79
+ }
80
+
81
+ insertRun(run: any): void {
82
+ this.db.prepare(`
83
+ INSERT INTO runs (id, source, tool, input, status, started_at, finished_at, token_used, metadata, created_at)
84
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
85
+ `).run(
86
+ run.id,
87
+ run.source,
88
+ run.tool,
89
+ JSON.stringify(run.input) ?? null,
90
+ run.status,
91
+ run.startedAt,
92
+ run.finishedAt ?? null,
93
+ run.tokenUsed ?? 0,
94
+ JSON.stringify(run.metadata) ?? null,
95
+ Date.now()
96
+ );
97
+ }
98
+
99
+ getRun(id: string): any | null {
100
+ const row = this.db.prepare('SELECT * FROM runs WHERE id = ?').get(id) as any;
101
+ if (!row) return null;
102
+
103
+ return {
104
+ ...row,
105
+ input: row.input ? JSON.parse(row.input) : null,
106
+ metadata: row.metadata ? JSON.parse(row.metadata) : null
107
+ };
108
+ }
109
+
110
+ updateRun(id: string, updates: any): void {
111
+ const sets: string[] = [];
112
+ const values: any[] = [];
113
+
114
+ if (updates.status !== undefined) {
115
+ sets.push('status = ?');
116
+ values.push(updates.status);
117
+ }
118
+ if (updates.finishedAt !== undefined) {
119
+ sets.push('finished_at = ?');
120
+ values.push(updates.finishedAt);
121
+ }
122
+ if (updates.tokenUsed !== undefined) {
123
+ sets.push('token_used = ?');
124
+ values.push(updates.tokenUsed);
125
+ }
126
+
127
+ if (sets.length > 0) {
128
+ values.push(id);
129
+ this.db.prepare(`UPDATE runs SET ${sets.join(', ')} WHERE id = ?`).run(...values);
130
+ }
131
+ }
132
+
133
+ insertRunStep(step: any): void {
134
+ this.db.prepare(`
135
+ INSERT INTO run_steps (run_id, name, start_time, end_time, duration, status, metadata)
136
+ VALUES (?, ?, ?, ?, ?, ?, ?)
137
+ `).run(
138
+ step.runId,
139
+ step.name,
140
+ step.startTime,
141
+ step.endTime,
142
+ step.duration,
143
+ step.status,
144
+ JSON.stringify(step.metadata) ?? null
145
+ );
146
+ }
147
+
148
+ getRunSteps(runId: string): any[] {
149
+ const rows = this.db.prepare('SELECT * FROM run_steps WHERE run_id = ?').all(runId) as any[];
150
+ return rows.map(row => ({
151
+ ...row,
152
+ metadata: row.metadata ? JSON.parse(row.metadata) : null
153
+ }));
154
+ }
155
+
156
+ insertEvent(event: any): void {
157
+ this.db.prepare(`
158
+ INSERT INTO telemetry_events (run_id, kind, timestamp, duration, success, tool_name, plugin_name, metadata)
159
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
160
+ `).run(
161
+ event.runId,
162
+ event.kind,
163
+ event.timestamp,
164
+ event.duration,
165
+ event.success,
166
+ event.toolName,
167
+ event.pluginName,
168
+ JSON.stringify(event.metadata) ?? null
169
+ );
170
+ }
171
+
172
+ getEvents(runId: string): any[] {
173
+ const rows = this.db.prepare('SELECT * FROM telemetry_events WHERE run_id = ?').all(runId) as any[];
174
+ return rows.map(row => ({
175
+ ...row,
176
+ metadata: row.metadata ? JSON.parse(row.metadata) : null
177
+ }));
178
+ }
179
+
180
+ getProjectStats(projectRoot: string): any | null {
181
+ const row = this.db.prepare('SELECT * FROM project_stats WHERE project_root = ?').get(projectRoot) as any;
182
+ if (!row) return null;
183
+
184
+ return {
185
+ projectRoot: row.project_root,
186
+ totalRuns: row.total_runs,
187
+ successfulRuns: row.successful_runs,
188
+ totalTokens: row.total_tokens,
189
+ lastActiveAt: row.last_active_at,
190
+ topTools: row.top_tools ? JSON.parse(row.top_tools) : null,
191
+ updatedAt: row.updated_at
192
+ };
193
+ }
194
+
195
+ upsertProjectStats(stats: any): void {
196
+ this.db.prepare(`
197
+ INSERT INTO project_stats (project_root, total_runs, successful_runs, total_tokens, last_active_at, top_tools, updated_at)
198
+ VALUES (?, ?, ?, ?, ?, ?, ?)
199
+ ON CONFLICT(project_root) DO UPDATE SET
200
+ total_runs = excluded.total_runs,
201
+ successful_runs = excluded.successful_runs,
202
+ total_tokens = excluded.total_tokens,
203
+ last_active_at = excluded.last_active_at,
204
+ top_tools = excluded.top_tools,
205
+ updated_at = excluded.updated_at
206
+ `).run(
207
+ stats.projectRoot,
208
+ stats.totalRuns,
209
+ stats.successfulRuns,
210
+ stats.totalTokens,
211
+ stats.lastActiveAt,
212
+ JSON.stringify(stats.topTools) ?? null,
213
+ Date.now()
214
+ );
215
+ }
216
+
217
+ getRuns(): any[] {
218
+ return this.db.prepare('SELECT * FROM runs').all();
219
+ }
220
+
221
+ close(): void {
222
+ this.db.close();
223
+ }
224
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { DevFlowDatabase } from './database';
@@ -0,0 +1,54 @@
1
+ // node:sqlite is a Node.js 22+ experimental built-in. TypeScript does not
2
+ // resolve `node:` prefixed modules unless @types/node includes them (which
3
+ // requires @types/node >= 22.5). We suppress the TS error here and rely on
4
+ // the ambient types defined below until the project upgrades @types/node.
5
+ // @ts-expect-error - node:sqlite types not available in @types/node@20
6
+ import { DatabaseSync } from 'node:sqlite';
7
+ import type { Database, Statement, RunResult } from './types';
8
+
9
+ export class NodeSqliteDatabase implements Database {
10
+ private db: DatabaseSync;
11
+
12
+ constructor(path: string) {
13
+ this.db = new DatabaseSync(path);
14
+ }
15
+
16
+ exec(sql: string): void {
17
+ this.db.exec(sql);
18
+ }
19
+
20
+ prepare(sql: string): Statement {
21
+ const stmt = this.db.prepare(sql);
22
+ return {
23
+ run: (...params: any[]): RunResult => {
24
+ const result = stmt.run(...params);
25
+ return {
26
+ changes: result.changes,
27
+ lastInsertRowid: result.lastInsertRowid,
28
+ };
29
+ },
30
+ get: (...params: any[]): any => {
31
+ return stmt.get(...params);
32
+ },
33
+ all: (...params: any[]): any[] => {
34
+ return stmt.all(...params);
35
+ },
36
+ };
37
+ }
38
+
39
+ transaction<T>(fn: () => T): T {
40
+ this.db.exec('BEGIN');
41
+ try {
42
+ const result = fn();
43
+ this.db.exec('COMMIT');
44
+ return result;
45
+ } catch (err) {
46
+ this.db.exec('ROLLBACK');
47
+ throw err;
48
+ }
49
+ }
50
+
51
+ close(): void {
52
+ this.db.close();
53
+ }
54
+ }
package/src/types.ts ADDED
@@ -0,0 +1,17 @@
1
+ export interface Database {
2
+ exec(sql: string): void;
3
+ prepare(sql: string): Statement;
4
+ transaction<T>(fn: () => T): T;
5
+ close(): void;
6
+ }
7
+
8
+ export interface Statement {
9
+ run(...params: any[]): RunResult;
10
+ get(...params: any[]): any;
11
+ all(...params: any[]): any[];
12
+ }
13
+
14
+ export interface RunResult {
15
+ changes: number;
16
+ lastInsertRowid: number | bigint;
17
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "declaration": true,
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "composite": true,
14
+ "moduleResolution": "node",
15
+ "resolveJsonModule": true
16
+ },
17
+ "include": ["src/**/*"],
18
+ "exclude": ["node_modules", "dist", "__tests__"]
19
+ }
@@ -0,0 +1,41 @@
1
+ import { defineConfig, type Plugin } from 'vitest/config';
2
+
3
+ // Vite 5 doesn't recognize node:sqlite and strips the `node:` prefix,
4
+ // trying to resolve `sqlite` as a regular module. This plugin prevents that
5
+ // by providing the module directly as a re-export.
6
+ const nodeSqlitePlugin: Plugin = {
7
+ name: 'node-sqlite-resolver',
8
+ enforce: 'pre',
9
+ resolveId(source) {
10
+ if (source === 'node:sqlite') {
11
+ // Return a virtual module id that won't be further processed
12
+ return '\0node:sqlite';
13
+ }
14
+ return null;
15
+ },
16
+ load(id) {
17
+ if (id === '\0node:sqlite') {
18
+ // Use createRequire to load node:sqlite without triggering vite resolution
19
+ return `
20
+ import { createRequire } from 'module';
21
+ const require = createRequire(import.meta.url);
22
+ const mod = require('node:sqlite');
23
+ export const DatabaseSync = mod.DatabaseSync;
24
+ `;
25
+ }
26
+ return null;
27
+ },
28
+ };
29
+
30
+ export default defineConfig({
31
+ plugins: [nodeSqlitePlugin],
32
+ test: {
33
+ globals: false,
34
+ pool: 'forks',
35
+ poolOptions: {
36
+ forks: {
37
+ execArgv: ['--experimental-sqlite'],
38
+ },
39
+ },
40
+ },
41
+ });