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,212 @@
1
+ const config = require("../config");
2
+ const logger = require("../logger");
3
+
4
+ const SHELL_BLOCKLIST_PATTERNS = [
5
+ new RegExp("rm\\s+-rf\\s+/(?:\\s|$)", "i"),
6
+ /shutdown/i,
7
+ /reboot/i,
8
+ /systemctl\s+stop/i,
9
+ /mkfs\w*/i,
10
+ /dd\s+if=\/dev\//i,
11
+ /:(){:|:&};:/, // fork bomb
12
+ /chown\s+-R\s+root/i,
13
+ ];
14
+
15
+ const PYTHON_BLOCKLIST_PATTERNS = [
16
+ /os\.remove\s*\(\s*['"]\/['"]\s*\)/i,
17
+ /subprocess\.(call|run)\s*\(\s*["']rm\s+-rf/i,
18
+ /shutil\.rmtree\s*\(\s*['"]\/['"]\s*\)/i,
19
+ ];
20
+
21
+ const SENSITIVE_CONTENT_PATTERNS = [
22
+ {
23
+ regex: /-----BEGIN [^-]+ PRIVATE KEY-----/i,
24
+ replacement: "[REDACTED PRIVATE KEY]",
25
+ },
26
+ {
27
+ regex: new RegExp("\\b[A-Za-z0-9+/]{32,}={0,2}\\b", "g"),
28
+ maxLength: 64,
29
+ replacement: "[POTENTIAL SECRET]",
30
+ },
31
+ ];
32
+
33
+ function parseArguments(call) {
34
+ const raw = call?.function?.arguments;
35
+ if (typeof raw !== "string") return {};
36
+ try {
37
+ return JSON.parse(raw);
38
+ } catch {
39
+ return {};
40
+ }
41
+ }
42
+
43
+ function isToolAllowed(toolName) {
44
+ if (!toolName) return true;
45
+ const disallowed = config.policy.disallowedTools ?? [];
46
+ return !disallowed.includes(toolName);
47
+ }
48
+
49
+ function matchesAny(patterns, value) {
50
+ if (typeof value !== "string") return false;
51
+ return patterns.some((regex) => regex.test(value));
52
+ }
53
+
54
+ function evaluateShellCall(args) {
55
+ const command =
56
+ typeof args.command === "string"
57
+ ? args.command
58
+ : typeof args.cmd === "string"
59
+ ? args.cmd
60
+ : typeof args.run === "string"
61
+ ? args.run
62
+ : Array.isArray(args.command)
63
+ ? args.command.join(" ")
64
+ : Array.isArray(args.args)
65
+ ? args.args.join(" ")
66
+ : "";
67
+ if (!command) return { allowed: true };
68
+ if (matchesAny(SHELL_BLOCKLIST_PATTERNS, command)) {
69
+ return {
70
+ allowed: false,
71
+ reason: "Command matched restricted pattern",
72
+ };
73
+ }
74
+ return { allowed: true };
75
+ }
76
+
77
+ function evaluatePythonCall(args) {
78
+ const code =
79
+ typeof args.code === "string"
80
+ ? args.code
81
+ : typeof args.script === "string"
82
+ ? args.script
83
+ : typeof args.input === "string"
84
+ ? args.input
85
+ : "";
86
+ if (!code) return { allowed: true };
87
+ if (matchesAny(PYTHON_BLOCKLIST_PATTERNS, code)) {
88
+ return {
89
+ allowed: false,
90
+ reason: "Python code matched restricted pattern",
91
+ };
92
+ }
93
+ return { allowed: true };
94
+ }
95
+
96
+ function evaluateToolCall({ call, toolCallsExecuted }) {
97
+ const toolName = call?.function?.name ?? call?.name;
98
+ if (!isToolAllowed(toolName)) {
99
+ return {
100
+ allowed: false,
101
+ reason: `Tool ${toolName} is disallowed by policy`,
102
+ status: 403,
103
+ code: "tool_disallowed",
104
+ };
105
+ }
106
+
107
+ const maxToolCalls = config.policy.maxToolCallsPerTurn;
108
+ if (toolCallsExecuted >= maxToolCalls) {
109
+ return {
110
+ allowed: false,
111
+ reason: `Exceeded max tool calls (${maxToolCalls})`,
112
+ status: 429,
113
+ code: "tool_limit_reached",
114
+ };
115
+ }
116
+
117
+ const args = parseArguments(call);
118
+
119
+ if (toolName && toolName.startsWith("workspace_git_")) {
120
+ const gitPolicy = config.policy.git ?? {};
121
+ if (
122
+ toolName === "workspace_git_push" &&
123
+ gitPolicy.allowPush !== true
124
+ ) {
125
+ return {
126
+ allowed: false,
127
+ reason: "Git push is disabled by policy.",
128
+ status: 403,
129
+ code: "git_push_disabled",
130
+ };
131
+ }
132
+ if (
133
+ toolName === "workspace_git_pull" &&
134
+ gitPolicy.allowPull !== true
135
+ ) {
136
+ return {
137
+ allowed: false,
138
+ reason: "Git pull is disabled by policy.",
139
+ status: 403,
140
+ code: "git_pull_disabled",
141
+ };
142
+ }
143
+ if (
144
+ toolName === "workspace_git_commit" &&
145
+ gitPolicy.allowCommit !== true
146
+ ) {
147
+ return {
148
+ allowed: false,
149
+ reason: "Git commit is disabled by policy.",
150
+ status: 403,
151
+ code: "git_commit_disabled",
152
+ };
153
+ }
154
+ }
155
+
156
+ if (toolName === "shell") {
157
+ const decision = evaluateShellCall(args);
158
+ if (!decision.allowed) {
159
+ return {
160
+ allowed: false,
161
+ reason: decision.reason,
162
+ status: 403,
163
+ code: "unsafe_shell_command",
164
+ };
165
+ }
166
+ }
167
+
168
+ if (toolName === "python_exec") {
169
+ const decision = evaluatePythonCall(args);
170
+ if (!decision.allowed) {
171
+ return {
172
+ allowed: false,
173
+ reason: decision.reason,
174
+ status: 403,
175
+ code: "unsafe_python_code",
176
+ };
177
+ }
178
+ }
179
+
180
+ return { allowed: true };
181
+ }
182
+
183
+ function sanitiseText(text) {
184
+ if (typeof text !== "string") return text;
185
+ let output = text;
186
+ for (const pattern of SENSITIVE_CONTENT_PATTERNS) {
187
+ if (pattern.maxLength && output.length < pattern.maxLength) continue;
188
+ output = output.replace(pattern.regex, pattern.replacement);
189
+ }
190
+ return output;
191
+ }
192
+
193
+ function sanitiseContent(contentItems) {
194
+ if (!Array.isArray(contentItems)) return contentItems;
195
+ return contentItems.map((item) => {
196
+ if (item?.type === "text" && typeof item.text === "string") {
197
+ return { ...item, text: sanitiseText(item.text) };
198
+ }
199
+ return item;
200
+ });
201
+ }
202
+
203
+ function logPolicyDecision(decision, context = {}) {
204
+ if (decision.allowed) return;
205
+ logger.warn({ decision, context }, "Policy blocked tool call");
206
+ }
207
+
208
+ module.exports = {
209
+ evaluateToolCall,
210
+ sanitiseContent,
211
+ logPolicyDecision,
212
+ };
@@ -0,0 +1,33 @@
1
+ const BROWSING_FALLBACK_PATTERNS = [
2
+ /i (do|don't|cannot) have (browser|browsing|internet) (capability|access)/i,
3
+ /cannot look up information/i,
4
+ /no web browsing capability/i,
5
+ /can'?t (access|reach) the internet/i,
6
+ /(do not|don't) have access to .*web (?:browsing|browser|internet)/i,
7
+ /(do not|don't) have .*browser/i,
8
+ /web(fetch|_fetch| search).*(not available|disabled|unavailable)/i,
9
+ /tool.*(not available|disabled|unavailable)/i,
10
+ /don't have access to real-time/i,
11
+ ];
12
+
13
+ function needsWebFallback(text) {
14
+ if (typeof text !== "string") return false;
15
+ const trimmed = text.trim();
16
+ if (!trimmed) return false;
17
+
18
+ // If the response already includes concrete financial data, skip fallback.
19
+ if (
20
+ /\bclosed at \$\d[\d.,]*/i.test(trimmed) ||
21
+ /\bprevious close\b/i.test(trimmed) ||
22
+ /\bday'?s range\b/i.test(trimmed) ||
23
+ /\btrading volume\b/i.test(trimmed)
24
+ ) {
25
+ return false;
26
+ }
27
+
28
+ return BROWSING_FALLBACK_PATTERNS.some((regex) => regex.test(trimmed));
29
+ }
30
+
31
+ module.exports = {
32
+ needsWebFallback,
33
+ };
package/src/server.js ADDED
@@ -0,0 +1,73 @@
1
+ const express = require("express");
2
+ const config = require("./config");
3
+ const loggingMiddleware = require("./api/middleware/logging");
4
+ const router = require("./api/router");
5
+ const { sessionMiddleware } = require("./api/middleware/session");
6
+ const metrics = require("./metrics");
7
+ const logger = require("./logger");
8
+ const { initialiseMcp } = require("./mcp");
9
+ const { registerStubTools } = require("./tools/stubs");
10
+ const { registerWorkspaceTools } = require("./tools/workspace");
11
+ const { registerExecutionTools } = require("./tools/execution");
12
+ const { registerWebTools } = require("./tools/web");
13
+ const { registerIndexerTools } = require("./tools/indexer");
14
+ const { registerEditTools } = require("./tools/edits");
15
+ const { registerGitTools } = require("./tools/git");
16
+ const { registerTaskTools } = require("./tools/tasks");
17
+ const { registerTestTools } = require("./tools/tests");
18
+ const { registerMcpTools } = require("./tools/mcp");
19
+
20
+ initialiseMcp();
21
+ registerStubTools();
22
+ registerWorkspaceTools();
23
+ registerExecutionTools();
24
+ registerWebTools();
25
+ registerIndexerTools();
26
+ registerEditTools();
27
+ registerGitTools();
28
+ registerTaskTools();
29
+ registerTestTools();
30
+ registerMcpTools();
31
+
32
+ function createApp() {
33
+ const app = express();
34
+
35
+ app.use(express.json({ limit: config.server.jsonLimit }));
36
+ app.use(sessionMiddleware);
37
+ app.use(loggingMiddleware);
38
+
39
+ app.get("/metrics", (req, res) => {
40
+ res.json(metrics.snapshot());
41
+ });
42
+
43
+ app.use(router);
44
+
45
+ // Basic error handler to surface issues cleanly.
46
+ app.use((err, req, res, next) => {
47
+ logger.error({ err }, "Request error");
48
+ if (res.headersSent) {
49
+ return next(err);
50
+ }
51
+ const status = err.status ?? 500;
52
+ metrics.recordResponse(status);
53
+ res.status(status).json({
54
+ error: err.code ?? "internal_error",
55
+ message: err.message ?? "Unexpected error",
56
+ });
57
+ });
58
+
59
+ return app;
60
+ }
61
+
62
+ function start() {
63
+ const app = createApp();
64
+ app.listen(config.port, () => {
65
+ console.log(`Claude→Databricks proxy listening on http://localhost:${config.port}`);
66
+ });
67
+ return app;
68
+ }
69
+
70
+ module.exports = {
71
+ createApp,
72
+ start,
73
+ };
@@ -0,0 +1,15 @@
1
+ const {
2
+ getSession,
3
+ getOrCreateSession,
4
+ upsertSession,
5
+ appendSessionTurn,
6
+ deleteSession,
7
+ } = require("./store");
8
+
9
+ module.exports = {
10
+ getSession,
11
+ getOrCreateSession,
12
+ upsertSession,
13
+ appendSessionTurn,
14
+ deleteSession,
15
+ };
@@ -0,0 +1,31 @@
1
+ const { appendSessionTurn } = require("./store");
2
+
3
+ function ensureSessionShape(session) {
4
+ if (!session) return null;
5
+ if (!Array.isArray(session.history)) {
6
+ session.history = [];
7
+ }
8
+ if (!session.createdAt) {
9
+ session.createdAt = Date.now();
10
+ }
11
+ return session;
12
+ }
13
+
14
+ function appendTurnToSession(session, entry) {
15
+ const target = ensureSessionShape(session);
16
+ if (!target) return null;
17
+
18
+ const turn = { ...entry, timestamp: Date.now() };
19
+ target.history.push(turn);
20
+ target.updatedAt = turn.timestamp;
21
+
22
+ if (target.id) {
23
+ appendSessionTurn(target.id, turn, target.metadata ?? {});
24
+ }
25
+
26
+ return turn;
27
+ }
28
+
29
+ module.exports = {
30
+ appendTurnToSession,
31
+ };
@@ -0,0 +1,179 @@
1
+ const db = require("../db");
2
+ const logger = require("../logger");
3
+
4
+ const selectSessionStmt = db.prepare(
5
+ "SELECT id, created_at, updated_at, metadata FROM sessions WHERE id = ?",
6
+ );
7
+ const selectHistoryStmt = db.prepare(
8
+ `SELECT role, type, status, content, metadata, timestamp
9
+ FROM session_history
10
+ WHERE session_id = ?
11
+ ORDER BY timestamp ASC, id ASC`,
12
+ );
13
+ const insertSessionStmt = db.prepare(
14
+ "INSERT INTO sessions (id, created_at, updated_at, metadata) VALUES (@id, @created_at, @updated_at, @metadata)",
15
+ );
16
+ const updateSessionStmt = db.prepare(
17
+ "UPDATE sessions SET updated_at = @updated_at, metadata = @metadata WHERE id = @id",
18
+ );
19
+ const updateSessionTimestampStmt = db.prepare(
20
+ "UPDATE sessions SET updated_at = @updated_at WHERE id = @id",
21
+ );
22
+ const deleteSessionStmt = db.prepare("DELETE FROM sessions WHERE id = ?");
23
+ const deleteHistoryStmt = db.prepare("DELETE FROM session_history WHERE session_id = ?");
24
+ const insertHistoryStmt = db.prepare(
25
+ `INSERT INTO session_history (session_id, role, type, status, content, metadata, timestamp)
26
+ VALUES (@session_id, @role, @type, @status, @content, @metadata, @timestamp)`,
27
+ );
28
+
29
+ function parseJSON(value, fallback) {
30
+ if (value === null || value === undefined) return fallback;
31
+ try {
32
+ return JSON.parse(value);
33
+ } catch (err) {
34
+ logger.warn({ err }, "Failed to parse JSON from session store");
35
+ return fallback;
36
+ }
37
+ }
38
+
39
+ function serialize(value) {
40
+ if (value === undefined) return null;
41
+ try {
42
+ return JSON.stringify(value);
43
+ } catch (err) {
44
+ logger.warn({ err }, "Failed to serialize JSON for session store");
45
+ return null;
46
+ }
47
+ }
48
+
49
+ function toSession(row, historyRows = []) {
50
+ return {
51
+ id: row.id,
52
+ createdAt: row.created_at,
53
+ updatedAt: row.updated_at,
54
+ metadata: parseJSON(row.metadata, {}) ?? {},
55
+ history: historyRows.map((item) => ({
56
+ role: item.role ?? undefined,
57
+ type: item.type ?? undefined,
58
+ status: item.status ?? undefined,
59
+ content: parseJSON(item.content, null),
60
+ metadata: parseJSON(item.metadata, null) ?? undefined,
61
+ timestamp: item.timestamp,
62
+ })),
63
+ };
64
+ }
65
+
66
+ function getSession(sessionId) {
67
+ if (!sessionId) return null;
68
+ const sessionRow = selectSessionStmt.get(sessionId);
69
+ if (!sessionRow) return null;
70
+ const historyRows = selectHistoryStmt.all(sessionId);
71
+ return toSession(sessionRow, historyRows);
72
+ }
73
+
74
+ function createSession(sessionId, metadata = {}) {
75
+ const now = Date.now();
76
+ insertSessionStmt.run({
77
+ id: sessionId,
78
+ created_at: now,
79
+ updated_at: now,
80
+ metadata: serialize(metadata) ?? "{}",
81
+ });
82
+ return {
83
+ id: sessionId,
84
+ createdAt: now,
85
+ updatedAt: now,
86
+ metadata: metadata ?? {},
87
+ history: [],
88
+ };
89
+ }
90
+
91
+ function getOrCreateSession(sessionId) {
92
+ const existing = getSession(sessionId);
93
+ if (existing) return existing;
94
+
95
+ try {
96
+ return createSession(sessionId);
97
+ } catch (err) {
98
+ if (err.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
99
+ return getSession(sessionId);
100
+ }
101
+ throw err;
102
+ }
103
+ }
104
+
105
+ function upsertSession(sessionId, data = {}) {
106
+ if (!sessionId) return null;
107
+ const metadata = data.metadata ?? {};
108
+ const updatedAt = data.updatedAt ?? Date.now();
109
+ const createdAt = data.createdAt ?? updatedAt;
110
+
111
+ const existing = selectSessionStmt.get(sessionId);
112
+ if (!existing) {
113
+ insertSessionStmt.run({
114
+ id: sessionId,
115
+ created_at: createdAt,
116
+ updated_at: updatedAt,
117
+ metadata: serialize(metadata) ?? "{}",
118
+ });
119
+ } else {
120
+ updateSessionStmt.run({
121
+ id: sessionId,
122
+ updated_at: updatedAt,
123
+ metadata: serialize(metadata) ?? "{}",
124
+ });
125
+ }
126
+ return getSession(sessionId);
127
+ }
128
+
129
+ function appendSessionTurn(sessionId, turn, metadata) {
130
+ if (!sessionId) return null;
131
+ const timestamp = turn.timestamp ?? Date.now();
132
+
133
+ const params = {
134
+ session_id: sessionId,
135
+ role: turn.role ?? null,
136
+ type: turn.type ?? null,
137
+ status: typeof turn.status === "number" ? turn.status : null,
138
+ content: serialize(turn.content),
139
+ metadata: serialize(turn.metadata),
140
+ timestamp,
141
+ };
142
+
143
+ logger.debug({ params }, "Inserting session history row");
144
+ insertHistoryStmt.run(params);
145
+
146
+ logger.debug({ sessionId, timestamp, metadata }, "Updating session metadata");
147
+
148
+ if (metadata !== undefined) {
149
+ updateSessionStmt.run({
150
+ id: sessionId,
151
+ updated_at: timestamp,
152
+ metadata: serialize(metadata) ?? "{}",
153
+ });
154
+ } else {
155
+ updateSessionTimestampStmt.run({
156
+ id: sessionId,
157
+ updated_at: timestamp,
158
+ });
159
+ }
160
+
161
+ return { ...turn, timestamp };
162
+ }
163
+
164
+ function deleteSession(sessionId) {
165
+ if (!sessionId) return;
166
+ const deleteHistory = db.transaction((id) => {
167
+ deleteHistoryStmt.run(id);
168
+ deleteSessionStmt.run(id);
169
+ });
170
+ deleteHistory(sessionId);
171
+ }
172
+
173
+ module.exports = {
174
+ getSession,
175
+ getOrCreateSession,
176
+ upsertSession,
177
+ appendSessionTurn,
178
+ deleteSession,
179
+ };