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.
Files changed (52) hide show
  1. package/.eslintrc.cjs +12 -0
  2. package/CLAUDE.md +39 -0
  3. package/LICENSE +21 -0
  4. package/README.md +417 -0
  5. package/bin/cli.js +3 -0
  6. package/index.js +3 -0
  7. package/package.json +54 -0
  8. package/src/api/middleware/logging.js +37 -0
  9. package/src/api/middleware/session.js +55 -0
  10. package/src/api/router.js +80 -0
  11. package/src/cache/prompt.js +183 -0
  12. package/src/clients/databricks.js +72 -0
  13. package/src/config/index.js +301 -0
  14. package/src/db/index.js +192 -0
  15. package/src/diff/comments.js +153 -0
  16. package/src/edits/index.js +171 -0
  17. package/src/indexer/index.js +1610 -0
  18. package/src/indexer/navigation/index.js +32 -0
  19. package/src/indexer/navigation/providers/treeSitter.js +36 -0
  20. package/src/indexer/parser.js +324 -0
  21. package/src/logger/index.js +27 -0
  22. package/src/mcp/client.js +194 -0
  23. package/src/mcp/index.js +34 -0
  24. package/src/mcp/permissions.js +69 -0
  25. package/src/mcp/registry.js +225 -0
  26. package/src/mcp/sandbox.js +238 -0
  27. package/src/metrics/index.js +38 -0
  28. package/src/orchestrator/index.js +1492 -0
  29. package/src/policy/index.js +212 -0
  30. package/src/policy/web-fallback.js +33 -0
  31. package/src/server.js +73 -0
  32. package/src/sessions/index.js +15 -0
  33. package/src/sessions/record.js +31 -0
  34. package/src/sessions/store.js +179 -0
  35. package/src/tasks/store.js +349 -0
  36. package/src/tests/coverage.js +173 -0
  37. package/src/tests/index.js +171 -0
  38. package/src/tests/store.js +213 -0
  39. package/src/tools/edits.js +94 -0
  40. package/src/tools/execution.js +169 -0
  41. package/src/tools/git.js +1346 -0
  42. package/src/tools/index.js +258 -0
  43. package/src/tools/indexer.js +360 -0
  44. package/src/tools/mcp-remote.js +81 -0
  45. package/src/tools/mcp.js +116 -0
  46. package/src/tools/process.js +151 -0
  47. package/src/tools/stubs.js +55 -0
  48. package/src/tools/tasks.js +260 -0
  49. package/src/tools/tests.js +132 -0
  50. package/src/tools/web.js +286 -0
  51. package/src/tools/workspace.js +173 -0
  52. package/src/workspace/index.js +95 -0
@@ -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
+ };