lynkr 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/.eslintrc.cjs +12 -0
- package/CLAUDE.md +39 -0
- package/LICENSE +21 -0
- package/README.md +417 -0
- package/bin/cli.js +3 -0
- package/index.js +3 -0
- package/package.json +54 -0
- package/src/api/middleware/logging.js +37 -0
- package/src/api/middleware/session.js +55 -0
- package/src/api/router.js +80 -0
- package/src/cache/prompt.js +183 -0
- package/src/clients/databricks.js +72 -0
- package/src/config/index.js +301 -0
- package/src/db/index.js +192 -0
- package/src/diff/comments.js +153 -0
- package/src/edits/index.js +171 -0
- package/src/indexer/index.js +1610 -0
- package/src/indexer/navigation/index.js +32 -0
- package/src/indexer/navigation/providers/treeSitter.js +36 -0
- package/src/indexer/parser.js +324 -0
- package/src/logger/index.js +27 -0
- package/src/mcp/client.js +194 -0
- package/src/mcp/index.js +34 -0
- package/src/mcp/permissions.js +69 -0
- package/src/mcp/registry.js +225 -0
- package/src/mcp/sandbox.js +238 -0
- package/src/metrics/index.js +38 -0
- package/src/orchestrator/index.js +1492 -0
- package/src/policy/index.js +212 -0
- package/src/policy/web-fallback.js +33 -0
- package/src/server.js +73 -0
- package/src/sessions/index.js +15 -0
- package/src/sessions/record.js +31 -0
- package/src/sessions/store.js +179 -0
- package/src/tasks/store.js +349 -0
- package/src/tests/coverage.js +173 -0
- package/src/tests/index.js +171 -0
- package/src/tests/store.js +213 -0
- package/src/tools/edits.js +94 -0
- package/src/tools/execution.js +169 -0
- package/src/tools/git.js +1346 -0
- package/src/tools/index.js +258 -0
- package/src/tools/indexer.js +360 -0
- package/src/tools/mcp-remote.js +81 -0
- package/src/tools/mcp.js +116 -0
- package/src/tools/process.js +151 -0
- package/src/tools/stubs.js +55 -0
- package/src/tools/tasks.js +260 -0
- package/src/tools/tests.js +132 -0
- package/src/tools/web.js +286 -0
- package/src/tools/workspace.js +173 -0
- package/src/workspace/index.js +95 -0
package/src/db/index.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const Database = require("better-sqlite3");
|
|
4
|
+
const config = require("../config");
|
|
5
|
+
const logger = require("../logger");
|
|
6
|
+
|
|
7
|
+
const dbPath = config.sessionStore.dbPath;
|
|
8
|
+
const directory = path.dirname(dbPath);
|
|
9
|
+
|
|
10
|
+
if (!fs.existsSync(directory)) {
|
|
11
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const db = new Database(dbPath);
|
|
15
|
+
|
|
16
|
+
db.pragma("journal_mode = WAL");
|
|
17
|
+
db.pragma("synchronous = NORMAL");
|
|
18
|
+
db.pragma("foreign_keys = ON");
|
|
19
|
+
|
|
20
|
+
db.exec(`
|
|
21
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
22
|
+
id TEXT PRIMARY KEY,
|
|
23
|
+
created_at INTEGER NOT NULL,
|
|
24
|
+
updated_at INTEGER NOT NULL,
|
|
25
|
+
metadata TEXT NOT NULL
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
CREATE TABLE IF NOT EXISTS session_history (
|
|
29
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
30
|
+
session_id TEXT NOT NULL,
|
|
31
|
+
role TEXT,
|
|
32
|
+
type TEXT,
|
|
33
|
+
status INTEGER,
|
|
34
|
+
content TEXT,
|
|
35
|
+
metadata TEXT,
|
|
36
|
+
timestamp INTEGER NOT NULL,
|
|
37
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
CREATE INDEX IF NOT EXISTS idx_session_history_session_id_timestamp
|
|
41
|
+
ON session_history(session_id, timestamp);
|
|
42
|
+
|
|
43
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
44
|
+
path TEXT PRIMARY KEY,
|
|
45
|
+
size_bytes INTEGER NOT NULL,
|
|
46
|
+
mtime_ms INTEGER NOT NULL,
|
|
47
|
+
language TEXT,
|
|
48
|
+
summary TEXT
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
CREATE TABLE IF NOT EXISTS symbols (
|
|
52
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
53
|
+
file_path TEXT NOT NULL,
|
|
54
|
+
name TEXT NOT NULL,
|
|
55
|
+
kind TEXT,
|
|
56
|
+
line INTEGER,
|
|
57
|
+
column INTEGER,
|
|
58
|
+
metadata TEXT,
|
|
59
|
+
FOREIGN KEY (file_path) REFERENCES files(path) ON DELETE CASCADE
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
CREATE TABLE IF NOT EXISTS framework_signals (
|
|
63
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
64
|
+
type TEXT NOT NULL,
|
|
65
|
+
file_path TEXT,
|
|
66
|
+
detail TEXT,
|
|
67
|
+
metadata TEXT
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
CREATE TABLE IF NOT EXISTS workspace_metadata (
|
|
71
|
+
key TEXT PRIMARY KEY,
|
|
72
|
+
value TEXT NOT NULL
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
CREATE TABLE IF NOT EXISTS edits (
|
|
76
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
77
|
+
session_id TEXT,
|
|
78
|
+
file_path TEXT NOT NULL,
|
|
79
|
+
created_at INTEGER NOT NULL,
|
|
80
|
+
source TEXT,
|
|
81
|
+
before_content TEXT,
|
|
82
|
+
after_content TEXT,
|
|
83
|
+
diff TEXT,
|
|
84
|
+
metadata TEXT
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
CREATE INDEX IF NOT EXISTS idx_edits_file_path_created
|
|
88
|
+
ON edits(file_path, created_at DESC);
|
|
89
|
+
|
|
90
|
+
CREATE INDEX IF NOT EXISTS idx_edits_session_created
|
|
91
|
+
ON edits(session_id, created_at DESC);
|
|
92
|
+
|
|
93
|
+
CREATE TABLE IF NOT EXISTS symbol_references (
|
|
94
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
95
|
+
symbol_id INTEGER NOT NULL,
|
|
96
|
+
file_path TEXT NOT NULL,
|
|
97
|
+
line INTEGER,
|
|
98
|
+
column INTEGER,
|
|
99
|
+
snippet TEXT,
|
|
100
|
+
metadata TEXT,
|
|
101
|
+
FOREIGN KEY (symbol_id) REFERENCES symbols(id) ON DELETE CASCADE
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
CREATE INDEX IF NOT EXISTS idx_symbol_references_symbol
|
|
105
|
+
ON symbol_references(symbol_id);
|
|
106
|
+
|
|
107
|
+
CREATE INDEX IF NOT EXISTS idx_symbol_references_file
|
|
108
|
+
ON symbol_references(file_path, line);
|
|
109
|
+
|
|
110
|
+
CREATE TABLE IF NOT EXISTS file_dependencies (
|
|
111
|
+
from_path TEXT NOT NULL,
|
|
112
|
+
to_path TEXT NOT NULL,
|
|
113
|
+
kind TEXT,
|
|
114
|
+
metadata TEXT,
|
|
115
|
+
FOREIGN KEY (from_path) REFERENCES files(path) ON DELETE CASCADE
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
CREATE INDEX IF NOT EXISTS idx_file_dependencies_from
|
|
119
|
+
ON file_dependencies(from_path);
|
|
120
|
+
|
|
121
|
+
CREATE INDEX IF NOT EXISTS idx_file_dependencies_to
|
|
122
|
+
ON file_dependencies(to_path);
|
|
123
|
+
|
|
124
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
125
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
126
|
+
title TEXT NOT NULL,
|
|
127
|
+
status TEXT NOT NULL,
|
|
128
|
+
priority INTEGER DEFAULT 0,
|
|
129
|
+
tags TEXT,
|
|
130
|
+
linked_file TEXT,
|
|
131
|
+
created_at INTEGER NOT NULL,
|
|
132
|
+
updated_at INTEGER NOT NULL,
|
|
133
|
+
created_by TEXT,
|
|
134
|
+
updated_by TEXT,
|
|
135
|
+
metadata TEXT
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_status
|
|
139
|
+
ON tasks(status, priority DESC, updated_at DESC);
|
|
140
|
+
|
|
141
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_linked_file
|
|
142
|
+
ON tasks(linked_file, status);
|
|
143
|
+
|
|
144
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_updated_at
|
|
145
|
+
ON tasks(updated_at DESC);
|
|
146
|
+
|
|
147
|
+
CREATE TABLE IF NOT EXISTS diff_comments (
|
|
148
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
149
|
+
thread_id TEXT,
|
|
150
|
+
session_id TEXT,
|
|
151
|
+
file_path TEXT NOT NULL,
|
|
152
|
+
hunk TEXT,
|
|
153
|
+
line INTEGER,
|
|
154
|
+
comment TEXT NOT NULL,
|
|
155
|
+
author TEXT,
|
|
156
|
+
created_at INTEGER NOT NULL,
|
|
157
|
+
metadata TEXT
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
CREATE INDEX IF NOT EXISTS idx_diff_comments_thread
|
|
161
|
+
ON diff_comments(thread_id);
|
|
162
|
+
|
|
163
|
+
CREATE INDEX IF NOT EXISTS idx_diff_comments_file
|
|
164
|
+
ON diff_comments(file_path, line);
|
|
165
|
+
|
|
166
|
+
CREATE TABLE IF NOT EXISTS test_runs (
|
|
167
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
168
|
+
profile TEXT,
|
|
169
|
+
status TEXT,
|
|
170
|
+
command TEXT NOT NULL,
|
|
171
|
+
args TEXT,
|
|
172
|
+
cwd TEXT,
|
|
173
|
+
exit_code INTEGER,
|
|
174
|
+
timed_out INTEGER DEFAULT 0,
|
|
175
|
+
duration_ms INTEGER,
|
|
176
|
+
sandbox TEXT,
|
|
177
|
+
stdout TEXT,
|
|
178
|
+
stderr TEXT,
|
|
179
|
+
coverage TEXT,
|
|
180
|
+
created_at INTEGER NOT NULL
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
CREATE INDEX IF NOT EXISTS idx_test_runs_created
|
|
184
|
+
ON test_runs(created_at DESC);
|
|
185
|
+
|
|
186
|
+
CREATE INDEX IF NOT EXISTS idx_test_runs_status
|
|
187
|
+
ON test_runs(status);
|
|
188
|
+
`);
|
|
189
|
+
|
|
190
|
+
logger.info({ dbPath }, "SQLite session store initialised");
|
|
191
|
+
|
|
192
|
+
module.exports = db;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
2
|
+
const db = require("../db");
|
|
3
|
+
const logger = require("../logger");
|
|
4
|
+
|
|
5
|
+
const insertCommentStmt = db.prepare(
|
|
6
|
+
`INSERT INTO diff_comments (
|
|
7
|
+
thread_id,
|
|
8
|
+
session_id,
|
|
9
|
+
file_path,
|
|
10
|
+
hunk,
|
|
11
|
+
line,
|
|
12
|
+
comment,
|
|
13
|
+
author,
|
|
14
|
+
created_at,
|
|
15
|
+
metadata
|
|
16
|
+
) VALUES (
|
|
17
|
+
@thread_id,
|
|
18
|
+
@session_id,
|
|
19
|
+
@file_path,
|
|
20
|
+
@hunk,
|
|
21
|
+
@line,
|
|
22
|
+
@comment,
|
|
23
|
+
@author,
|
|
24
|
+
@created_at,
|
|
25
|
+
@metadata
|
|
26
|
+
)`,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const deleteCommentStmt = db.prepare("DELETE FROM diff_comments WHERE id = ?");
|
|
30
|
+
|
|
31
|
+
function buildSelectCommentsQuery({ filePath, threadId }) {
|
|
32
|
+
let sql = `SELECT id,
|
|
33
|
+
thread_id,
|
|
34
|
+
session_id,
|
|
35
|
+
file_path,
|
|
36
|
+
hunk,
|
|
37
|
+
line,
|
|
38
|
+
comment,
|
|
39
|
+
author,
|
|
40
|
+
created_at,
|
|
41
|
+
metadata
|
|
42
|
+
FROM diff_comments`;
|
|
43
|
+
const conditions = [];
|
|
44
|
+
const params = [];
|
|
45
|
+
if (filePath) {
|
|
46
|
+
conditions.push("file_path = ?");
|
|
47
|
+
params.push(filePath);
|
|
48
|
+
}
|
|
49
|
+
if (threadId) {
|
|
50
|
+
conditions.push("thread_id = ?");
|
|
51
|
+
params.push(threadId);
|
|
52
|
+
}
|
|
53
|
+
if (conditions.length) {
|
|
54
|
+
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
55
|
+
}
|
|
56
|
+
sql += " ORDER BY created_at ASC, id ASC";
|
|
57
|
+
return { sql, params };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normaliseCommentRow(row) {
|
|
61
|
+
return {
|
|
62
|
+
id: row.id,
|
|
63
|
+
threadId: row.thread_id ?? null,
|
|
64
|
+
sessionId: row.session_id ?? null,
|
|
65
|
+
filePath: row.file_path,
|
|
66
|
+
hunk: row.hunk ?? null,
|
|
67
|
+
line: typeof row.line === "number" ? row.line : null,
|
|
68
|
+
comment: row.comment,
|
|
69
|
+
author: row.author ?? null,
|
|
70
|
+
createdAt: row.created_at,
|
|
71
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function addDiffComment({
|
|
76
|
+
threadId,
|
|
77
|
+
sessionId,
|
|
78
|
+
filePath,
|
|
79
|
+
line,
|
|
80
|
+
hunk,
|
|
81
|
+
comment,
|
|
82
|
+
author,
|
|
83
|
+
metadata,
|
|
84
|
+
}) {
|
|
85
|
+
if (typeof filePath !== "string" || filePath.trim().length === 0) {
|
|
86
|
+
throw new Error("Diff comment requires a file path.");
|
|
87
|
+
}
|
|
88
|
+
if (typeof comment !== "string" || comment.trim().length === 0) {
|
|
89
|
+
throw new Error("Diff comment requires non-empty comment text.");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const createdAt = Date.now();
|
|
93
|
+
const thread = threadId ?? crypto.randomUUID();
|
|
94
|
+
|
|
95
|
+
const params = {
|
|
96
|
+
thread_id: thread,
|
|
97
|
+
session_id: sessionId ?? null,
|
|
98
|
+
file_path: filePath,
|
|
99
|
+
hunk: hunk ?? null,
|
|
100
|
+
line: typeof line === "number" ? line : null,
|
|
101
|
+
comment,
|
|
102
|
+
author: author ?? null,
|
|
103
|
+
created_at: createdAt,
|
|
104
|
+
metadata: metadata ? JSON.stringify(metadata) : null,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const result = insertCommentStmt.run(params);
|
|
108
|
+
|
|
109
|
+
logger.debug(
|
|
110
|
+
{
|
|
111
|
+
id: result.lastInsertRowid,
|
|
112
|
+
thread,
|
|
113
|
+
filePath,
|
|
114
|
+
line,
|
|
115
|
+
},
|
|
116
|
+
"Recorded diff comment",
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
return normaliseCommentRow({
|
|
120
|
+
id: result.lastInsertRowid,
|
|
121
|
+
thread_id: thread,
|
|
122
|
+
session_id: sessionId ?? null,
|
|
123
|
+
file_path: filePath,
|
|
124
|
+
hunk: hunk ?? null,
|
|
125
|
+
line: typeof line === "number" ? line : null,
|
|
126
|
+
comment,
|
|
127
|
+
author: author ?? null,
|
|
128
|
+
created_at: createdAt,
|
|
129
|
+
metadata: metadata ? JSON.stringify(metadata) : null,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function listDiffComments({ filePath, threadId } = {}) {
|
|
134
|
+
const { sql, params } = buildSelectCommentsQuery({ filePath, threadId });
|
|
135
|
+
return db
|
|
136
|
+
.prepare(sql)
|
|
137
|
+
.all(...params)
|
|
138
|
+
.map(normaliseCommentRow);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function deleteDiffComment({ id }) {
|
|
142
|
+
if (!id) {
|
|
143
|
+
throw new Error("diff_comment_delete requires an id.");
|
|
144
|
+
}
|
|
145
|
+
const result = deleteCommentStmt.run(id);
|
|
146
|
+
return result.changes > 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = {
|
|
150
|
+
addDiffComment,
|
|
151
|
+
listDiffComments,
|
|
152
|
+
deleteDiffComment,
|
|
153
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
const fs = require("fs/promises");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { createTwoFilesPatch } = require("diff");
|
|
4
|
+
const db = require("../db");
|
|
5
|
+
const { resolveWorkspacePath } = require("../workspace");
|
|
6
|
+
const logger = require("../logger");
|
|
7
|
+
|
|
8
|
+
const insertEditStmt = db.prepare(
|
|
9
|
+
`INSERT INTO edits (
|
|
10
|
+
session_id,
|
|
11
|
+
file_path,
|
|
12
|
+
created_at,
|
|
13
|
+
source,
|
|
14
|
+
before_content,
|
|
15
|
+
after_content,
|
|
16
|
+
diff,
|
|
17
|
+
metadata
|
|
18
|
+
) VALUES (
|
|
19
|
+
@session_id,
|
|
20
|
+
@file_path,
|
|
21
|
+
@created_at,
|
|
22
|
+
@source,
|
|
23
|
+
@before_content,
|
|
24
|
+
@after_content,
|
|
25
|
+
@diff,
|
|
26
|
+
@metadata
|
|
27
|
+
)`,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const selectEditByIdStmt = db.prepare(
|
|
31
|
+
`SELECT id, session_id, file_path, created_at, source,
|
|
32
|
+
before_content, after_content, diff, metadata
|
|
33
|
+
FROM edits WHERE id = ?`,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
function buildHistoryQuery({ filePath, sessionId, limit }) {
|
|
37
|
+
let query = `SELECT id, session_id, file_path, created_at, source,
|
|
38
|
+
before_content, after_content, diff, metadata
|
|
39
|
+
FROM edits`;
|
|
40
|
+
const conditions = [];
|
|
41
|
+
const params = [];
|
|
42
|
+
if (filePath) {
|
|
43
|
+
conditions.push("file_path = ?");
|
|
44
|
+
params.push(filePath);
|
|
45
|
+
}
|
|
46
|
+
if (sessionId) {
|
|
47
|
+
conditions.push("session_id = ?");
|
|
48
|
+
params.push(sessionId);
|
|
49
|
+
}
|
|
50
|
+
if (conditions.length) {
|
|
51
|
+
query += ` WHERE ${conditions.join(" AND ")}`;
|
|
52
|
+
}
|
|
53
|
+
query += " ORDER BY created_at DESC";
|
|
54
|
+
if (limit) {
|
|
55
|
+
query += ` LIMIT ${limit}`;
|
|
56
|
+
}
|
|
57
|
+
return { query, params };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function computeDiff(filePath, beforeContent, afterContent) {
|
|
61
|
+
const before = beforeContent ?? "";
|
|
62
|
+
const after = afterContent ?? "";
|
|
63
|
+
if (before === after) return null;
|
|
64
|
+
return createTwoFilesPatch(
|
|
65
|
+
path.join("before", filePath),
|
|
66
|
+
path.join("after", filePath),
|
|
67
|
+
before,
|
|
68
|
+
after,
|
|
69
|
+
undefined,
|
|
70
|
+
undefined,
|
|
71
|
+
{ context: 3 },
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function recordEdit({
|
|
76
|
+
sessionId,
|
|
77
|
+
filePath,
|
|
78
|
+
source = "tool",
|
|
79
|
+
beforeContent,
|
|
80
|
+
afterContent,
|
|
81
|
+
metadata,
|
|
82
|
+
}) {
|
|
83
|
+
if ((beforeContent ?? "") === (afterContent ?? "")) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
const diff = computeDiff(filePath, beforeContent, afterContent);
|
|
87
|
+
const createdAt = Date.now();
|
|
88
|
+
insertEditStmt.run({
|
|
89
|
+
session_id: sessionId ?? null,
|
|
90
|
+
file_path: filePath,
|
|
91
|
+
created_at: createdAt,
|
|
92
|
+
source,
|
|
93
|
+
before_content: beforeContent ?? null,
|
|
94
|
+
after_content: afterContent ?? null,
|
|
95
|
+
diff,
|
|
96
|
+
metadata: metadata ? JSON.stringify(metadata) : null,
|
|
97
|
+
});
|
|
98
|
+
logger.info(
|
|
99
|
+
{
|
|
100
|
+
sessionId,
|
|
101
|
+
filePath,
|
|
102
|
+
source,
|
|
103
|
+
},
|
|
104
|
+
"Recorded workspace edit",
|
|
105
|
+
);
|
|
106
|
+
return {
|
|
107
|
+
sessionId,
|
|
108
|
+
filePath,
|
|
109
|
+
createdAt,
|
|
110
|
+
diff,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getEditHistory({ filePath, sessionId, limit }) {
|
|
115
|
+
const { query, params } = buildHistoryQuery({
|
|
116
|
+
filePath,
|
|
117
|
+
sessionId,
|
|
118
|
+
limit,
|
|
119
|
+
});
|
|
120
|
+
const rows = db.prepare(query).all(...params);
|
|
121
|
+
return rows.map((row) => ({
|
|
122
|
+
id: row.id,
|
|
123
|
+
sessionId: row.session_id,
|
|
124
|
+
filePath: row.file_path,
|
|
125
|
+
createdAt: row.created_at,
|
|
126
|
+
source: row.source,
|
|
127
|
+
diff: row.diff,
|
|
128
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function revertEdit({ editId, sessionId }) {
|
|
133
|
+
const row = selectEditByIdStmt.get(editId);
|
|
134
|
+
if (!row) {
|
|
135
|
+
throw new Error(`Edit ${editId} not found.`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const absolute = resolveWorkspacePath(row.file_path);
|
|
139
|
+
if (row.before_content === null || row.before_content === undefined) {
|
|
140
|
+
// Original file did not exist; delete if present.
|
|
141
|
+
try {
|
|
142
|
+
await fs.unlink(absolute);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
if (err.code !== "ENOENT") {
|
|
145
|
+
throw err;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
await fs.writeFile(absolute, row.before_content, "utf8");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
recordEdit({
|
|
153
|
+
sessionId,
|
|
154
|
+
filePath: row.file_path,
|
|
155
|
+
source: "revert_edit",
|
|
156
|
+
beforeContent: row.after_content,
|
|
157
|
+
afterContent: row.before_content,
|
|
158
|
+
metadata: { revertedEditId: row.id },
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
revertedEditId: row.id,
|
|
163
|
+
filePath: row.file_path,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = {
|
|
168
|
+
recordEdit,
|
|
169
|
+
getEditHistory,
|
|
170
|
+
revertEdit,
|
|
171
|
+
};
|