neural-loom 0.2.69 → 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 +3 -3
- 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/{008bf~h8rh5_3.js → 0uyuco-451j.n.js} +2 -2
- package/.next/static/chunks/11c6f8pb_6s91.js +4 -0
- package/.next/static/chunks/{00pgauqedvo01.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/MarkdownPreview.tsx +17 -10
- package/src/app/components/TerminalConsole.tsx +99 -21
- 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/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/EglcK14vXeSWIjxjJCP9A/_clientMiddlewareManifest.js +0 -1
- package/.next/static/chunks/0ii_nb0g40rmj.js +0 -4
- /package/.next/static/{EglcK14vXeSWIjxjJCP9A → 04j4ht7eCOOXSLQ5Wixq0}/_buildManifest.js +0 -0
- /package/.next/static/{EglcK14vXeSWIjxjJCP9A → 04j4ht7eCOOXSLQ5Wixq0}/_ssgManifest.js +0 -0
package/src/app/api/git/route.ts
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import { promisify } from "util";
|
|
3
|
-
import {
|
|
3
|
+
import { execFile } from "child_process";
|
|
4
4
|
import fs from "fs";
|
|
5
5
|
import path from "path";
|
|
6
|
+
import { isValidBranchName } from "@/lib/gitSafe";
|
|
6
7
|
|
|
7
|
-
const
|
|
8
|
+
const execFilePromise = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
// Run git without a shell. Arguments are passed as an array, so values like file
|
|
11
|
+
// names, branch names and commit messages cannot break out into shell commands.
|
|
12
|
+
async function git(args: string[], cwd: string): Promise<{ stdout: string; stderr: string }> {
|
|
13
|
+
const { stdout, stderr } = await execFilePromise("git", args, {
|
|
14
|
+
cwd,
|
|
15
|
+
maxBuffer: 1024 * 1024 * 20,
|
|
16
|
+
windowsHide: true,
|
|
17
|
+
});
|
|
18
|
+
return { stdout: stdout.toString(), stderr: stderr.toString() };
|
|
19
|
+
}
|
|
8
20
|
|
|
9
21
|
export async function GET(request: Request) {
|
|
10
22
|
try {
|
|
@@ -17,13 +29,13 @@ export async function GET(request: Request) {
|
|
|
17
29
|
|
|
18
30
|
// Check if it's a git repo
|
|
19
31
|
try {
|
|
20
|
-
await
|
|
32
|
+
await git(["rev-parse", "--is-inside-work-tree"], targetRoot);
|
|
21
33
|
} catch {
|
|
22
34
|
return NextResponse.json({ success: true, isGit: false, files: [] });
|
|
23
35
|
}
|
|
24
36
|
|
|
25
37
|
if (action === "status") {
|
|
26
|
-
const { stdout } = await
|
|
38
|
+
const { stdout } = await git(["status", "--porcelain"], targetRoot);
|
|
27
39
|
const lines = stdout.split(/\r?\n/).filter(Boolean);
|
|
28
40
|
const files = lines.map(line => {
|
|
29
41
|
// porcelain format: XY path or XY "path"
|
|
@@ -49,7 +61,7 @@ export async function GET(request: Request) {
|
|
|
49
61
|
// Get branch name
|
|
50
62
|
let branch = "";
|
|
51
63
|
try {
|
|
52
|
-
const { stdout: branchOut } = await
|
|
64
|
+
const { stdout: branchOut } = await git(["rev-parse", "--abbrev-ref", "HEAD"], targetRoot);
|
|
53
65
|
branch = branchOut.trim();
|
|
54
66
|
} catch {}
|
|
55
67
|
|
|
@@ -57,7 +69,7 @@ export async function GET(request: Request) {
|
|
|
57
69
|
let ahead = 0;
|
|
58
70
|
let behind = 0;
|
|
59
71
|
try {
|
|
60
|
-
const { stdout: syncOut } = await
|
|
72
|
+
const { stdout: syncOut } = await git(["rev-list", "--left-right", "--count", "HEAD...@{u}"], targetRoot);
|
|
61
73
|
const parts = syncOut.trim().split(/\s+/);
|
|
62
74
|
if (parts.length === 2) {
|
|
63
75
|
ahead = parseInt(parts[0], 10) || 0;
|
|
@@ -79,7 +91,7 @@ export async function GET(request: Request) {
|
|
|
79
91
|
}
|
|
80
92
|
|
|
81
93
|
// Check status to see if it is untracked
|
|
82
|
-
const { stdout: statusOut } = await
|
|
94
|
+
const { stdout: statusOut } = await git(["status", "--porcelain", "--", file], targetRoot);
|
|
83
95
|
const isUntracked = statusOut.startsWith("??");
|
|
84
96
|
|
|
85
97
|
if (isUntracked) {
|
|
@@ -88,7 +100,7 @@ export async function GET(request: Request) {
|
|
|
88
100
|
return NextResponse.json({ success: true, diff: diffLines });
|
|
89
101
|
} else {
|
|
90
102
|
try {
|
|
91
|
-
const { stdout: diffOut } = await
|
|
103
|
+
const { stdout: diffOut } = await git(["diff", "--no-color", "--", file], targetRoot);
|
|
92
104
|
return NextResponse.json({ success: true, diff: diffOut });
|
|
93
105
|
} catch {
|
|
94
106
|
return NextResponse.json({ success: true, diff: `Binary file or diff unavailable.` });
|
|
@@ -97,7 +109,7 @@ export async function GET(request: Request) {
|
|
|
97
109
|
}
|
|
98
110
|
|
|
99
111
|
if (action === "branches") {
|
|
100
|
-
const { stdout } = await
|
|
112
|
+
const { stdout } = await git(["branch", "--format=%(refname:short)|%(HEAD)"], targetRoot);
|
|
101
113
|
const branches = stdout.split(/\r?\n/).filter(Boolean).map(line => {
|
|
102
114
|
const [name, active] = line.split("|");
|
|
103
115
|
return {
|
|
@@ -110,7 +122,10 @@ export async function GET(request: Request) {
|
|
|
110
122
|
|
|
111
123
|
if (action === "log") {
|
|
112
124
|
try {
|
|
113
|
-
const { stdout } = await
|
|
125
|
+
const { stdout } = await git(
|
|
126
|
+
["log", "--graph", "--abbrev-commit", "--decorate", "--format=format:%h||%p||%s||%an||%ar", "--all", "-n", "100"],
|
|
127
|
+
targetRoot
|
|
128
|
+
);
|
|
114
129
|
const lines = stdout.split(/\r?\n/);
|
|
115
130
|
const logItems = lines.map(line => {
|
|
116
131
|
const parts = line.split("||");
|
|
@@ -119,7 +134,7 @@ export async function GET(request: Request) {
|
|
|
119
134
|
const hashMatch = firstPart.match(/([0-9a-f]{7,40})$/);
|
|
120
135
|
const hash = hashMatch ? hashMatch[1] : "";
|
|
121
136
|
const graph = firstPart.substring(0, firstPart.length - hash.length);
|
|
122
|
-
|
|
137
|
+
|
|
123
138
|
return {
|
|
124
139
|
isCommit: true,
|
|
125
140
|
graph,
|
|
@@ -142,7 +157,7 @@ export async function GET(request: Request) {
|
|
|
142
157
|
}
|
|
143
158
|
});
|
|
144
159
|
return NextResponse.json({ success: true, log: logItems });
|
|
145
|
-
} catch
|
|
160
|
+
} catch {
|
|
146
161
|
return NextResponse.json({ success: true, log: [] });
|
|
147
162
|
}
|
|
148
163
|
}
|
|
@@ -163,25 +178,25 @@ export async function POST(request: Request) {
|
|
|
163
178
|
|
|
164
179
|
// Check if it's a git repo
|
|
165
180
|
try {
|
|
166
|
-
await
|
|
181
|
+
await git(["rev-parse", "--is-inside-work-tree"], targetRoot);
|
|
167
182
|
} catch {
|
|
168
183
|
return NextResponse.json({ error: "Not a git repository" }, { status: 400 });
|
|
169
184
|
}
|
|
170
185
|
|
|
171
186
|
if (action === "stage") {
|
|
172
187
|
if (file) {
|
|
173
|
-
await
|
|
188
|
+
await git(["add", "--", file], targetRoot);
|
|
174
189
|
} else {
|
|
175
|
-
await
|
|
190
|
+
await git(["add", "-A"], targetRoot);
|
|
176
191
|
}
|
|
177
192
|
return NextResponse.json({ success: true });
|
|
178
193
|
}
|
|
179
194
|
|
|
180
195
|
if (action === "unstage") {
|
|
181
196
|
if (file) {
|
|
182
|
-
await
|
|
197
|
+
await git(["reset", "HEAD", "--", file], targetRoot);
|
|
183
198
|
} else {
|
|
184
|
-
await
|
|
199
|
+
await git(["reset", "HEAD", "--", "."], targetRoot);
|
|
185
200
|
}
|
|
186
201
|
return NextResponse.json({ success: true });
|
|
187
202
|
}
|
|
@@ -192,7 +207,7 @@ export async function POST(request: Request) {
|
|
|
192
207
|
}
|
|
193
208
|
const absolutePath = path.resolve(targetRoot, file).replace(/\\/g, "/");
|
|
194
209
|
// Check status to see if it is untracked
|
|
195
|
-
const { stdout: statusOut } = await
|
|
210
|
+
const { stdout: statusOut } = await git(["status", "--porcelain", "--", file], targetRoot);
|
|
196
211
|
const isUntracked = statusOut.startsWith("??");
|
|
197
212
|
|
|
198
213
|
if (isUntracked) {
|
|
@@ -200,7 +215,7 @@ export async function POST(request: Request) {
|
|
|
200
215
|
fs.unlinkSync(absolutePath);
|
|
201
216
|
}
|
|
202
217
|
} else {
|
|
203
|
-
await
|
|
218
|
+
await git(["checkout", "--", file], targetRoot);
|
|
204
219
|
}
|
|
205
220
|
return NextResponse.json({ success: true });
|
|
206
221
|
}
|
|
@@ -209,7 +224,7 @@ export async function POST(request: Request) {
|
|
|
209
224
|
if (!message || !message.trim()) {
|
|
210
225
|
return NextResponse.json({ error: "Commit message cannot be empty" }, { status: 400 });
|
|
211
226
|
}
|
|
212
|
-
const { stdout } = await
|
|
227
|
+
const { stdout } = await git(["commit", "-m", message.trim()], targetRoot);
|
|
213
228
|
return NextResponse.json({ success: true, output: stdout });
|
|
214
229
|
}
|
|
215
230
|
|
|
@@ -231,40 +246,40 @@ export async function POST(request: Request) {
|
|
|
231
246
|
}
|
|
232
247
|
|
|
233
248
|
if (action === "pull") {
|
|
234
|
-
const { stdout, stderr } = await
|
|
249
|
+
const { stdout, stderr } = await git(["pull"], targetRoot);
|
|
235
250
|
return NextResponse.json({ success: true, output: stdout + stderr });
|
|
236
251
|
}
|
|
237
252
|
|
|
238
253
|
if (action === "push") {
|
|
239
|
-
const { stdout, stderr } = await
|
|
254
|
+
const { stdout, stderr } = await git(["push"], targetRoot);
|
|
240
255
|
return NextResponse.json({ success: true, output: stdout + stderr });
|
|
241
256
|
}
|
|
242
257
|
|
|
243
258
|
if (action === "checkout") {
|
|
244
259
|
const { branchName } = body;
|
|
245
|
-
if (!branchName) {
|
|
246
|
-
return NextResponse.json({ error: "Missing branchName parameter" }, { status: 400 });
|
|
260
|
+
if (!isValidBranchName(branchName)) {
|
|
261
|
+
return NextResponse.json({ error: "Missing or invalid branchName parameter" }, { status: 400 });
|
|
247
262
|
}
|
|
248
|
-
const { stdout, stderr } = await
|
|
263
|
+
const { stdout, stderr } = await git(["checkout", branchName], targetRoot);
|
|
249
264
|
return NextResponse.json({ success: true, output: stdout + stderr });
|
|
250
265
|
}
|
|
251
266
|
|
|
252
267
|
if (action === "create_branch") {
|
|
253
268
|
const { branchName } = body;
|
|
254
|
-
if (!branchName) {
|
|
255
|
-
return NextResponse.json({ error: "Missing branchName parameter" }, { status: 400 });
|
|
269
|
+
if (!isValidBranchName(branchName)) {
|
|
270
|
+
return NextResponse.json({ error: "Missing or invalid branchName parameter" }, { status: 400 });
|
|
256
271
|
}
|
|
257
|
-
const { stdout, stderr } = await
|
|
272
|
+
const { stdout, stderr } = await git(["checkout", "-b", branchName], targetRoot);
|
|
258
273
|
return NextResponse.json({ success: true, output: stdout + stderr });
|
|
259
274
|
}
|
|
260
275
|
|
|
261
276
|
if (action === "delete_branch") {
|
|
262
277
|
const { branchName, force } = body;
|
|
263
|
-
if (!branchName) {
|
|
264
|
-
return NextResponse.json({ error: "Missing branchName parameter" }, { status: 400 });
|
|
278
|
+
if (!isValidBranchName(branchName)) {
|
|
279
|
+
return NextResponse.json({ error: "Missing or invalid branchName parameter" }, { status: 400 });
|
|
265
280
|
}
|
|
266
281
|
const flag = force ? "-D" : "-d";
|
|
267
|
-
const { stdout, stderr } = await
|
|
282
|
+
const { stdout, stderr } = await git(["branch", flag, branchName], targetRoot);
|
|
268
283
|
return NextResponse.json({ success: true, output: stdout + stderr });
|
|
269
284
|
}
|
|
270
285
|
|
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import { SessionManager } from "@/lib/agents/SessionManager";
|
|
3
3
|
import { promisify } from "util";
|
|
4
|
-
import {
|
|
4
|
+
import { execFile } from "child_process";
|
|
5
5
|
import os from "os";
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const execFilePromise = promisify(execFile);
|
|
8
|
+
|
|
9
|
+
// Real per-process stats for the PTY. Returns null when unavailable (pidusage
|
|
10
|
+
// not installed, or no live pid) so callers report honestly instead of faking it.
|
|
11
|
+
async function getProcessStats(pid: number | undefined): Promise<{ cpu: number; memory: number } | null> {
|
|
12
|
+
if (!pid) return null;
|
|
13
|
+
try {
|
|
14
|
+
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
15
|
+
const pidusage = eval("require")("pidusage");
|
|
16
|
+
const stat = await pidusage(pid);
|
|
17
|
+
return { cpu: stat.cpu, memory: stat.memory };
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
8
22
|
|
|
9
23
|
export async function GET(request: Request) {
|
|
10
24
|
try {
|
|
@@ -21,42 +35,45 @@ export async function GET(request: Request) {
|
|
|
21
35
|
return NextResponse.json({ error: "Session not found" }, { status: 404 });
|
|
22
36
|
}
|
|
23
37
|
|
|
24
|
-
// Default response metrics
|
|
38
|
+
// Default response metrics. `available` reports whether these are real
|
|
39
|
+
// measurements (vs. a placeholder when telemetry can't be collected).
|
|
25
40
|
let cpu = "0.0%";
|
|
26
41
|
let memory = "0.0MiB";
|
|
27
42
|
let memoryPercent = "0.0%";
|
|
43
|
+
let available = false;
|
|
28
44
|
|
|
29
45
|
if (session.status === "running") {
|
|
30
46
|
const isDocker = session.type === "claude-docker" || session.type === "aider-docker";
|
|
31
47
|
if (isDocker) {
|
|
32
48
|
try {
|
|
33
|
-
//
|
|
49
|
+
// Real container stats from the Docker daemon.
|
|
34
50
|
const containerName = `neural-loom-session-${id}`;
|
|
35
|
-
const { stdout } = await
|
|
36
|
-
|
|
51
|
+
const { stdout } = await execFilePromise("docker", [
|
|
52
|
+
"stats",
|
|
53
|
+
containerName,
|
|
54
|
+
"--no-stream",
|
|
55
|
+
"--format",
|
|
56
|
+
"{{.CPUPerc}},{{.MemUsage}},{{.MemPerc}}",
|
|
57
|
+
]);
|
|
58
|
+
const parts = stdout.toString().trim().split(",");
|
|
37
59
|
if (parts.length >= 3) {
|
|
38
60
|
cpu = parts[0].trim();
|
|
39
|
-
memory = parts[1].trim().split(" / ")[0]; //
|
|
61
|
+
memory = parts[1].trim().split(" / ")[0]; // used part, e.g. "142.2MiB"
|
|
40
62
|
memoryPercent = parts[2].trim();
|
|
63
|
+
available = true;
|
|
41
64
|
}
|
|
42
65
|
} catch {
|
|
43
|
-
// Docker
|
|
44
|
-
const simulatedCpu = (Math.random() * 3 + 1).toFixed(1) + "%";
|
|
45
|
-
const simulatedMem = (130 + Math.random() * 20).toFixed(1) + "MiB";
|
|
46
|
-
const simulatedMemPerc = (1.5 + Math.random() * 0.5).toFixed(2) + "%";
|
|
47
|
-
cpu = simulatedCpu;
|
|
48
|
-
memory = simulatedMem;
|
|
49
|
-
memoryPercent = simulatedMemPerc;
|
|
66
|
+
// Docker daemon/container not reachable — leave defaults, available=false.
|
|
50
67
|
}
|
|
51
68
|
} else {
|
|
52
|
-
//
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
69
|
+
// Real per-process stats for the local/SSH PTY process.
|
|
70
|
+
const stats = await getProcessStats(session.getPid());
|
|
71
|
+
if (stats) {
|
|
72
|
+
cpu = stats.cpu.toFixed(1) + "%";
|
|
73
|
+
memory = (stats.memory / (1024 * 1024)).toFixed(1) + "MiB";
|
|
74
|
+
memoryPercent = ((stats.memory / os.totalmem()) * 100).toFixed(2) + "%";
|
|
75
|
+
available = true;
|
|
76
|
+
}
|
|
60
77
|
}
|
|
61
78
|
}
|
|
62
79
|
|
|
@@ -68,6 +85,7 @@ export async function GET(request: Request) {
|
|
|
68
85
|
cpu,
|
|
69
86
|
memory,
|
|
70
87
|
memoryPercent,
|
|
88
|
+
available,
|
|
71
89
|
});
|
|
72
90
|
} catch (error) {
|
|
73
91
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
@@ -1,19 +1,6 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import { ConnectionManager } from "@/lib/sql/ConnectionManager";
|
|
3
|
-
|
|
4
|
-
const isMutation = (sql: string): boolean => {
|
|
5
|
-
const clean = sql.trim().toLowerCase();
|
|
6
|
-
return (
|
|
7
|
-
clean.startsWith("insert") ||
|
|
8
|
-
clean.startsWith("update") ||
|
|
9
|
-
clean.startsWith("delete") ||
|
|
10
|
-
clean.startsWith("drop") ||
|
|
11
|
-
clean.startsWith("alter") ||
|
|
12
|
-
clean.startsWith("create") ||
|
|
13
|
-
clean.startsWith("truncate") ||
|
|
14
|
-
clean.startsWith("replace")
|
|
15
|
-
);
|
|
16
|
-
};
|
|
3
|
+
import { isReadOnlyViolation } from "@/lib/sql/sqlGuard";
|
|
17
4
|
|
|
18
5
|
export async function POST(request: Request) {
|
|
19
6
|
try {
|
|
@@ -33,7 +20,7 @@ export async function POST(request: Request) {
|
|
|
33
20
|
|
|
34
21
|
const { adapter, profile } = active;
|
|
35
22
|
|
|
36
|
-
if (profile.readOnly &&
|
|
23
|
+
if (profile.readOnly && isReadOnlyViolation(sql)) {
|
|
37
24
|
return NextResponse.json({
|
|
38
25
|
error: "This connection profile is configured as Read-Only. Modifications are disabled."
|
|
39
26
|
}, { status: 403 });
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import path from "path";
|
|
4
|
+
import { getConfigDir } from "@/lib/config";
|
|
4
5
|
|
|
5
6
|
export async function GET() {
|
|
6
7
|
try {
|
|
7
|
-
const historyFile = path.join(
|
|
8
|
+
const historyFile = path.join(getConfigDir(), "token-history.json");
|
|
8
9
|
if (!fs.existsSync(historyFile)) {
|
|
9
10
|
return NextResponse.json({ success: true, history: [] });
|
|
10
11
|
}
|
|
@@ -322,12 +322,8 @@ export default function MarkdownPreview({ content }: MarkdownPreviewProps) {
|
|
|
322
322
|
|
|
323
323
|
function copyTextToClipboard(text: string): boolean {
|
|
324
324
|
if (typeof window === "undefined") return false;
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
console.error("Failed to copy using clipboard API:", err);
|
|
328
|
-
});
|
|
329
|
-
return true;
|
|
330
|
-
}
|
|
325
|
+
|
|
326
|
+
let successful = false;
|
|
331
327
|
try {
|
|
332
328
|
const textArea = document.createElement("textarea");
|
|
333
329
|
textArea.value = text;
|
|
@@ -338,13 +334,24 @@ function copyTextToClipboard(text: string): boolean {
|
|
|
338
334
|
document.body.appendChild(textArea);
|
|
339
335
|
textArea.focus();
|
|
340
336
|
textArea.select();
|
|
341
|
-
|
|
337
|
+
successful = document.execCommand("copy");
|
|
342
338
|
document.body.removeChild(textArea);
|
|
343
|
-
return successful;
|
|
344
339
|
} catch (err) {
|
|
345
|
-
console.
|
|
346
|
-
|
|
340
|
+
console.warn("Fallback execCommand copy failed:", err);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (successful) {
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
348
|
+
navigator.clipboard.writeText(text).catch((err) => {
|
|
349
|
+
console.error("Failed to copy using clipboard API:", err);
|
|
350
|
+
});
|
|
351
|
+
return true;
|
|
347
352
|
}
|
|
353
|
+
|
|
354
|
+
return false;
|
|
348
355
|
}
|
|
349
356
|
|
|
350
357
|
function CodeBlock({ code, language }: { code: string; language: string }) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
3
|
|
|
4
|
-
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
5
5
|
import { Terminal } from "xterm";
|
|
6
6
|
import { FitAddon } from "xterm-addon-fit";
|
|
7
7
|
import { SearchAddon } from "@xterm/addon-search";
|
|
@@ -19,14 +19,22 @@ interface LogBufferItem {
|
|
|
19
19
|
message?: string;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
function copyTextToClipboard(text: string): boolean {
|
|
23
|
-
if (typeof window === "undefined") return false;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
async function copyTextToClipboard(text: string): Promise<boolean> {
|
|
23
|
+
if (typeof window === "undefined" || !text) return false;
|
|
24
|
+
|
|
25
|
+
// Preferred path: async Clipboard API. Reliable in a secure context, which
|
|
26
|
+
// includes localhost (NeuralLoom's default bind).
|
|
27
|
+
if (window.isSecureContext && navigator.clipboard?.writeText) {
|
|
28
|
+
try {
|
|
29
|
+
await navigator.clipboard.writeText(text);
|
|
30
|
+
return true;
|
|
31
|
+
} catch {
|
|
32
|
+
// Fall through to the execCommand fallback (e.g. permission denied).
|
|
33
|
+
}
|
|
29
34
|
}
|
|
35
|
+
|
|
36
|
+
// Fallback: hidden textarea + execCommand("copy") for non-secure contexts
|
|
37
|
+
// (e.g. accessed over a plain-HTTP LAN address).
|
|
30
38
|
try {
|
|
31
39
|
const textArea = document.createElement("textarea");
|
|
32
40
|
textArea.value = text;
|
|
@@ -41,7 +49,7 @@ function copyTextToClipboard(text: string): boolean {
|
|
|
41
49
|
document.body.removeChild(textArea);
|
|
42
50
|
return successful;
|
|
43
51
|
} catch (err) {
|
|
44
|
-
console.
|
|
52
|
+
console.warn("Clipboard copy failed:", err);
|
|
45
53
|
return false;
|
|
46
54
|
}
|
|
47
55
|
}
|
|
@@ -64,6 +72,31 @@ export default function TerminalConsole({ sessionId, height = "400px", wsPort =
|
|
|
64
72
|
const [caseSensitive, setCaseSensitive] = useState(false);
|
|
65
73
|
const searchAddonRef = useRef<SearchAddon | null>(null);
|
|
66
74
|
|
|
75
|
+
// Copy feedback ("Copied!" indicator)
|
|
76
|
+
const [copied, setCopied] = useState(false);
|
|
77
|
+
const copiedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
78
|
+
|
|
79
|
+
// Copy the given text (or the current terminal selection) to the clipboard.
|
|
80
|
+
// Stable identity so the terminal effect can use it without re-initializing.
|
|
81
|
+
const copySelection = useCallback((text?: string): void => {
|
|
82
|
+
const term = xtermRef.current;
|
|
83
|
+
const selection = (text ?? term?.getSelection() ?? "").trim();
|
|
84
|
+
if (!selection) return;
|
|
85
|
+
copyTextToClipboard(selection).then((ok) => {
|
|
86
|
+
if (!ok) return;
|
|
87
|
+
setCopied(true);
|
|
88
|
+
if (copiedTimerRef.current) clearTimeout(copiedTimerRef.current);
|
|
89
|
+
copiedTimerRef.current = setTimeout(() => setCopied(false), 1200);
|
|
90
|
+
});
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
// Clear the copy-feedback timer on unmount
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
return () => {
|
|
96
|
+
if (copiedTimerRef.current) clearTimeout(copiedTimerRef.current);
|
|
97
|
+
};
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
67
100
|
// Focus search input when opened
|
|
68
101
|
useEffect(() => {
|
|
69
102
|
if (searchOpen && searchInputRef.current) {
|
|
@@ -182,20 +215,18 @@ export default function TerminalConsole({ sessionId, height = "400px", wsPort =
|
|
|
182
215
|
term.focus();
|
|
183
216
|
|
|
184
217
|
const container = terminalRef.current;
|
|
218
|
+
// Keep right-click from clearing an active selection so the context-menu
|
|
219
|
+
// copy can read it.
|
|
185
220
|
const handleMouseDown = (e: MouseEvent) => {
|
|
186
|
-
if (e.button === 2 && term.hasSelection())
|
|
187
|
-
e.stopPropagation();
|
|
188
|
-
}
|
|
221
|
+
if (e.button === 2 && term.hasSelection()) e.stopPropagation();
|
|
189
222
|
};
|
|
190
223
|
const handleMouseUp = (e: MouseEvent) => {
|
|
191
|
-
if (e.button === 2 && term.hasSelection())
|
|
192
|
-
e.stopPropagation();
|
|
193
|
-
}
|
|
224
|
+
if (e.button === 2 && term.hasSelection()) e.stopPropagation();
|
|
194
225
|
};
|
|
195
226
|
const handleContextMenu = (e: MouseEvent) => {
|
|
196
227
|
if (term.hasSelection()) {
|
|
197
228
|
e.preventDefault();
|
|
198
|
-
|
|
229
|
+
copySelection(term.getSelection());
|
|
199
230
|
term.clearSelection();
|
|
200
231
|
}
|
|
201
232
|
};
|
|
@@ -205,6 +236,17 @@ export default function TerminalConsole({ sessionId, height = "400px", wsPort =
|
|
|
205
236
|
container.addEventListener("contextmenu", handleContextMenu);
|
|
206
237
|
}
|
|
207
238
|
|
|
239
|
+
// Copy-on-select: capture the selection the moment it is made. Claude Code's
|
|
240
|
+
// TUI redraws constantly, which clears the xterm selection — so we copy
|
|
241
|
+
// immediately (debounced) rather than waiting for a later Ctrl+C/right-click.
|
|
242
|
+
let selTimer: ReturnType<typeof setTimeout> | null = null;
|
|
243
|
+
const selDisposable = term.onSelectionChange(() => {
|
|
244
|
+
const sel = term.getSelection();
|
|
245
|
+
if (!sel || !sel.trim()) return;
|
|
246
|
+
if (selTimer) clearTimeout(selTimer);
|
|
247
|
+
selTimer = setTimeout(() => copySelection(sel), 150);
|
|
248
|
+
});
|
|
249
|
+
|
|
208
250
|
// Attach search key handler and clipboard copy handler
|
|
209
251
|
term.attachCustomKeyEventHandler((e) => {
|
|
210
252
|
// Search: Ctrl+F
|
|
@@ -213,11 +255,20 @@ export default function TerminalConsole({ sessionId, height = "400px", wsPort =
|
|
|
213
255
|
setSearchOpen(true);
|
|
214
256
|
return false;
|
|
215
257
|
}
|
|
216
|
-
|
|
217
|
-
//
|
|
218
|
-
if ((e.ctrlKey || e.metaKey) && e.key === "c" && e.type === "keydown") {
|
|
258
|
+
|
|
259
|
+
// Explicit copy: Ctrl+Shift+C / Cmd+Shift+C (always, when a selection exists)
|
|
260
|
+
if ((e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === "c" || e.key === "C") && e.type === "keydown") {
|
|
261
|
+
if (term.hasSelection()) {
|
|
262
|
+
copySelection(term.getSelection());
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Copy: Ctrl+C / Cmd+C only when a selection is active (otherwise let it
|
|
268
|
+
// pass through to the PTY as an interrupt).
|
|
269
|
+
if ((e.ctrlKey || e.metaKey) && !e.shiftKey && e.key === "c" && e.type === "keydown") {
|
|
219
270
|
if (term.hasSelection()) {
|
|
220
|
-
|
|
271
|
+
copySelection(term.getSelection());
|
|
221
272
|
return false; // Prevent sending interrupt to PTY
|
|
222
273
|
}
|
|
223
274
|
}
|
|
@@ -400,6 +451,8 @@ export default function TerminalConsole({ sessionId, height = "400px", wsPort =
|
|
|
400
451
|
return () => {
|
|
401
452
|
isDisposed = true;
|
|
402
453
|
clearTimeout(fitTimeout);
|
|
454
|
+
if (selTimer) clearTimeout(selTimer);
|
|
455
|
+
selDisposable.dispose();
|
|
403
456
|
resizeObserver.disconnect();
|
|
404
457
|
ws.close();
|
|
405
458
|
if (container) {
|
|
@@ -414,7 +467,7 @@ export default function TerminalConsole({ sessionId, height = "400px", wsPort =
|
|
|
414
467
|
console.warn("Error disposing terminal:", e);
|
|
415
468
|
}
|
|
416
469
|
};
|
|
417
|
-
}, [sessionId, wsPort]);
|
|
470
|
+
}, [sessionId, wsPort, copySelection]);
|
|
418
471
|
const handlePaste = async (e: React.ClipboardEvent<HTMLDivElement>) => {
|
|
419
472
|
const items = e.clipboardData?.items;
|
|
420
473
|
if (!items) return;
|
|
@@ -516,6 +569,31 @@ export default function TerminalConsole({ sessionId, height = "400px", wsPort =
|
|
|
516
569
|
)}
|
|
517
570
|
</div>
|
|
518
571
|
<div style={{ display: "flex", gap: "0.35rem", alignItems: "center" }}>
|
|
572
|
+
{copied && (
|
|
573
|
+
<span style={{
|
|
574
|
+
color: "#50fa7b",
|
|
575
|
+
fontSize: "0.65rem",
|
|
576
|
+
fontWeight: 600,
|
|
577
|
+
}}>
|
|
578
|
+
✓ Copied
|
|
579
|
+
</span>
|
|
580
|
+
)}
|
|
581
|
+
<button
|
|
582
|
+
onClick={() => copySelection()}
|
|
583
|
+
style={{
|
|
584
|
+
padding: "2px 6px",
|
|
585
|
+
borderRadius: "3px",
|
|
586
|
+
border: "1px solid var(--border)",
|
|
587
|
+
backgroundColor: "transparent",
|
|
588
|
+
color: "var(--muted)",
|
|
589
|
+
fontSize: "0.65rem",
|
|
590
|
+
fontWeight: 600,
|
|
591
|
+
cursor: "pointer",
|
|
592
|
+
}}
|
|
593
|
+
title="Copy selected terminal text (also auto-copies on selection)"
|
|
594
|
+
>
|
|
595
|
+
📋 Copy
|
|
596
|
+
</button>
|
|
519
597
|
<button
|
|
520
598
|
onClick={togglePause}
|
|
521
599
|
style={{
|