neural-loom 0.2.70 → 0.3.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/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +3 -3
- package/.next/cache/.previewinfo +1 -1
- package/.next/cache/.rscinfo +1 -1
- package/.next/cache/.tsbuildinfo +1 -1
- package/.next/diagnostics/route-bundle-stats.json +11 -11
- package/.next/fallback-build-manifest.json +3 -3
- package/.next/next-server.js.nft.json +1 -1
- package/.next/prerender-manifest.json +3 -3
- package/.next/required-server-files.js +20 -20
- package/.next/required-server-files.json +20 -20
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +1 -1
- package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/api/configs/route.js.nft.json +1 -1
- package/.next/server/app/api/context/aider/route.js.nft.json +1 -1
- package/.next/server/app/api/context/claude/route.js.nft.json +1 -1
- package/.next/server/app/api/context/route.js.nft.json +1 -1
- package/.next/server/app/api/context/ssh/route.js.nft.json +1 -1
- package/.next/server/app/api/files/route.js +1 -1
- package/.next/server/app/api/files/route.js.nft.json +1 -1
- package/.next/server/app/api/recents/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/inject/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/input/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/launch/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/scope/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/start/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/stats/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/stop/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/upload-image/route.js.nft.json +1 -1
- package/.next/server/app/api/sql/connections/route.js.nft.json +1 -1
- package/.next/server/app/api/sql/mcp/route.js +1 -1
- package/.next/server/app/api/sql/mcp/route.js.nft.json +1 -1
- package/.next/server/app/api/sql/query/route.js +1 -1
- package/.next/server/app/api/sql/query/route.js.nft.json +1 -1
- package/.next/server/app/api/sql/schema/route.js.nft.json +1 -1
- package/.next/server/app/api/telemetry/route.js +1 -1
- package/.next/server/app/api/telemetry/route.js.nft.json +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +2 -2
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/index.segments/_full.segment.rsc +2 -2
- package/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/page/react-loadable-manifest.json +2 -2
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__0.hmw1f._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0.hmw1f._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__00-sqou._.js +2 -2
- package/.next/server/chunks/[root-of-the-server]__00-sqou._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__00l5btc._.js +4 -4
- package/.next/server/chunks/[root-of-the-server]__00l5btc._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__06men8g._.js +4 -4
- package/.next/server/chunks/[root-of-the-server]__06men8g._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__097vtn1._.js +2 -2
- package/.next/server/chunks/[root-of-the-server]__097vtn1._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__0_v2.a8._.js +2 -2
- package/.next/server/chunks/[root-of-the-server]__0_v2.a8._.js.map +1 -1
- package/.next/server/chunks/{[root-of-the-server]__0.ptwor._.js → [root-of-the-server]__0ajln56._.js} +7 -7
- package/.next/server/chunks/[root-of-the-server]__0ajln56._.js.map +1 -0
- package/.next/server/chunks/{[root-of-the-server]__0co.g5k._.js → [root-of-the-server]__0auh-2z._.js} +5 -5
- package/.next/server/chunks/[root-of-the-server]__0auh-2z._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__0e03ite._.js +2 -2
- package/.next/server/chunks/[root-of-the-server]__0e03ite._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__0fxtx7z._.js +2 -2
- package/.next/server/chunks/[root-of-the-server]__0fxtx7z._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__0h.~hv6._.js +2 -2
- package/.next/server/chunks/[root-of-the-server]__0h.~hv6._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__0i83~95._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__0li1h1u._.js +19 -19
- package/.next/server/chunks/[root-of-the-server]__0li1h1u._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__0s1axek._.js +5 -5
- package/.next/server/chunks/[root-of-the-server]__0s1axek._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__0t713sv._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__0t713sv._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__0tq0maq._.js +5 -5
- package/.next/server/chunks/[root-of-the-server]__0tq0maq._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__0viil6b._.js +2 -2
- package/.next/server/chunks/[root-of-the-server]__0viil6b._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__0ykd4~u._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0ykd4~u._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__11h_nxt._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__11h_nxt._.js.map +1 -1
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_016qk~0.js +2 -2
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_016qk~0.js.map +1 -1
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0ev3h.z.js +1 -1
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0ev3h.z.js.map +1 -1
- package/.next/server/edge/chunks/[root-of-the-server]__08ca2jv._.js +11 -0
- package/.next/server/edge/chunks/[root-of-the-server]__08ca2jv._.js.map +1 -0
- package/.next/server/edge/chunks/node_modules_next_dist_esm_build_templates_edge-wrapper_0kvehva.js +3 -0
- package/.next/server/edge/chunks/node_modules_next_dist_esm_build_templates_edge-wrapper_0kvehva.js.map +1 -0
- package/.next/server/edge/chunks/node_modules_next_dist_esm_build_templates_edge-wrapper_0lcpsxv.js.map +1 -0
- package/.next/server/edge/chunks/turbopack-node_modules_next_dist_esm_build_templates_edge-wrapper_0lcpsxv.js +3 -0
- package/.next/server/middleware/middleware-manifest.json +32 -0
- package/.next/server/middleware-build-manifest.js +3 -3
- package/.next/server/middleware-manifest.json +30 -2
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/04j4ht7eCOOXSLQ5Wixq0/_clientMiddlewareManifest.js +6 -0
- package/.next/static/chunks/{0fxouf2n_5adf.js → 0uyuco-451j.n.js} +2 -2
- package/.next/static/chunks/{0r3x2gcn0sde..js → 14~-ojwsxf~oe.js} +4 -4
- package/.next/trace +1 -1
- package/.next/trace-build +1 -1
- package/README.md +18 -0
- package/bin/cli.js +12 -2
- package/package.json +9 -6
- package/src/app/api/files/route.ts +30 -0
- package/src/app/api/git/route.ts +46 -31
- package/src/app/api/sessions/stats/route.ts +40 -22
- package/src/app/api/sql/query/route.ts +2 -15
- package/src/app/api/telemetry/route.ts +2 -1
- package/src/app/components/TerminalConsole.tsx +103 -39
- package/src/lib/agents/AgentRunner.ts +41 -5
- package/src/lib/agents/AiderRunner.ts +20 -36
- package/src/lib/agents/ClaudeRunner.ts +8 -62
- package/src/lib/agents/DockerRunner.ts +18 -54
- package/src/lib/agents/MockRunner.ts +3 -1
- package/src/lib/agents/SSHRunner.ts +8 -54
- package/src/lib/agents/SessionPersister.ts +11 -1
- package/src/lib/agents/ShellRunner.ts +10 -46
- package/src/lib/agents/WebSocketServer.ts +17 -2
- package/src/lib/agents/pricing.ts +45 -0
- package/src/lib/agents/runtime.ts +92 -0
- package/src/lib/gitSafe.ts +15 -0
- package/src/lib/security.ts +76 -0
- package/src/lib/sql/McpServer.ts +2 -11
- package/src/lib/sql/adapters/MssqlAdapter.ts +16 -1
- package/src/lib/sql/adapters/MysqlAdapter.ts +23 -1
- package/src/lib/sql/adapters/PostgresAdapter.ts +22 -1
- package/src/lib/sql/adapters/SqliteAdapter.ts +21 -10
- package/src/lib/sql/sqlGuard.ts +20 -0
- package/src/lib/workspaceParser.ts +23 -6
- package/src/middleware.ts +27 -0
- package/.next/dev/build-manifest.json +0 -50
- package/.next/dev/fallback-build-manifest.json +0 -37
- package/.next/dev/logs/next-development.log +0 -3737
- package/.next/dev/server/chunks/[root-of-the-server]__11uf~fd._.js +0 -2396
- package/.next/dev/server/chunks/[root-of-the-server]__11uf~fd._.js.map +0 -20
- package/.next/dev/server/chunks/ssr/[externals]__0.o~_~l._.js +0 -32
- package/.next/dev/server/chunks/ssr/[externals]__0.o~_~l._.js.map +0 -5
- package/.next/dev/server/chunks/ssr/[externals]_next_dist_shared_lib_no-fallback-error_external_0fgm~bn.js +0 -8
- package/.next/dev/server/chunks/ssr/[externals]_next_dist_shared_lib_no-fallback-error_external_0fgm~bn.js.map +0 -5
- package/.next/dev/server/chunks/ssr/[root-of-the-server]__0je5zm2._.js +0 -285
- package/.next/dev/server/chunks/ssr/[root-of-the-server]__0je5zm2._.js.map +0 -9
- package/.next/dev/server/chunks/ssr/node_modules_01ed9g5._.js +0 -6621
- package/.next/dev/server/chunks/ssr/node_modules_01ed9g5._.js.map +0 -76
- package/.next/dev/server/chunks/ssr/node_modules_129w7gn._.js +0 -4399
- package/.next/dev/server/chunks/ssr/node_modules_129w7gn._.js.map +0 -35
- package/.next/dev/server/chunks/ssr/node_modules_next_12i4inu._.js +0 -55
- package/.next/dev/server/chunks/ssr/node_modules_next_12i4inu._.js.map +0 -7
- package/.next/dev/server/middleware-build-manifest.js +0 -50
- package/.next/dev/server/next-font-manifest.js +0 -1
- package/.next/dev/server/next-font-manifest.json +0 -11
- package/.next/dev/server/pages/_app/build-manifest.json +0 -23
- package/.next/dev/server/pages/_app/client-build-manifest.json +0 -5
- package/.next/dev/server/pages/_app/next-font-manifest.json +0 -6
- package/.next/dev/server/pages/_app/pages-manifest.json +0 -3
- package/.next/dev/server/pages/_app/react-loadable-manifest.json +0 -1
- package/.next/dev/server/pages/_app.js +0 -4
- package/.next/dev/server/pages/_app.js.map +0 -5
- package/.next/dev/server/pages/_document/next-font-manifest.json +0 -6
- package/.next/dev/server/pages/_document/pages-manifest.json +0 -3
- package/.next/dev/server/pages/_document/react-loadable-manifest.json +0 -1
- package/.next/dev/server/pages/_document.js +0 -5
- package/.next/dev/server/pages/_document.js.map +0 -5
- package/.next/dev/server/pages/_error/build-manifest.json +0 -22
- package/.next/dev/server/pages/_error/client-build-manifest.json +0 -5
- package/.next/dev/server/pages/_error/next-font-manifest.json +0 -6
- package/.next/dev/server/pages/_error/pages-manifest.json +0 -3
- package/.next/dev/server/pages/_error/react-loadable-manifest.json +0 -1
- package/.next/dev/server/pages/_error.js +0 -8
- package/.next/dev/server/pages/_error.js.map +0 -5
- package/.next/dev/server/pages-manifest.json +0 -5
- package/.next/dev/static/chunks/[next]_entry_page-loader_ts_0j~flwh._.js +0 -23
- package/.next/dev/static/chunks/[next]_entry_page-loader_ts_0j~flwh._.js.map +0 -6
- package/.next/dev/static/chunks/[next]_entry_page-loader_ts_0rqw6yo._.js +0 -23
- package/.next/dev/static/chunks/[next]_entry_page-loader_ts_0rqw6yo._.js.map +0 -6
- package/.next/dev/static/chunks/[root-of-the-server]__01mw43t._.js +0 -476
- package/.next/dev/static/chunks/[root-of-the-server]__01mw43t._.js.map +0 -6
- package/.next/dev/static/chunks/[root-of-the-server]__0c0okpg._.js +0 -476
- package/.next/dev/static/chunks/[root-of-the-server]__0c0okpg._.js.map +0 -6
- package/.next/dev/static/chunks/node_modules_0lx093h._.js +0 -1343
- package/.next/dev/static/chunks/node_modules_0lx093h._.js.map +0 -13
- package/.next/dev/static/chunks/node_modules_next_app_0jt-zj..js +0 -8
- package/.next/dev/static/chunks/node_modules_next_app_0jt-zj..js.map +0 -6
- package/.next/dev/static/chunks/node_modules_next_dist_0rt-2cr._.js +0 -2334
- package/.next/dev/static/chunks/node_modules_next_dist_0rt-2cr._.js.map +0 -26
- package/.next/dev/static/chunks/node_modules_next_dist_0u_w_5s._.js +0 -2165
- package/.next/dev/static/chunks/node_modules_next_dist_0u_w_5s._.js.map +0 -25
- package/.next/dev/static/chunks/node_modules_next_dist_client_0pe1dg-._.js +0 -4091
- package/.next/dev/static/chunks/node_modules_next_dist_client_0pe1dg-._.js.map +0 -49
- package/.next/dev/static/chunks/node_modules_next_dist_compiled_0o6l_m6._.js +0 -6064
- package/.next/dev/static/chunks/node_modules_next_dist_compiled_0o6l_m6._.js.map +0 -17
- package/.next/dev/static/chunks/node_modules_next_dist_shared_lib_0ek5eq7._.js +0 -6535
- package/.next/dev/static/chunks/node_modules_next_dist_shared_lib_0ek5eq7._.js.map +0 -72
- package/.next/dev/static/chunks/node_modules_next_dist_shared_lib_0n1wg2c._.js +0 -6294
- package/.next/dev/static/chunks/node_modules_next_dist_shared_lib_0n1wg2c._.js.map +0 -70
- package/.next/dev/static/chunks/node_modules_react-dom_0bruynb._.js +0 -15381
- package/.next/dev/static/chunks/node_modules_react-dom_0bruynb._.js.map +0 -9
- package/.next/dev/static/chunks/pages/_app.js +0 -13
- package/.next/dev/static/chunks/pages/_error.js +0 -12
- package/.next/dev/static/chunks/pages__app_0-bg2z-._.js.map +0 -11
- package/.next/dev/static/chunks/pages__app_07xvfw~._.js +0 -5
- package/.next/dev/static/chunks/pages__error_0.i_zsq._.js.map +0 -11
- package/.next/dev/static/chunks/pages__error_07xvfw~._.js +0 -5
- package/.next/dev/static/chunks/turbopack-pages__app_0-bg2z-._.js +0 -2236
- package/.next/dev/static/chunks/turbopack-pages__error_0.i_zsq._.js +0 -2236
- package/.next/server/chunks/[externals]__048y_op._.js +0 -3
- package/.next/server/chunks/[externals]__048y_op._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__0.ptwor._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__08r0s-s._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__08r0s-s._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__0co.g5k._.js.map +0 -1
- package/.next/static/llmJsGQafolZV5_sqEw4l/_clientMiddlewareManifest.js +0 -1
- /package/.next/static/{llmJsGQafolZV5_sqEw4l → 04j4ht7eCOOXSLQ5Wixq0}/_buildManifest.js +0 -0
- /package/.next/static/{llmJsGQafolZV5_sqEw4l → 04j4ht7eCOOXSLQ5Wixq0}/_ssgManifest.js +0 -0
|
@@ -3,29 +3,7 @@ import path from "path";
|
|
|
3
3
|
import { getConfigDir } from "../config";
|
|
4
4
|
import type { IPty } from "node-pty";
|
|
5
5
|
import { AgentRunner } from "./AgentRunner";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
function isWindows(): boolean {
|
|
9
|
-
return typeof process !== "undefined" && process.platform === "win32";
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function findGitBash(): string | null {
|
|
13
|
-
if (!isWindows()) {
|
|
14
|
-
return "bash";
|
|
15
|
-
}
|
|
16
|
-
const pf = process.env.PROGRAMFILES || "C:\\Program Files";
|
|
17
|
-
const pf86 = process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)";
|
|
18
|
-
const local = process.env.LOCALAPPDATA || "";
|
|
19
|
-
|
|
20
|
-
const candidates: string[] = [
|
|
21
|
-
process.env.WINGMAN_BASH_PATH,
|
|
22
|
-
path.join(pf, "Git", "bin", "bash.exe"),
|
|
23
|
-
path.join(pf86, "Git", "bin", "bash.exe"),
|
|
24
|
-
local ? path.join(local, "Programs", "Git", "bin", "bash.exe") : "",
|
|
25
|
-
].filter((p): p is string => typeof p === "string" && p !== "");
|
|
26
|
-
|
|
27
|
-
return candidates.find(p => fs.existsSync(p)) || null;
|
|
28
|
-
}
|
|
6
|
+
import { findGitBash, loadNodePty, loadClaudeExtraFlags } from "./runtime";
|
|
29
7
|
|
|
30
8
|
interface SSHConfig {
|
|
31
9
|
host: string;
|
|
@@ -52,16 +30,6 @@ export class SSHRunner extends AgentRunner {
|
|
|
52
30
|
this.createdAt = new Date().toISOString();
|
|
53
31
|
}
|
|
54
32
|
|
|
55
|
-
private appendLog(line: string) {
|
|
56
|
-
this.logs.push(line);
|
|
57
|
-
if (this.logs.length > 5000) {
|
|
58
|
-
this.logs.shift();
|
|
59
|
-
}
|
|
60
|
-
this.parseTokenUsage(line);
|
|
61
|
-
this.emitOutput(line);
|
|
62
|
-
SessionPersister.saveSession(this);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
33
|
async start(resume?: boolean): Promise<void> {
|
|
66
34
|
if (this.status === 'running') return;
|
|
67
35
|
|
|
@@ -110,32 +78,14 @@ export class SSHRunner extends AgentRunner {
|
|
|
110
78
|
return;
|
|
111
79
|
}
|
|
112
80
|
|
|
113
|
-
|
|
114
|
-
this.ptyModule = eval("require")("node-pty");
|
|
115
|
-
}
|
|
81
|
+
this.ptyModule = loadNodePty();
|
|
116
82
|
|
|
117
83
|
this.appendLog(`[System] Connecting to ${config.username}@${config.host}...\r\n`);
|
|
118
84
|
|
|
119
85
|
const portOpt = config.port ? `-p ${config.port}` : "";
|
|
120
86
|
const keyOpt = config.privateKeyPath ? `-i "${config.privateKeyPath}"` : "";
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
let extraFlags = "";
|
|
124
|
-
if (resume) {
|
|
125
|
-
extraFlags = "--resume";
|
|
126
|
-
} else {
|
|
127
|
-
const claudeConfigPath = path.join(getConfigDir(), "claude.json");
|
|
128
|
-
if (fs.existsSync(claudeConfigPath)) {
|
|
129
|
-
try {
|
|
130
|
-
const config = JSON.parse(fs.readFileSync(claudeConfigPath, "utf8"));
|
|
131
|
-
if (config.extraFlags) {
|
|
132
|
-
extraFlags = config.extraFlags;
|
|
133
|
-
}
|
|
134
|
-
} catch (err) {
|
|
135
|
-
console.error("Failed to parse Claude configuration:", err);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
87
|
+
|
|
88
|
+
const extraFlags = loadClaudeExtraFlags(resume);
|
|
139
89
|
|
|
140
90
|
const sshCmd = `ssh -t ${keyOpt} ${portOpt} ${config.username}@${config.host} "claude ${extraFlags}"`;
|
|
141
91
|
|
|
@@ -200,5 +150,9 @@ export class SSHRunner extends AgentRunner {
|
|
|
200
150
|
}
|
|
201
151
|
}
|
|
202
152
|
}
|
|
153
|
+
|
|
154
|
+
override getPid(): number | undefined {
|
|
155
|
+
return this.ptyProcess?.pid;
|
|
156
|
+
}
|
|
203
157
|
}
|
|
204
158
|
|
|
@@ -75,10 +75,20 @@ export class SessionPersister {
|
|
|
75
75
|
try {
|
|
76
76
|
const dir = this.getSessionsDir();
|
|
77
77
|
const filePath = path.join(dir, `${id}.json`);
|
|
78
|
-
|
|
78
|
+
// Write to a temp file then rename, so a crash mid-write can't leave a
|
|
79
|
+
// truncated/corrupt session file (rename is atomic on the same filesystem).
|
|
80
|
+
const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
81
|
+
fs.writeFile(tmpPath, JSON.stringify(data, null, 2), "utf8", (err) => {
|
|
79
82
|
if (err) {
|
|
80
83
|
console.error(`[SessionPersister] Failed to save session ${id} asynchronously:`, err);
|
|
84
|
+
return;
|
|
81
85
|
}
|
|
86
|
+
fs.rename(tmpPath, filePath, (renameErr) => {
|
|
87
|
+
if (renameErr) {
|
|
88
|
+
console.error(`[SessionPersister] Failed to finalize session ${id}:`, renameErr);
|
|
89
|
+
fs.unlink(tmpPath, () => {});
|
|
90
|
+
}
|
|
91
|
+
});
|
|
82
92
|
});
|
|
83
93
|
} catch (err) {
|
|
84
94
|
console.error(`[SessionPersister] Failed to save session ${id}:`, err);
|
|
@@ -1,40 +1,7 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
2
|
import type { IPty } from "node-pty";
|
|
4
3
|
import { AgentRunner } from "./AgentRunner";
|
|
5
|
-
|
|
6
|
-
function isWindows(): boolean {
|
|
7
|
-
return typeof process !== "undefined" && process.platform === "win32";
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function findGitBash(): string | null {
|
|
11
|
-
if (!isWindows()) {
|
|
12
|
-
return "bash";
|
|
13
|
-
}
|
|
14
|
-
const pf = process.env.PROGRAMFILES || "C:\\Program Files";
|
|
15
|
-
const pf86 = process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)";
|
|
16
|
-
const local = process.env.LOCALAPPDATA || "";
|
|
17
|
-
|
|
18
|
-
const candidates: string[] = [
|
|
19
|
-
process.env.WINGMAN_BASH_PATH,
|
|
20
|
-
path.join(pf, "Git", "bin", "bash.exe"),
|
|
21
|
-
path.join(pf86, "Git", "bin", "bash.exe"),
|
|
22
|
-
local ? path.join(local, "Programs", "Git", "bin", "bash.exe") : "",
|
|
23
|
-
].filter((p): p is string => typeof p === "string" && p !== "");
|
|
24
|
-
|
|
25
|
-
const found = candidates.find(p => fs.existsSync(p));
|
|
26
|
-
if (found) return found;
|
|
27
|
-
|
|
28
|
-
// Fallback to PowerShell or Cmd on Windows
|
|
29
|
-
const windir = process.env.windir || "C:\\Windows";
|
|
30
|
-
const powershell = path.join(windir, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
31
|
-
if (fs.existsSync(powershell)) return powershell;
|
|
32
|
-
|
|
33
|
-
const cmd = path.join(windir, "System32", "cmd.exe");
|
|
34
|
-
if (fs.existsSync(cmd)) return cmd;
|
|
35
|
-
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
4
|
+
import { isWindows, findInteractiveShell, loadNodePty } from "./runtime";
|
|
38
5
|
|
|
39
6
|
export class ShellRunner extends AgentRunner {
|
|
40
7
|
id: string;
|
|
@@ -53,20 +20,15 @@ export class ShellRunner extends AgentRunner {
|
|
|
53
20
|
this.name = name || "Workspace Shell";
|
|
54
21
|
this.workspaceRoot = workspaceRoot;
|
|
55
22
|
this.createdAt = new Date().toISOString();
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
this.logs.push(line);
|
|
60
|
-
if (this.logs.length > 5000) {
|
|
61
|
-
this.logs.shift();
|
|
62
|
-
}
|
|
63
|
-
this.emitOutput(line);
|
|
23
|
+
// The workspace shell is ephemeral: don't parse tokens or persist its logs.
|
|
24
|
+
this.parseTokensOnLog = false;
|
|
25
|
+
this.persistOnLog = false;
|
|
64
26
|
}
|
|
65
27
|
|
|
66
28
|
async start(resume?: boolean): Promise<void> {
|
|
67
29
|
if (this.status === 'running') return;
|
|
68
30
|
|
|
69
|
-
const shellPath =
|
|
31
|
+
const shellPath = findInteractiveShell();
|
|
70
32
|
if (!shellPath) {
|
|
71
33
|
this.status = 'stopped';
|
|
72
34
|
this.appendLog("[System Error] Shell executable (Git Bash/PowerShell) not found.\r\n");
|
|
@@ -74,9 +36,7 @@ export class ShellRunner extends AgentRunner {
|
|
|
74
36
|
}
|
|
75
37
|
|
|
76
38
|
try {
|
|
77
|
-
|
|
78
|
-
this.ptyModule = eval("require")("node-pty");
|
|
79
|
-
}
|
|
39
|
+
this.ptyModule = loadNodePty();
|
|
80
40
|
|
|
81
41
|
this.status = 'running';
|
|
82
42
|
this.appendLog("\x1b[1;36m[System] Launching workspace terminal interactive shell...\x1b[0m\r\n");
|
|
@@ -149,5 +109,9 @@ export class ShellRunner extends AgentRunner {
|
|
|
149
109
|
}
|
|
150
110
|
}
|
|
151
111
|
}
|
|
112
|
+
|
|
113
|
+
override getPid(): number | undefined {
|
|
114
|
+
return this.ptyProcess?.pid;
|
|
115
|
+
}
|
|
152
116
|
}
|
|
153
117
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { WebSocketServer, WebSocket } from "ws";
|
|
2
2
|
import { AgentRunner } from "./AgentRunner";
|
|
3
|
+
import { getBindHost, isRequestAllowed } from "../security";
|
|
3
4
|
|
|
4
5
|
declare global {
|
|
5
6
|
var wss: WebSocketServer | undefined;
|
|
@@ -20,12 +21,26 @@ export function initWebSocketServer(port = 9601, getSession?: (id: string) => Ag
|
|
|
20
21
|
|
|
21
22
|
let currentPort = port;
|
|
22
23
|
const maxPort = port + 10;
|
|
24
|
+
const bindHost = getBindHost();
|
|
23
25
|
|
|
24
26
|
function tryBind() {
|
|
25
|
-
console.log(`[WebSocketServer] Launching NeuralLoom WebSocket Gateway on
|
|
27
|
+
console.log(`[WebSocketServer] Launching NeuralLoom WebSocket Gateway on ${bindHost}:${currentPort}...`);
|
|
26
28
|
|
|
27
29
|
try {
|
|
28
|
-
const wss = new WebSocketServer({
|
|
30
|
+
const wss = new WebSocketServer({
|
|
31
|
+
port: currentPort,
|
|
32
|
+
host: bindHost,
|
|
33
|
+
// Reject cross-origin / non-loopback handshakes (mirrors the HTTP middleware).
|
|
34
|
+
verifyClient: (info: { origin?: string; req: { headers: Record<string, string | string[] | undefined> } }) => {
|
|
35
|
+
const hostHeader = (info.req.headers.host as string) || null;
|
|
36
|
+
const originHeader = info.origin || (info.req.headers.origin as string) || null;
|
|
37
|
+
const allowed = isRequestAllowed(hostHeader, originHeader);
|
|
38
|
+
if (!allowed) {
|
|
39
|
+
console.warn(`[WebSocketServer] Rejected gateway connection (origin=${originHeader ?? "none"}, host=${hostHeader ?? "none"}).`);
|
|
40
|
+
}
|
|
41
|
+
return allowed;
|
|
42
|
+
},
|
|
43
|
+
});
|
|
29
44
|
|
|
30
45
|
wss.on("error", (err: unknown) => {
|
|
31
46
|
const socketErr = err as { code?: string };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-keyed pricing used to estimate session cost when the agent's own output
|
|
3
|
+
* doesn't report a dollar figure. Rates are USD per token (list price / 1e6).
|
|
4
|
+
* These are approximate and should be updated as provider pricing changes.
|
|
5
|
+
*/
|
|
6
|
+
export interface ModelRate {
|
|
7
|
+
input: number;
|
|
8
|
+
output: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const M = 1_000_000;
|
|
12
|
+
|
|
13
|
+
const MODEL_RATES: Record<string, ModelRate> = {
|
|
14
|
+
// Anthropic Claude (per 1M tokens: input / output)
|
|
15
|
+
"claude-opus-4": { input: 15 / M, output: 75 / M },
|
|
16
|
+
"claude-sonnet-4": { input: 3 / M, output: 15 / M },
|
|
17
|
+
"claude-haiku-4": { input: 1 / M, output: 5 / M },
|
|
18
|
+
"claude-3-5-sonnet": { input: 3 / M, output: 15 / M },
|
|
19
|
+
"claude-3-5-haiku": { input: 0.8 / M, output: 4 / M },
|
|
20
|
+
"claude-3-opus": { input: 15 / M, output: 75 / M },
|
|
21
|
+
// OpenAI (for Aider users)
|
|
22
|
+
"gpt-4o": { input: 2.5 / M, output: 10 / M },
|
|
23
|
+
"gpt-4o-mini": { input: 0.15 / M, output: 0.6 / M },
|
|
24
|
+
"gpt-4-turbo": { input: 10 / M, output: 30 / M },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Default when the model is unknown — a mid-tier Claude rate. */
|
|
28
|
+
export const DEFAULT_MODEL = "claude-sonnet-4";
|
|
29
|
+
|
|
30
|
+
export function getModelRate(model?: string): ModelRate {
|
|
31
|
+
if (model) {
|
|
32
|
+
const key = model.toLowerCase();
|
|
33
|
+
if (MODEL_RATES[key]) return MODEL_RATES[key];
|
|
34
|
+
// Substring match so e.g. "claude-sonnet-4-6" or "anthropic/claude-3-5-sonnet" resolve.
|
|
35
|
+
for (const k of Object.keys(MODEL_RATES)) {
|
|
36
|
+
if (key.includes(k)) return MODEL_RATES[k];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return MODEL_RATES[DEFAULT_MODEL];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function estimateCost(inputTokens: number, outputTokens: number, model?: string): number {
|
|
43
|
+
const rate = getModelRate(model);
|
|
44
|
+
return inputTokens * rate.input + outputTokens * rate.output;
|
|
45
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import { getConfigDir } from "../config";
|
|
5
|
+
|
|
6
|
+
/** Shared runtime helpers used by the PTY-backed runners. */
|
|
7
|
+
|
|
8
|
+
export function isWindows(): boolean {
|
|
9
|
+
return typeof process !== "undefined" && process.platform === "win32";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Locate a Git Bash executable. Returns "bash" on non-Windows. On Windows,
|
|
14
|
+
* probes the usual install locations and returns null if none are found.
|
|
15
|
+
*/
|
|
16
|
+
export function findGitBash(): string | null {
|
|
17
|
+
if (!isWindows()) {
|
|
18
|
+
return "bash";
|
|
19
|
+
}
|
|
20
|
+
const pf = process.env.PROGRAMFILES || "C:\\Program Files";
|
|
21
|
+
const pf86 = process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)";
|
|
22
|
+
const local = process.env.LOCALAPPDATA || "";
|
|
23
|
+
|
|
24
|
+
const candidates: string[] = [
|
|
25
|
+
process.env.WINGMAN_BASH_PATH,
|
|
26
|
+
path.join(pf, "Git", "bin", "bash.exe"),
|
|
27
|
+
path.join(pf86, "Git", "bin", "bash.exe"),
|
|
28
|
+
local ? path.join(local, "Programs", "Git", "bin", "bash.exe") : "",
|
|
29
|
+
].filter((p): p is string => typeof p === "string" && p !== "");
|
|
30
|
+
|
|
31
|
+
return candidates.find((p) => fs.existsSync(p)) || null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Like findGitBash, but for an interactive workspace shell: on Windows, falls
|
|
36
|
+
* back to PowerShell and then cmd.exe when Git Bash is unavailable.
|
|
37
|
+
*/
|
|
38
|
+
export function findInteractiveShell(): string | null {
|
|
39
|
+
const bash = findGitBash();
|
|
40
|
+
if (bash) return bash;
|
|
41
|
+
if (!isWindows()) return null;
|
|
42
|
+
|
|
43
|
+
const windir = process.env.windir || "C:\\Windows";
|
|
44
|
+
const powershell = path.join(windir, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
45
|
+
if (fs.existsSync(powershell)) return powershell;
|
|
46
|
+
|
|
47
|
+
const cmd = path.join(windir, "System32", "cmd.exe");
|
|
48
|
+
if (fs.existsSync(cmd)) return cmd;
|
|
49
|
+
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let cachedPty: typeof import("node-pty") | null = null;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Load node-pty at runtime. Uses an indirected require so the Next/Turbopack
|
|
57
|
+
* bundler does not try to statically analyse the native module (it is also
|
|
58
|
+
* declared in serverExternalPackages). Cached after first load.
|
|
59
|
+
*/
|
|
60
|
+
export function loadNodePty(): typeof import("node-pty") {
|
|
61
|
+
if (!cachedPty) {
|
|
62
|
+
cachedPty = eval("require")("node-pty") as typeof import("node-pty");
|
|
63
|
+
}
|
|
64
|
+
return cachedPty;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Resolve extra CLI flags for the `claude` command. `--resume` when resuming,
|
|
69
|
+
* otherwise the `extraFlags` field from the user's claude.json (if any).
|
|
70
|
+
*/
|
|
71
|
+
export function loadClaudeExtraFlags(resume?: boolean): string {
|
|
72
|
+
if (resume) return "--resume";
|
|
73
|
+
const claudeConfigPath = path.join(getConfigDir(), "claude.json");
|
|
74
|
+
if (fs.existsSync(claudeConfigPath)) {
|
|
75
|
+
try {
|
|
76
|
+
const config = JSON.parse(fs.readFileSync(claudeConfigPath, "utf8"));
|
|
77
|
+
if (config.extraFlags) return config.extraFlags;
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.error("Failed to parse Claude configuration:", err);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return "";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Build the Windows PATH override (prepends VS Node + global npm). */
|
|
86
|
+
export function windowsPathOverride(): string {
|
|
87
|
+
const vsNode =
|
|
88
|
+
"C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\MSBuild\\Microsoft\\VisualStudio\\NodeJs";
|
|
89
|
+
const localNpm = path.join(os.homedir(), "AppData", "Local", "npm");
|
|
90
|
+
const existingPath = process.env.Path || process.env.PATH || "";
|
|
91
|
+
return [vsNode, localNpm, existingPath].filter(Boolean).join(";");
|
|
92
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate a git ref name before passing it to `git`. Even with execFile (no
|
|
3
|
+
* shell), a value starting with `-` could be misread as an option, so we reject
|
|
4
|
+
* those along with git-illegal characters and `..` sequences.
|
|
5
|
+
*/
|
|
6
|
+
export function isValidBranchName(name: unknown): name is string {
|
|
7
|
+
return (
|
|
8
|
+
typeof name === "string" &&
|
|
9
|
+
name.length > 0 &&
|
|
10
|
+
name.length <= 255 &&
|
|
11
|
+
!name.startsWith("-") &&
|
|
12
|
+
!name.includes("..") &&
|
|
13
|
+
/^[A-Za-z0-9._\/-]+$/.test(name)
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared request-origin guard for NeuralLoom.
|
|
3
|
+
*
|
|
4
|
+
* Contains NO Node-only imports so it can run in the Edge middleware runtime,
|
|
5
|
+
* in Node API routes, and in the raw `ws` gateway alike.
|
|
6
|
+
*
|
|
7
|
+
* Default posture is "localhost only": the dashboard, file/git/SQL APIs and the
|
|
8
|
+
* terminal gateway all expose powerful local capabilities (arbitrary shell,
|
|
9
|
+
* filesystem and DB access), so by default we only accept requests whose Host
|
|
10
|
+
* and Origin resolve to the loopback interface. This blocks LAN peers and
|
|
11
|
+
* defends against DNS-rebinding (where a malicious page carries the attacker's
|
|
12
|
+
* domain in Host/Origin while resolving to 127.0.0.1).
|
|
13
|
+
*
|
|
14
|
+
* Set NEURAL_LOOM_ALLOW_LAN=1 to expose the tool on the network. Even then we
|
|
15
|
+
* reject cross-site requests (Origin hostname must match the Host hostname) as a
|
|
16
|
+
* CSRF defense.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const LOCAL_HOSTNAMES = new Set(["localhost", "127.0.0.1", "::1", "0.0.0.0"]);
|
|
20
|
+
|
|
21
|
+
/** Extract the bare hostname from a Host header or Origin URL (no scheme/port). */
|
|
22
|
+
export function hostnameOf(value: string | null | undefined): string | null {
|
|
23
|
+
if (!value) return null;
|
|
24
|
+
let v = value.trim();
|
|
25
|
+
if (!v) return null;
|
|
26
|
+
// Strip scheme (Origin headers look like "http://host:port")
|
|
27
|
+
v = v.replace(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//, "");
|
|
28
|
+
// Strip any path
|
|
29
|
+
v = v.split("/")[0];
|
|
30
|
+
// Bracketed IPv6, e.g. "[::1]:9601"
|
|
31
|
+
if (v.startsWith("[")) {
|
|
32
|
+
const end = v.indexOf("]");
|
|
33
|
+
return (end >= 0 ? v.slice(1, end) : v).toLowerCase();
|
|
34
|
+
}
|
|
35
|
+
// Strip ":port"
|
|
36
|
+
const colon = v.lastIndexOf(":");
|
|
37
|
+
if (colon >= 0) v = v.slice(0, colon);
|
|
38
|
+
return v.toLowerCase();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function isLanAllowed(): boolean {
|
|
42
|
+
const v = (process.env.NEURAL_LOOM_ALLOW_LAN || "").toLowerCase();
|
|
43
|
+
return v === "1" || v === "true" || v === "yes";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function isLocalHostname(hostname: string | null): boolean {
|
|
47
|
+
return !!hostname && LOCAL_HOSTNAMES.has(hostname);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The interface the HTTP/WS servers should bind to given the current posture. */
|
|
51
|
+
export function getBindHost(): string {
|
|
52
|
+
return process.env.NEURAL_LOOM_HOST || (isLanAllowed() ? "0.0.0.0" : "127.0.0.1");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Decide whether an HTTP/WS request is allowed based on its Host and Origin
|
|
57
|
+
* headers. See module docs for the policy.
|
|
58
|
+
*/
|
|
59
|
+
export function isRequestAllowed(
|
|
60
|
+
hostHeader: string | null,
|
|
61
|
+
originHeader: string | null
|
|
62
|
+
): boolean {
|
|
63
|
+
const host = hostnameOf(hostHeader);
|
|
64
|
+
const origin = hostnameOf(originHeader);
|
|
65
|
+
|
|
66
|
+
if (!isLanAllowed()) {
|
|
67
|
+
if (!isLocalHostname(host)) return false;
|
|
68
|
+
// A present Origin must also be loopback (blocks rebinding/cross-site).
|
|
69
|
+
if (originHeader && !isLocalHostname(origin)) return false;
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// LAN explicitly allowed: still block obvious cross-site requests.
|
|
74
|
+
if (originHeader && origin !== host) return false;
|
|
75
|
+
return true;
|
|
76
|
+
}
|
package/src/lib/sql/McpServer.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import readline from "readline";
|
|
3
3
|
import { ConnectionManager } from "./ConnectionManager";
|
|
4
|
+
import { isReadOnlyViolation } from "./sqlGuard";
|
|
4
5
|
|
|
5
6
|
export class McpServer {
|
|
6
7
|
private workspaceRoot?: string;
|
|
@@ -232,17 +233,7 @@ export class McpServer {
|
|
|
232
233
|
return;
|
|
233
234
|
}
|
|
234
235
|
|
|
235
|
-
|
|
236
|
-
const isMutation = clean.startsWith("insert") ||
|
|
237
|
-
clean.startsWith("update") ||
|
|
238
|
-
clean.startsWith("delete") ||
|
|
239
|
-
clean.startsWith("drop") ||
|
|
240
|
-
clean.startsWith("alter") ||
|
|
241
|
-
clean.startsWith("create") ||
|
|
242
|
-
clean.startsWith("truncate") ||
|
|
243
|
-
clean.startsWith("replace");
|
|
244
|
-
|
|
245
|
-
if (profile.readOnly && isMutation) {
|
|
236
|
+
if (profile.readOnly && isReadOnlyViolation(sql)) {
|
|
246
237
|
this.sendToolResult(id, "Error: Access Denied. This database connection is configured as Read-Only.", true);
|
|
247
238
|
return;
|
|
248
239
|
}
|
|
@@ -41,8 +41,23 @@ export class MssqlAdapter extends DbAdapter {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// Validate the cached connection before use; transparently reconnect if the
|
|
45
|
+
// server dropped it (the pooled adapter instance is long-lived).
|
|
46
|
+
private async ensureLive(): Promise<void> {
|
|
47
|
+
if (!this.pool) {
|
|
48
|
+
await this.connect();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
await this.pool.request().query("SELECT 1");
|
|
53
|
+
} catch {
|
|
54
|
+
try { await this.disconnect(); } catch { /* ignore */ }
|
|
55
|
+
await this.connect();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
44
59
|
async query(sql: string, params: any[] = []): Promise<QueryResult> {
|
|
45
|
-
await this.
|
|
60
|
+
await this.ensureLive();
|
|
46
61
|
try {
|
|
47
62
|
const request = this.pool.request();
|
|
48
63
|
|
|
@@ -31,6 +31,13 @@ export class MysqlAdapter extends DbAdapter {
|
|
|
31
31
|
password: this.config.password || "",
|
|
32
32
|
ssl: this.config.ssl ? this.config.ssl : undefined
|
|
33
33
|
});
|
|
34
|
+
|
|
35
|
+
// Enforce read-only at the session level. With autocommit each statement
|
|
36
|
+
// runs in its own read-only transaction, so writes are rejected by the
|
|
37
|
+
// server rather than relying solely on our SQL guard.
|
|
38
|
+
if (this.config.readOnly) {
|
|
39
|
+
await this.connection.query("SET SESSION TRANSACTION READ ONLY");
|
|
40
|
+
}
|
|
34
41
|
} catch (err: any) {
|
|
35
42
|
console.error("[MysqlAdapter] Connect failed:", err);
|
|
36
43
|
this.connection = null;
|
|
@@ -38,8 +45,23 @@ export class MysqlAdapter extends DbAdapter {
|
|
|
38
45
|
}
|
|
39
46
|
}
|
|
40
47
|
|
|
48
|
+
// Validate the cached connection before use; transparently reconnect if the
|
|
49
|
+
// server dropped it (the pooled adapter instance is long-lived).
|
|
50
|
+
private async ensureLive(): Promise<void> {
|
|
51
|
+
if (!this.connection) {
|
|
52
|
+
await this.connect();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
await this.connection.query("SELECT 1");
|
|
57
|
+
} catch {
|
|
58
|
+
try { await this.disconnect(); } catch { /* ignore */ }
|
|
59
|
+
await this.connect();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
41
63
|
async query(sql: string, params: any[] = []): Promise<QueryResult> {
|
|
42
|
-
await this.
|
|
64
|
+
await this.ensureLive();
|
|
43
65
|
try {
|
|
44
66
|
const [rows, fields] = await this.connection.execute(sql, params);
|
|
45
67
|
|
|
@@ -34,6 +34,12 @@ export class PostgresAdapter extends DbAdapter {
|
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
await this.client.connect();
|
|
37
|
+
|
|
38
|
+
// Enforce read-only at the session level so every subsequent statement
|
|
39
|
+
// (including CTEs and stacked statements) is rejected by the server.
|
|
40
|
+
if (this.config.readOnly) {
|
|
41
|
+
await this.client.query("SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY");
|
|
42
|
+
}
|
|
37
43
|
} catch (err: any) {
|
|
38
44
|
console.error("[PostgresAdapter] Connect failed:", err);
|
|
39
45
|
this.client = null;
|
|
@@ -41,8 +47,23 @@ export class PostgresAdapter extends DbAdapter {
|
|
|
41
47
|
}
|
|
42
48
|
}
|
|
43
49
|
|
|
50
|
+
// Validate the cached connection before use; transparently reconnect if the
|
|
51
|
+
// server dropped it (the pooled adapter instance is long-lived).
|
|
52
|
+
private async ensureLive(): Promise<void> {
|
|
53
|
+
if (!this.client) {
|
|
54
|
+
await this.connect();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
await this.client.query("SELECT 1");
|
|
59
|
+
} catch {
|
|
60
|
+
try { await this.disconnect(); } catch { /* ignore */ }
|
|
61
|
+
await this.connect();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
44
65
|
async query(sql: string, params: any[] = []): Promise<QueryResult> {
|
|
45
|
-
await this.
|
|
66
|
+
await this.ensureLive();
|
|
46
67
|
try {
|
|
47
68
|
const res = await this.client.query(sql, params);
|
|
48
69
|
const rows = res.rows || [];
|
|
@@ -21,11 +21,13 @@ export class SqliteAdapter extends DbAdapter {
|
|
|
21
21
|
|
|
22
22
|
async connect(): Promise<void> {
|
|
23
23
|
if (this.db) return;
|
|
24
|
-
|
|
25
|
-
// Ensure parent directory exists
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
fs.
|
|
24
|
+
|
|
25
|
+
// Ensure parent directory exists (only when we may create/write the file).
|
|
26
|
+
if (!this.config.readOnly) {
|
|
27
|
+
const dir = path.dirname(this.resolvedPath);
|
|
28
|
+
if (!fs.existsSync(dir)) {
|
|
29
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
30
|
+
}
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
try {
|
|
@@ -39,17 +41,26 @@ export class SqliteAdapter extends DbAdapter {
|
|
|
39
41
|
);
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
|
|
44
|
+
// Read-only profiles open the database with the driver's readonly flag, so
|
|
45
|
+
// any write attempt fails at the engine level (not just our SQL guard).
|
|
46
|
+
this.db = new Database(this.resolvedPath, this.config.readOnly ? { readonly: true } : {});
|
|
43
47
|
} catch (err: any) {
|
|
44
48
|
console.error("[SqliteAdapter] Connect failed:", err);
|
|
45
49
|
throw err;
|
|
46
50
|
}
|
|
47
51
|
}
|
|
48
52
|
|
|
49
|
-
|
|
53
|
+
// Reopen the database handle if it was closed (e.g. after a disconnect).
|
|
54
|
+
private async ensureLive(): Promise<void> {
|
|
55
|
+
if (this.db && this.db.open) return;
|
|
56
|
+
this.db = null;
|
|
50
57
|
await this.connect();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async query(sql: string, params: any[] = []): Promise<QueryResult> {
|
|
61
|
+
await this.ensureLive();
|
|
51
62
|
try {
|
|
52
|
-
const isSelect = sql.trim().toLowerCase().startsWith("select") ||
|
|
63
|
+
const isSelect = sql.trim().toLowerCase().startsWith("select") ||
|
|
53
64
|
sql.trim().toLowerCase().startsWith("pragma") ||
|
|
54
65
|
sql.trim().toLowerCase().startsWith("explain");
|
|
55
66
|
|
|
@@ -80,8 +91,8 @@ export class SqliteAdapter extends DbAdapter {
|
|
|
80
91
|
}
|
|
81
92
|
|
|
82
93
|
async describeTable(tableName: string): Promise<ColumnSchema[]> {
|
|
83
|
-
await this.
|
|
84
|
-
|
|
94
|
+
await this.ensureLive();
|
|
95
|
+
|
|
85
96
|
// Get table columns
|
|
86
97
|
const columnsInfo = this.db.prepare(`PRAGMA table_info(\`${tableName}\`)`).all();
|
|
87
98
|
// Get foreign keys
|