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
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const express = require("express");
|
|
2
|
+
const { processMessage } = require("../orchestrator");
|
|
3
|
+
const { getSession } = require("../sessions");
|
|
4
|
+
const metrics = require("../metrics");
|
|
5
|
+
|
|
6
|
+
const router = express.Router();
|
|
7
|
+
|
|
8
|
+
router.get("/health", (req, res) => {
|
|
9
|
+
res.json({ status: "ok" });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
router.get("/debug/session", (req, res) => {
|
|
13
|
+
if (!req.sessionId) {
|
|
14
|
+
return res.status(400).json({ error: "missing_session_id", message: "Provide x-session-id header" });
|
|
15
|
+
}
|
|
16
|
+
const session = getSession(req.sessionId);
|
|
17
|
+
if (!session) {
|
|
18
|
+
return res.status(404).json({ error: "session_not_found", message: "Session not found" });
|
|
19
|
+
}
|
|
20
|
+
res.json({ session });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
router.post("/v1/messages", async (req, res, next) => {
|
|
24
|
+
try {
|
|
25
|
+
metrics.recordRequest();
|
|
26
|
+
const wantsStream = Boolean(req.body?.stream);
|
|
27
|
+
const result = await processMessage({
|
|
28
|
+
payload: req.body,
|
|
29
|
+
headers: req.headers,
|
|
30
|
+
session: req.session,
|
|
31
|
+
options: {
|
|
32
|
+
maxSteps: req.body?.max_steps,
|
|
33
|
+
maxDurationMs: req.body?.max_duration_ms,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (wantsStream) {
|
|
38
|
+
metrics.recordStreamingStart();
|
|
39
|
+
res.set({
|
|
40
|
+
"Content-Type": "text/event-stream",
|
|
41
|
+
"Cache-Control": "no-cache",
|
|
42
|
+
Connection: "keep-alive",
|
|
43
|
+
});
|
|
44
|
+
if (typeof res.flushHeaders === "function") {
|
|
45
|
+
res.flushHeaders();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const eventPayload = {
|
|
49
|
+
type: "message",
|
|
50
|
+
message: result.body,
|
|
51
|
+
};
|
|
52
|
+
res.write(`event: message\n`);
|
|
53
|
+
res.write(`data: ${JSON.stringify(eventPayload)}\n\n`);
|
|
54
|
+
|
|
55
|
+
res.write(`event: end\n`);
|
|
56
|
+
res.write(
|
|
57
|
+
`data: ${JSON.stringify({ termination: result.terminationReason ?? "completion" })}\n\n`,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
metrics.recordResponse(result.status);
|
|
61
|
+
res.end();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (result.headers) {
|
|
66
|
+
Object.entries(result.headers).forEach(([key, value]) => {
|
|
67
|
+
if (value !== undefined) {
|
|
68
|
+
res.setHeader(key, value);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
metrics.recordResponse(result.status);
|
|
74
|
+
res.status(result.status).send(result.body);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
next(error);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
module.exports = router;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
2
|
+
const config = require("../config");
|
|
3
|
+
const logger = require("../logger");
|
|
4
|
+
|
|
5
|
+
function cloneValue(value) {
|
|
6
|
+
if (typeof structuredClone === "function") {
|
|
7
|
+
return structuredClone(value);
|
|
8
|
+
}
|
|
9
|
+
return JSON.parse(JSON.stringify(value));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function normaliseObject(value) {
|
|
13
|
+
if (value === null || typeof value !== "object") return value;
|
|
14
|
+
if (Array.isArray(value)) {
|
|
15
|
+
return value.map((item) => normaliseObject(item));
|
|
16
|
+
}
|
|
17
|
+
const sorted = {};
|
|
18
|
+
for (const key of Object.keys(value).sort()) {
|
|
19
|
+
const candidate = value[key];
|
|
20
|
+
if (candidate === undefined) continue;
|
|
21
|
+
sorted[key] = normaliseObject(candidate);
|
|
22
|
+
}
|
|
23
|
+
return sorted;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function stableStringify(value) {
|
|
27
|
+
return JSON.stringify(normaliseObject(value));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class PromptCache {
|
|
31
|
+
constructor(options = {}) {
|
|
32
|
+
this.enabled = options.enabled === true;
|
|
33
|
+
this.maxEntries =
|
|
34
|
+
Number.isInteger(options.maxEntries) && options.maxEntries > 0
|
|
35
|
+
? options.maxEntries
|
|
36
|
+
: 64;
|
|
37
|
+
this.ttlMs =
|
|
38
|
+
Number.isInteger(options.ttlMs) && options.ttlMs > 0 ? options.ttlMs : 300000;
|
|
39
|
+
this.store = new Map();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
isEnabled() {
|
|
43
|
+
return this.enabled;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
buildKey(payload) {
|
|
47
|
+
if (!this.enabled) return null;
|
|
48
|
+
if (!payload || typeof payload !== "object") return null;
|
|
49
|
+
try {
|
|
50
|
+
const canonical = {
|
|
51
|
+
model: payload.model ?? null,
|
|
52
|
+
input: payload.input ?? null,
|
|
53
|
+
messages: payload.messages ? normaliseObject(payload.messages) : null,
|
|
54
|
+
tools: payload.tools ? normaliseObject(payload.tools) : null,
|
|
55
|
+
tool_choice: payload.tool_choice ? normaliseObject(payload.tool_choice) : null,
|
|
56
|
+
temperature: payload.temperature ?? null,
|
|
57
|
+
top_p: payload.top_p ?? null,
|
|
58
|
+
max_tokens: payload.max_tokens ?? null,
|
|
59
|
+
};
|
|
60
|
+
const serialised = stableStringify(canonical);
|
|
61
|
+
return crypto.createHash("sha256").update(serialised).digest("hex");
|
|
62
|
+
} catch (error) {
|
|
63
|
+
logger.warn(
|
|
64
|
+
{
|
|
65
|
+
err: error,
|
|
66
|
+
},
|
|
67
|
+
"Failed to build prompt cache key",
|
|
68
|
+
);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
pruneExpired() {
|
|
74
|
+
if (!this.enabled) return;
|
|
75
|
+
if (this.ttlMs <= 0) return;
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
for (const [key, entry] of this.store) {
|
|
78
|
+
if (entry.expiresAt && entry.expiresAt <= now) {
|
|
79
|
+
this.store.delete(key);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
lookup(payloadOrKey) {
|
|
85
|
+
if (!this.enabled) {
|
|
86
|
+
return { key: null, entry: null };
|
|
87
|
+
}
|
|
88
|
+
const key =
|
|
89
|
+
typeof payloadOrKey === "string" ? payloadOrKey : this.buildKey(payloadOrKey);
|
|
90
|
+
if (!key) {
|
|
91
|
+
return { key: null, entry: null };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.pruneExpired();
|
|
95
|
+
const entry = this.store.get(key);
|
|
96
|
+
if (!entry) {
|
|
97
|
+
return { key, entry: null };
|
|
98
|
+
}
|
|
99
|
+
if (entry.expiresAt && entry.expiresAt <= Date.now()) {
|
|
100
|
+
this.store.delete(key);
|
|
101
|
+
return { key, entry: null };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.store.delete(key);
|
|
105
|
+
this.store.set(key, entry);
|
|
106
|
+
return { key, entry };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
fetch(payload) {
|
|
110
|
+
const { key, entry } = this.lookup(payload);
|
|
111
|
+
if (!entry) return null;
|
|
112
|
+
return {
|
|
113
|
+
key,
|
|
114
|
+
response: cloneValue(entry.value),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
shouldCacheResponse(response) {
|
|
119
|
+
if (!response) return false;
|
|
120
|
+
if (response.ok !== true) return false;
|
|
121
|
+
if (!response.json) return false;
|
|
122
|
+
if (typeof response.status === "number" && response.status !== 200) return false;
|
|
123
|
+
|
|
124
|
+
const choice = response.json?.choices?.[0];
|
|
125
|
+
if (!choice) return false;
|
|
126
|
+
if (choice?.finish_reason === "tool_calls") return false;
|
|
127
|
+
|
|
128
|
+
const message = choice.message ?? {};
|
|
129
|
+
if (Array.isArray(message.tool_calls) && message.tool_calls.length > 0) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
storeResponse(payloadOrKey, response) {
|
|
136
|
+
if (!this.enabled) return null;
|
|
137
|
+
if (!this.shouldCacheResponse(response)) return null;
|
|
138
|
+
const key =
|
|
139
|
+
typeof payloadOrKey === "string" ? payloadOrKey : this.buildKey(payloadOrKey);
|
|
140
|
+
if (!key) return null;
|
|
141
|
+
|
|
142
|
+
this.pruneExpired();
|
|
143
|
+
const entry = {
|
|
144
|
+
value: cloneValue(response),
|
|
145
|
+
createdAt: Date.now(),
|
|
146
|
+
expiresAt: this.ttlMs > 0 ? Date.now() + this.ttlMs : null,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
if (this.store.has(key)) {
|
|
150
|
+
this.store.delete(key);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this.store.set(key, entry);
|
|
154
|
+
|
|
155
|
+
while (this.store.size > this.maxEntries) {
|
|
156
|
+
const oldestKey = this.store.keys().next().value;
|
|
157
|
+
this.store.delete(oldestKey);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
logger.debug(
|
|
161
|
+
{
|
|
162
|
+
cacheKey: key,
|
|
163
|
+
size: this.store.size,
|
|
164
|
+
},
|
|
165
|
+
"Stored response in prompt cache",
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
return key;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
stats() {
|
|
172
|
+
return {
|
|
173
|
+
enabled: this.enabled,
|
|
174
|
+
size: this.store.size,
|
|
175
|
+
ttlMs: this.ttlMs,
|
|
176
|
+
maxEntries: this.maxEntries,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const promptCache = new PromptCache(config.promptCache ?? {});
|
|
182
|
+
|
|
183
|
+
module.exports = promptCache;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const config = require("../config");
|
|
2
|
+
|
|
3
|
+
if (typeof fetch !== "function") {
|
|
4
|
+
throw new Error("Node 18+ is required for the built-in fetch API.");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
async function performJsonRequest(url, { headers = {}, body }, providerLabel) {
|
|
8
|
+
const response = await fetch(url, {
|
|
9
|
+
method: "POST",
|
|
10
|
+
headers,
|
|
11
|
+
body: JSON.stringify(body),
|
|
12
|
+
});
|
|
13
|
+
const text = await response.text();
|
|
14
|
+
console.log(`=== ${providerLabel} response ===`);
|
|
15
|
+
console.log("Status:", response.status);
|
|
16
|
+
console.log(text);
|
|
17
|
+
console.log("===========================");
|
|
18
|
+
|
|
19
|
+
let json;
|
|
20
|
+
try {
|
|
21
|
+
json = JSON.parse(text);
|
|
22
|
+
} catch {
|
|
23
|
+
json = null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
ok: response.ok,
|
|
28
|
+
status: response.status,
|
|
29
|
+
json,
|
|
30
|
+
text,
|
|
31
|
+
contentType: response.headers.get("content-type"),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function invokeDatabricks(body) {
|
|
36
|
+
if (!config.databricks?.url) {
|
|
37
|
+
throw new Error("Databricks configuration is missing required URL.");
|
|
38
|
+
}
|
|
39
|
+
const headers = {
|
|
40
|
+
Authorization: `Bearer ${config.databricks.apiKey}`,
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
};
|
|
43
|
+
return performJsonRequest(config.databricks.url, { headers, body }, "Databricks");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function invokeAzureAnthropic(body) {
|
|
47
|
+
if (!config.azureAnthropic?.endpoint) {
|
|
48
|
+
throw new Error("Azure Anthropic endpoint is not configured.");
|
|
49
|
+
}
|
|
50
|
+
const headers = {
|
|
51
|
+
"Content-Type": "application/json",
|
|
52
|
+
"x-api-key": config.azureAnthropic.apiKey,
|
|
53
|
+
"anthropic-version": config.azureAnthropic.version ?? "2023-06-01",
|
|
54
|
+
};
|
|
55
|
+
return performJsonRequest(
|
|
56
|
+
config.azureAnthropic.endpoint,
|
|
57
|
+
{ headers, body },
|
|
58
|
+
"Azure Anthropic",
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function invokeModel(body) {
|
|
63
|
+
const provider = config.modelProvider?.type ?? "databricks";
|
|
64
|
+
if (provider === "azure-anthropic") {
|
|
65
|
+
return invokeAzureAnthropic(body);
|
|
66
|
+
}
|
|
67
|
+
return invokeDatabricks(body);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = {
|
|
71
|
+
invokeModel,
|
|
72
|
+
};
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const dotenv = require("dotenv");
|
|
3
|
+
|
|
4
|
+
dotenv.config();
|
|
5
|
+
|
|
6
|
+
function trimTrailingSlash(value) {
|
|
7
|
+
if (typeof value !== "string") return value;
|
|
8
|
+
return value.replace(/\/$/, "");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function parseJson(value, fallback = null) {
|
|
12
|
+
if (typeof value !== "string" || value.trim().length === 0) return fallback;
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(value);
|
|
15
|
+
} catch {
|
|
16
|
+
return fallback;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseList(value, options = {}) {
|
|
21
|
+
if (typeof value !== "string" || value.trim().length === 0) return [];
|
|
22
|
+
const separator = options.separator ?? ",";
|
|
23
|
+
return value
|
|
24
|
+
.split(separator)
|
|
25
|
+
.map((item) => item.trim())
|
|
26
|
+
.filter(Boolean);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseMountList(value) {
|
|
30
|
+
if (typeof value !== "string" || value.trim().length === 0) return [];
|
|
31
|
+
return value
|
|
32
|
+
.split(";")
|
|
33
|
+
.map((entry) => entry.trim())
|
|
34
|
+
.filter(Boolean)
|
|
35
|
+
.map((entry) => {
|
|
36
|
+
const parts = entry.split(":");
|
|
37
|
+
if (parts.length < 2) return null;
|
|
38
|
+
const host = parts[0]?.trim();
|
|
39
|
+
const container = parts[1]?.trim();
|
|
40
|
+
const mode = parts[2]?.trim() || "rw";
|
|
41
|
+
if (!host || !container) return null;
|
|
42
|
+
return {
|
|
43
|
+
host: path.resolve(host),
|
|
44
|
+
container,
|
|
45
|
+
mode,
|
|
46
|
+
};
|
|
47
|
+
})
|
|
48
|
+
.filter(Boolean);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function resolveConfigPath(targetPath) {
|
|
52
|
+
if (typeof targetPath !== "string" || targetPath.trim().length === 0) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
let normalised = targetPath.trim();
|
|
56
|
+
if (normalised.startsWith("~")) {
|
|
57
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
58
|
+
if (home) {
|
|
59
|
+
normalised = path.join(home, normalised.slice(1));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return path.resolve(normalised);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const SUPPORTED_MODEL_PROVIDERS = new Set(["databricks", "azure-anthropic"]);
|
|
66
|
+
const rawModelProvider = (process.env.MODEL_PROVIDER ?? "databricks").toLowerCase();
|
|
67
|
+
const modelProvider = SUPPORTED_MODEL_PROVIDERS.has(rawModelProvider)
|
|
68
|
+
? rawModelProvider
|
|
69
|
+
: "databricks";
|
|
70
|
+
|
|
71
|
+
const rawBaseUrl = trimTrailingSlash(process.env.DATABRICKS_API_BASE);
|
|
72
|
+
const apiKey = process.env.DATABRICKS_API_KEY;
|
|
73
|
+
|
|
74
|
+
const azureAnthropicEndpoint = process.env.AZURE_ANTHROPIC_ENDPOINT ?? null;
|
|
75
|
+
const azureAnthropicApiKey = process.env.AZURE_ANTHROPIC_API_KEY ?? null;
|
|
76
|
+
const azureAnthropicVersion = process.env.AZURE_ANTHROPIC_VERSION ?? "2023-06-01";
|
|
77
|
+
|
|
78
|
+
if (modelProvider === "databricks" && (!rawBaseUrl || !apiKey)) {
|
|
79
|
+
throw new Error("Set DATABRICKS_API_BASE and DATABRICKS_API_KEY before starting the proxy.");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (modelProvider === "azure-anthropic" && (!azureAnthropicEndpoint || !azureAnthropicApiKey)) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
"Set AZURE_ANTHROPIC_ENDPOINT and AZURE_ANTHROPIC_API_KEY before starting the proxy.",
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const endpointPath =
|
|
89
|
+
process.env.DATABRICKS_ENDPOINT_PATH ??
|
|
90
|
+
"/serving-endpoints/databricks-claude-sonnet-4-5/invocations";
|
|
91
|
+
|
|
92
|
+
const databricksUrl =
|
|
93
|
+
rawBaseUrl && endpointPath
|
|
94
|
+
? `${rawBaseUrl}${endpointPath.startsWith("/") ? "" : "/"}${endpointPath}`
|
|
95
|
+
: null;
|
|
96
|
+
|
|
97
|
+
const defaultModel =
|
|
98
|
+
process.env.MODEL_DEFAULT ??
|
|
99
|
+
(modelProvider === "azure-anthropic" ? "claude-opus-4-5" : "databricks-claude-sonnet-4-5");
|
|
100
|
+
|
|
101
|
+
const port = Number.parseInt(process.env.PORT ?? "8080", 10);
|
|
102
|
+
const sessionDbPath =
|
|
103
|
+
process.env.SESSION_DB_PATH ?? path.join(process.cwd(), "data", "sessions.db");
|
|
104
|
+
const workspaceRoot = path.resolve(process.env.WORKSPACE_ROOT ?? process.cwd());
|
|
105
|
+
|
|
106
|
+
const defaultWebEndpoint = process.env.WEB_SEARCH_ENDPOINT ?? "http://localhost:8888/search";
|
|
107
|
+
let webEndpointHost = null;
|
|
108
|
+
try {
|
|
109
|
+
const { hostname } = new URL(defaultWebEndpoint);
|
|
110
|
+
webEndpointHost = hostname.toLowerCase();
|
|
111
|
+
} catch {
|
|
112
|
+
webEndpointHost = null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const allowAllWebHosts = process.env.WEB_SEARCH_ALLOW_ALL !== "false";
|
|
116
|
+
const configuredAllowedHosts =
|
|
117
|
+
process.env.WEB_SEARCH_ALLOWED_HOSTS?.split(",")
|
|
118
|
+
.map((host) => host.trim().toLowerCase())
|
|
119
|
+
.filter(Boolean) ?? [];
|
|
120
|
+
const webAllowedHosts = allowAllWebHosts
|
|
121
|
+
? null
|
|
122
|
+
: new Set([webEndpointHost, "localhost", "127.0.0.1"].filter(Boolean).concat(configuredAllowedHosts));
|
|
123
|
+
const webTimeoutMs = Number.parseInt(process.env.WEB_SEARCH_TIMEOUT_MS ?? "10000", 10);
|
|
124
|
+
|
|
125
|
+
const policyMaxSteps = Number.parseInt(process.env.POLICY_MAX_STEPS ?? "8", 10);
|
|
126
|
+
const policyMaxToolCalls = Number.parseInt(process.env.POLICY_MAX_TOOL_CALLS ?? "12", 10);
|
|
127
|
+
const policyDisallowedTools =
|
|
128
|
+
process.env.POLICY_DISALLOWED_TOOLS?.split(",")
|
|
129
|
+
.map((tool) => tool.trim())
|
|
130
|
+
.filter(Boolean) ?? [];
|
|
131
|
+
const policyGitAllowPush = process.env.POLICY_GIT_ALLOW_PUSH === "true";
|
|
132
|
+
const policyGitAllowPull = process.env.POLICY_GIT_ALLOW_PULL !== "false";
|
|
133
|
+
const policyGitAllowCommit = process.env.POLICY_GIT_ALLOW_COMMIT !== "false";
|
|
134
|
+
const policyGitTestCommand = process.env.POLICY_GIT_TEST_COMMAND ?? null;
|
|
135
|
+
const policyGitRequireTests = process.env.POLICY_GIT_REQUIRE_TESTS === "true";
|
|
136
|
+
const policyGitCommitRegex = process.env.POLICY_GIT_COMMIT_REGEX ?? null;
|
|
137
|
+
const policyGitAutoStash = process.env.POLICY_GIT_AUTOSTASH === "true";
|
|
138
|
+
|
|
139
|
+
const sandboxEnabled = process.env.MCP_SANDBOX_ENABLED !== "false";
|
|
140
|
+
const sandboxImage = process.env.MCP_SANDBOX_IMAGE ?? null;
|
|
141
|
+
const sandboxRuntime = process.env.MCP_SANDBOX_RUNTIME ?? "docker";
|
|
142
|
+
const sandboxContainerWorkspace =
|
|
143
|
+
process.env.MCP_SANDBOX_CONTAINER_WORKSPACE ?? "/workspace";
|
|
144
|
+
const sandboxMountWorkspace = process.env.MCP_SANDBOX_MOUNT_WORKSPACE !== "false";
|
|
145
|
+
const sandboxAllowNetworking = process.env.MCP_SANDBOX_ALLOW_NETWORKING === "true";
|
|
146
|
+
const sandboxNetworkMode = sandboxAllowNetworking
|
|
147
|
+
? process.env.MCP_SANDBOX_NETWORK_MODE ?? "bridge"
|
|
148
|
+
: "none";
|
|
149
|
+
const sandboxPassthroughEnv = parseList(
|
|
150
|
+
process.env.MCP_SANDBOX_PASSTHROUGH_ENV ?? "PATH,LANG,LC_ALL,TERM,HOME",
|
|
151
|
+
);
|
|
152
|
+
const sandboxExtraMounts = parseMountList(process.env.MCP_SANDBOX_EXTRA_MOUNTS ?? "");
|
|
153
|
+
const sandboxDefaultTimeoutMs = Number.parseInt(
|
|
154
|
+
process.env.MCP_SANDBOX_TIMEOUT_MS ?? "20000",
|
|
155
|
+
10,
|
|
156
|
+
);
|
|
157
|
+
const sandboxUser = process.env.MCP_SANDBOX_USER ?? null;
|
|
158
|
+
const sandboxEntrypoint = process.env.MCP_SANDBOX_ENTRYPOINT ?? null;
|
|
159
|
+
const sandboxReuseSessions = process.env.MCP_SANDBOX_REUSE_SESSION !== "false";
|
|
160
|
+
|
|
161
|
+
const sandboxPermissionMode =
|
|
162
|
+
(process.env.MCP_SANDBOX_PERMISSION_MODE ?? "auto").toLowerCase();
|
|
163
|
+
const sandboxPermissionAllow = parseList(process.env.MCP_SANDBOX_PERMISSION_ALLOW ?? "");
|
|
164
|
+
const sandboxPermissionDeny = parseList(process.env.MCP_SANDBOX_PERMISSION_DENY ?? "");
|
|
165
|
+
|
|
166
|
+
const sandboxManifestPath = resolveConfigPath(process.env.MCP_SERVER_MANIFEST ?? null);
|
|
167
|
+
|
|
168
|
+
let manifestDirList = null;
|
|
169
|
+
if (process.env.MCP_MANIFEST_DIRS === "") {
|
|
170
|
+
manifestDirList = [];
|
|
171
|
+
} else if (process.env.MCP_MANIFEST_DIRS) {
|
|
172
|
+
manifestDirList = parseList(process.env.MCP_MANIFEST_DIRS);
|
|
173
|
+
} else {
|
|
174
|
+
manifestDirList = ["~/.claude/mcp"];
|
|
175
|
+
}
|
|
176
|
+
const sandboxManifestDirs = manifestDirList
|
|
177
|
+
.map((dir) => resolveConfigPath(dir))
|
|
178
|
+
.filter((dir) => typeof dir === "string" && dir.length > 0);
|
|
179
|
+
|
|
180
|
+
const promptCacheEnabled = process.env.PROMPT_CACHE_ENABLED !== "false";
|
|
181
|
+
const promptCacheMaxEntriesRaw = Number.parseInt(
|
|
182
|
+
process.env.PROMPT_CACHE_MAX_ENTRIES ?? "64",
|
|
183
|
+
10,
|
|
184
|
+
);
|
|
185
|
+
const promptCacheTtlRaw = Number.parseInt(
|
|
186
|
+
process.env.PROMPT_CACHE_TTL_MS ?? "300000",
|
|
187
|
+
10,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const testDefaultCommand = process.env.WORKSPACE_TEST_COMMAND ?? null;
|
|
191
|
+
const testDefaultArgs = parseList(process.env.WORKSPACE_TEST_ARGS ?? "");
|
|
192
|
+
const testTimeoutMs = Number.parseInt(process.env.WORKSPACE_TEST_TIMEOUT_MS ?? "600000", 10);
|
|
193
|
+
const testSandboxMode = (process.env.WORKSPACE_TEST_SANDBOX ?? "auto").toLowerCase();
|
|
194
|
+
let testCoverageFiles = parseList(
|
|
195
|
+
process.env.WORKSPACE_TEST_COVERAGE_FILES ?? "coverage/coverage-summary.json",
|
|
196
|
+
);
|
|
197
|
+
if (testCoverageFiles.length === 0) {
|
|
198
|
+
testCoverageFiles = [];
|
|
199
|
+
}
|
|
200
|
+
const testProfiles = parseJson(process.env.WORKSPACE_TEST_PROFILES ?? "", null);
|
|
201
|
+
|
|
202
|
+
const config = {
|
|
203
|
+
env: process.env.NODE_ENV ?? "development",
|
|
204
|
+
port: Number.isNaN(port) ? 8080 : port,
|
|
205
|
+
databricks: {
|
|
206
|
+
baseUrl: rawBaseUrl,
|
|
207
|
+
apiKey,
|
|
208
|
+
endpointPath,
|
|
209
|
+
url: databricksUrl,
|
|
210
|
+
},
|
|
211
|
+
azureAnthropic: {
|
|
212
|
+
endpoint: azureAnthropicEndpoint,
|
|
213
|
+
apiKey: azureAnthropicApiKey,
|
|
214
|
+
version: azureAnthropicVersion,
|
|
215
|
+
},
|
|
216
|
+
modelProvider: {
|
|
217
|
+
type: modelProvider,
|
|
218
|
+
defaultModel,
|
|
219
|
+
},
|
|
220
|
+
server: {
|
|
221
|
+
jsonLimit: process.env.REQUEST_JSON_LIMIT ?? "1gb",
|
|
222
|
+
},
|
|
223
|
+
logger: {
|
|
224
|
+
level: process.env.LOG_LEVEL ?? "info",
|
|
225
|
+
},
|
|
226
|
+
sessionStore: {
|
|
227
|
+
dbPath: sessionDbPath,
|
|
228
|
+
},
|
|
229
|
+
workspace: {
|
|
230
|
+
root: workspaceRoot,
|
|
231
|
+
},
|
|
232
|
+
webSearch: {
|
|
233
|
+
endpoint: defaultWebEndpoint,
|
|
234
|
+
apiKey: process.env.WEB_SEARCH_API_KEY ?? null,
|
|
235
|
+
allowedHosts: allowAllWebHosts ? null : Array.from(webAllowedHosts ?? []),
|
|
236
|
+
allowAllHosts: allowAllWebHosts,
|
|
237
|
+
enabled: true,
|
|
238
|
+
timeoutMs: Number.isNaN(webTimeoutMs) ? 10000 : webTimeoutMs,
|
|
239
|
+
},
|
|
240
|
+
policy: {
|
|
241
|
+
maxStepsPerTurn: Number.isNaN(policyMaxSteps) ? 8 : policyMaxSteps,
|
|
242
|
+
maxToolCallsPerTurn: Number.isNaN(policyMaxToolCalls) ? 12 : policyMaxToolCalls,
|
|
243
|
+
disallowedTools: policyDisallowedTools,
|
|
244
|
+
git: {
|
|
245
|
+
allowPush: policyGitAllowPush,
|
|
246
|
+
allowPull: policyGitAllowPull,
|
|
247
|
+
allowCommit: policyGitAllowCommit,
|
|
248
|
+
testCommand: policyGitTestCommand,
|
|
249
|
+
requireTests: policyGitRequireTests,
|
|
250
|
+
commitMessageRegex: policyGitCommitRegex,
|
|
251
|
+
autoStash: policyGitAutoStash,
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
mcp: {
|
|
255
|
+
sandbox: {
|
|
256
|
+
enabled: sandboxEnabled && Boolean(sandboxImage),
|
|
257
|
+
runtime: sandboxRuntime,
|
|
258
|
+
image: sandboxImage,
|
|
259
|
+
containerWorkspace: sandboxContainerWorkspace,
|
|
260
|
+
mountWorkspace: sandboxMountWorkspace,
|
|
261
|
+
allowNetworking: sandboxAllowNetworking,
|
|
262
|
+
networkMode: sandboxNetworkMode,
|
|
263
|
+
passthroughEnv: sandboxPassthroughEnv,
|
|
264
|
+
extraMounts: sandboxExtraMounts,
|
|
265
|
+
defaultTimeoutMs: Number.isNaN(sandboxDefaultTimeoutMs)
|
|
266
|
+
? 20000
|
|
267
|
+
: sandboxDefaultTimeoutMs,
|
|
268
|
+
user: sandboxUser,
|
|
269
|
+
entrypoint: sandboxEntrypoint,
|
|
270
|
+
reuseSession: sandboxReuseSessions,
|
|
271
|
+
},
|
|
272
|
+
permissions: {
|
|
273
|
+
mode: ["auto", "require", "deny"].includes(sandboxPermissionMode)
|
|
274
|
+
? sandboxPermissionMode
|
|
275
|
+
: "auto",
|
|
276
|
+
allow: sandboxPermissionAllow,
|
|
277
|
+
deny: sandboxPermissionDeny,
|
|
278
|
+
},
|
|
279
|
+
servers: {
|
|
280
|
+
manifestPath: sandboxManifestPath,
|
|
281
|
+
manifestDirs: sandboxManifestDirs,
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
promptCache: {
|
|
285
|
+
enabled: promptCacheEnabled,
|
|
286
|
+
maxEntries: Number.isNaN(promptCacheMaxEntriesRaw) ? 64 : promptCacheMaxEntriesRaw,
|
|
287
|
+
ttlMs: Number.isNaN(promptCacheTtlRaw) ? 300000 : promptCacheTtlRaw,
|
|
288
|
+
},
|
|
289
|
+
tests: {
|
|
290
|
+
defaultCommand: testDefaultCommand ? testDefaultCommand.trim() : null,
|
|
291
|
+
defaultArgs: testDefaultArgs,
|
|
292
|
+
timeoutMs: Number.isNaN(testTimeoutMs) ? 600000 : testTimeoutMs,
|
|
293
|
+
sandbox: ["always", "never", "auto"].includes(testSandboxMode) ? testSandboxMode : "auto",
|
|
294
|
+
coverage: {
|
|
295
|
+
files: testCoverageFiles,
|
|
296
|
+
},
|
|
297
|
+
profiles: Array.isArray(testProfiles) ? testProfiles : null,
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
module.exports = config;
|