shubhjn commited on
Commit
5dc2f11
·
1 Parent(s): cf7a80b

fix ai asistance and make it per user friendly isolated enviroment

Browse files
.agent/memory/session.json CHANGED
@@ -1,7 +1,7 @@
1
  {
2
  "version": "1.0.0",
3
- "session_id": "50325c3e",
4
- "started_at": "2026-03-25T19:43:17.315877+05:30",
5
  "workspace": "D:\\Code\\codeverse",
6
  "active_task_id": null,
7
  "active_agent": null,
 
1
  {
2
  "version": "1.0.0",
3
+ "session_id": "f5a7d65f",
4
+ "started_at": "2026-03-25T20:17:36.335488+05:30",
5
  "workspace": "D:\\Code\\codeverse",
6
  "active_task_id": null,
7
  "active_agent": null,
app/api/agent/route.ts CHANGED
@@ -1,16 +1,89 @@
1
- import { streamText, generateObject, LanguageModel } from "ai";
 
 
 
 
 
 
 
 
2
  import { z } from "zod";
3
  import { getModel } from "@/lib/agents/registry";
4
- import { coreTools } from "@/lib/mcp/tools";
5
- import { NextRequest } from "next/server";
 
 
 
 
 
 
 
 
 
 
6
 
7
  export async function POST(req: NextRequest) {
 
 
 
 
8
  try {
9
- const { messages, modelId, mode = "execute", systemPrompt } = await req.json();
10
- const model: LanguageModel = getModel(modelId, req);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  if (mode === "plan") {
13
- // In plan mode, we don't stream immediately. We generate a structured JSON plan first.
14
  const result = await generateObject({
15
  model,
16
  system: systemPrompt,
@@ -27,22 +100,75 @@ export async function POST(req: NextRequest) {
27
  }),
28
  });
29
 
30
- return new Response(JSON.stringify(result.object), {
31
- headers: { "Content-Type": "application/json" }
32
- });
33
  }
34
 
35
- // Execute mode: Standard stream-text with workspace tool calls enabled
36
  const result = streamText({
37
  model,
38
  system: systemPrompt,
39
  messages,
40
- tools: coreTools,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  });
42
 
43
  return result.toTextStreamResponse();
44
  } catch (e: unknown) {
 
45
  const error = e instanceof Error ? e : new Error(String(e));
46
  return new Response(JSON.stringify({ error: error.message }), { status: 500 });
47
  }
48
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ streamText,
3
+ generateObject,
4
+ LanguageModel,
5
+ AssistantModelMessage,
6
+ UserModelMessage,
7
+ SystemModelMessage,
8
+ ToolModelMessage
9
+ } from "ai";
10
  import { z } from "zod";
11
  import { getModel } from "@/lib/agents/registry";
12
+ import { createTools } from "@/lib/mcp/tools";
13
+ import { auth } from "@/auth";
14
+ import { NextRequest, NextResponse } from "next/server";
15
+ import { db } from "@/lib/db";
16
+ import { randomUUID } from "crypto";
17
+
18
+ // Define a strict union type for core messages to avoid 'any'
19
+ type CoreChatMessage =
20
+ | UserModelMessage
21
+ | AssistantModelMessage
22
+ | SystemModelMessage
23
+ | ToolModelMessage;
24
 
25
  export async function POST(req: NextRequest) {
26
+ const session = await auth();
27
+ if (!session?.user?.id) return new Response("Unauthorized", { status: 401 });
28
+ const userId = session.user.id;
29
+
30
  try {
31
+ const { messages, modelId, workspaceName, mode = "execute" } = await req.json() as {
32
+ messages: CoreChatMessage[];
33
+ modelId?: string;
34
+ workspaceName: string;
35
+ mode?: "plan" | "execute";
36
+ };
37
+
38
+ if (!workspaceName) return new Response("Missing workspaceName", { status: 400 });
39
+
40
+ // Resolve workspaceId
41
+ const wsRes = await db.execute({
42
+ sql: "SELECT id FROM workspaces WHERE user_id = ? AND project_name = ?",
43
+ args: [userId, workspaceName]
44
+ });
45
+
46
+ if (wsRes.rows.length === 0) {
47
+ return new Response("Workspace not found", { status: 404 });
48
+ }
49
+ const workspaceId = wsRes.rows[0].id as string;
50
+
51
+ const model: LanguageModel = getModel(modelId as string, req);
52
+ const tools = createTools(userId, workspaceName);
53
+
54
+ const systemPrompt = `You are CodeVerse AI, a world-class autonomous coding agent.
55
+ You are embedded in a premium "AI Studio" environment.
56
+ Current Workspace: ${workspaceName} (Path: workspaces/${userId}/${workspaceName}/)
57
+
58
+ CORE PRINCIPLES:
59
+ 1. PLAN BEFORE ACTION: Always explain high-level strategy before using tools.
60
+ 2. PRECISION: Read files before editing. Ensure syntax is correct.
61
+ 3. CONTEXT AWARENESS: You are part of an IDE. Help with refactoring, debugging, and feature development.
62
+ 4. SAFETY: You have shell access but should remain within the workspace. Never attempt to escape the sandbox.
63
+
64
+ You have access to:
65
+ - read_file: View content.
66
+ - write_file: Create/edit files (overwrite mode).
67
+ - terminal_command: Run builds, tests, or scripts.
68
+ - search_code: Find patterns.
69
+ - list_files: Explore structure.
70
+
71
+ Respond in professional Markdown. Use code blocks for all technical output.`;
72
+
73
+ // Save incoming user message to history
74
+ const latestMessage = messages[messages.length - 1];
75
+ if (latestMessage && latestMessage.role === "user") {
76
+ const content = Array.isArray(latestMessage.content)
77
+ ? latestMessage.content.map(p => ('text' in p ? p.text : '')).join('')
78
+ : latestMessage.content;
79
+
80
+ await db.execute({
81
+ sql: "INSERT INTO chat_history (id, user_id, workspace_id, role, content) VALUES (?, ?, ?, ?, ?)",
82
+ args: [randomUUID(), userId as string, workspaceId as string, latestMessage.role, content]
83
+ });
84
+ }
85
 
86
  if (mode === "plan") {
 
87
  const result = await generateObject({
88
  model,
89
  system: systemPrompt,
 
100
  }),
101
  });
102
 
103
+ return NextResponse.json(result.object);
 
 
104
  }
105
 
 
106
  const result = streamText({
107
  model,
108
  system: systemPrompt,
109
  messages,
110
+ tools, // Assuming createTools returns compatible types now
111
+ onFinish: async (completion) => {
112
+ // Save assistant response to DB
113
+ try {
114
+ await db.execute({
115
+ sql: "INSERT INTO chat_history (id, user_id, workspace_id, role, content, tool_invocations) VALUES (?, ?, ?, ?, ?, ?)",
116
+ args: [
117
+ randomUUID(),
118
+ userId as string,
119
+ workspaceId as string,
120
+ "assistant",
121
+ completion.text,
122
+ JSON.stringify(completion.toolCalls || [])
123
+ ]
124
+ });
125
+ } catch (dbErr) {
126
+ console.error("Failed to save assistant response to history:", dbErr);
127
+ }
128
+ }
129
  });
130
 
131
  return result.toTextStreamResponse();
132
  } catch (e: unknown) {
133
+ console.error("[AGENT_ROUTE_ERROR]", e);
134
  const error = e instanceof Error ? e : new Error(String(e));
135
  return new Response(JSON.stringify({ error: error.message }), { status: 500 });
136
  }
137
  }
138
+
139
+ export async function GET(req: NextRequest) {
140
+ const session = await auth();
141
+ if (!session?.user?.id) return new Response("Unauthorized", { status: 401 });
142
+ const userId = session.user.id;
143
+
144
+ const { searchParams } = new URL(req.url);
145
+ const workspaceName = searchParams.get("workspaceName");
146
+ if (!workspaceName) return new Response("Missing workspaceName", { status: 400 });
147
+
148
+ try {
149
+ const wsRes = await db.execute({
150
+ sql: "SELECT id FROM workspaces WHERE user_id = ? AND project_name = ?",
151
+ args: [userId, workspaceName]
152
+ });
153
+
154
+ if (wsRes.rows.length === 0) return NextResponse.json({ messages: [] });
155
+ const workspaceId = wsRes.rows[0].id as string;
156
+
157
+ const res = await db.execute({
158
+ sql: "SELECT role, content, tool_invocations as toolInvocations, created_at FROM chat_history WHERE user_id = ? AND workspace_id = ? ORDER BY created_at ASC",
159
+ args: [userId, workspaceId]
160
+ });
161
+
162
+ const messages = res.rows.map(row => ({
163
+ id: randomUUID(),
164
+ role: row.role as string,
165
+ content: row.content as string,
166
+ toolInvocations: row.toolInvocations ? JSON.parse(row.toolInvocations as string) : undefined
167
+ }));
168
+
169
+ return NextResponse.json({ messages });
170
+ } catch (e) {
171
+ console.error("[FETCH_HISTORY_ERROR]", e);
172
+ return NextResponse.json({ messages: [] });
173
+ }
174
+ }
app/api/fs/route.ts CHANGED
@@ -1,50 +1,62 @@
1
  import { NextRequest, NextResponse } from "next/server";
2
  import { readDir, readFile, writeFile, deletePath } from "@/lib/fs";
 
 
3
 
4
  export async function GET(req: NextRequest) {
 
 
 
5
  const { searchParams } = new URL(req.url);
6
- const path = searchParams.get("path") || "";
7
  const action = searchParams.get("action") || "list";
8
 
9
  try {
 
 
10
  if (action === "list") {
11
- const nodes = await readDir(path);
12
  return NextResponse.json(nodes);
13
  }
14
  if (action === "read") {
15
- const content = await readFile(path);
16
  return NextResponse.json({ content });
17
  }
18
  return NextResponse.json({ error: "Invalid action" }, { status: 400 });
19
  } catch (e: unknown) {
20
- const error = e instanceof Error ? e : new Error(String(e));
21
- return NextResponse.json({ error: error.message }, { status: 500 });
22
  }
23
  }
24
 
25
  export async function POST(req: NextRequest) {
 
 
 
26
  try {
27
- const { path, content } = await req.json();
28
- if (!path) return NextResponse.json({ error: "Path required" }, { status: 400 });
29
 
30
- await writeFile(path, content);
 
31
  return NextResponse.json({ success: true });
32
  } catch (e: unknown) {
33
- const error = e instanceof Error ? e : new Error(String(e));
34
- return NextResponse.json({ error: error.message }, { status: 500 });
35
  }
36
  }
37
 
38
  export async function DELETE(req: NextRequest) {
 
 
 
39
  const { searchParams } = new URL(req.url);
40
- const path = searchParams.get("path");
41
 
42
  try {
43
- if (!path) return NextResponse.json({ error: "Path required" }, { status: 400 });
44
- await deletePath(path);
 
45
  return NextResponse.json({ success: true });
46
  } catch (e: unknown) {
47
- const error = e instanceof Error ? e : new Error(String(e));
48
- return NextResponse.json({ error: error.message }, { status: 500 });
49
  }
50
  }
 
1
  import { NextRequest, NextResponse } from "next/server";
2
  import { readDir, readFile, writeFile, deletePath } from "@/lib/fs";
3
+ import { auth } from "@/auth";
4
+ import { resolveSafePath } from "@/lib/fs/isolation";
5
 
6
  export async function GET(req: NextRequest) {
7
+ const session = await auth();
8
+ if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
9
+
10
  const { searchParams } = new URL(req.url);
11
+ const subPath = searchParams.get("path") || "";
12
  const action = searchParams.get("action") || "list";
13
 
14
  try {
15
+ const fullPath = await resolveSafePath(session.user.id, subPath);
16
+
17
  if (action === "list") {
18
+ const nodes = await readDir(fullPath);
19
  return NextResponse.json(nodes);
20
  }
21
  if (action === "read") {
22
+ const content = await readFile(fullPath);
23
  return NextResponse.json({ content });
24
  }
25
  return NextResponse.json({ error: "Invalid action" }, { status: 400 });
26
  } catch (e: unknown) {
27
+ return NextResponse.json({ error: (e as Error).message }, { status: 500 });
 
28
  }
29
  }
30
 
31
  export async function POST(req: NextRequest) {
32
+ const session = await auth();
33
+ if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
34
+
35
  try {
36
+ const { path: subPath, content } = await req.json();
37
+ if (!subPath) return NextResponse.json({ error: "Path required" }, { status: 400 });
38
 
39
+ const fullPath = await resolveSafePath(session.user.id, subPath);
40
+ await writeFile(fullPath, content);
41
  return NextResponse.json({ success: true });
42
  } catch (e: unknown) {
43
+ return NextResponse.json({ error: (e as Error).message }, { status: 500 });
 
44
  }
45
  }
46
 
47
  export async function DELETE(req: NextRequest) {
48
+ const session = await auth();
49
+ if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
50
+
51
  const { searchParams } = new URL(req.url);
52
+ const subPath = searchParams.get("path");
53
 
54
  try {
55
+ if (!subPath) return NextResponse.json({ error: "Path required" }, { status: 400 });
56
+ const fullPath = await resolveSafePath(session.user.id, subPath);
57
+ await deletePath(fullPath);
58
  return NextResponse.json({ success: true });
59
  } catch (e: unknown) {
60
+ return NextResponse.json({ error: (e as Error).message }, { status: 500 });
 
61
  }
62
  }
app/api/git/route.ts CHANGED
@@ -7,29 +7,41 @@ import {
7
  pushBranch,
8
  pullBranch,
9
  checkoutBranch,
10
- git
11
  } from "@/lib/git";
 
 
12
 
13
  export async function GET(req: NextRequest) {
 
 
 
14
  const { searchParams } = new URL(req.url);
15
  const action = searchParams.get("action");
 
 
 
16
 
17
  try {
 
 
 
18
  switch (action) {
19
  case "status":
20
- return NextResponse.json(await getGitStatus());
21
  case "branches":
22
- return NextResponse.json(await getBranchList());
23
  case "diff":
24
  const file = searchParams.get("file");
25
  if (!file) return NextResponse.json({ error: "File required" }, { status: 400 });
26
- return NextResponse.json({ diff: await getFileDiff(file) });
27
  case "log":
28
- const log = await git.log({ maxCount: 50 });
29
  return NextResponse.json(log.all);
30
  case "checkConfig":
31
- const name = await git.getConfig("user.name", "local");
32
- const email = await git.getConfig("user.email", "local");
 
33
  return NextResponse.json({
34
  configured: !!(name.value && email.value),
35
  name: name.value,
@@ -39,46 +51,51 @@ export async function GET(req: NextRequest) {
39
  return NextResponse.json({ error: "Invalid action" }, { status: 400 });
40
  }
41
  } catch (e: unknown) {
42
- const error = e instanceof Error ? e : new Error(String(e));
43
- return NextResponse.json({ error: error.message }, { status: 500 });
44
  }
45
  }
46
 
47
  export async function POST(req: NextRequest) {
 
 
 
48
  try {
49
  const body = await req.json();
50
- const { action } = body;
 
 
 
51
 
52
  switch (action) {
53
  case "commit":
54
  if (!body.message) return NextResponse.json({ error: "Message required" }, { status: 400 });
55
- const res = await commitFiles(body.message, body.files);
56
  return NextResponse.json({ success: true, commit: res.commit });
57
 
58
  case "push":
59
- await pushBranch();
60
  return NextResponse.json({ success: true });
61
 
62
  case "pull":
63
- await pullBranch();
64
  return NextResponse.json({ success: true });
65
 
66
  case "checkout":
67
  if (!body.branch) return NextResponse.json({ error: "Branch required" }, { status: 400 });
68
- await checkoutBranch(body.branch, body.create);
69
  return NextResponse.json({ success: true });
70
 
71
  case "setConfig":
72
  if (!body.name || !body.email) return NextResponse.json({ error: "Name and email required" }, { status: 400 });
73
- await git.addConfig("user.name", body.name, false, "local");
74
- await git.addConfig("user.email", body.email, false, "local");
 
75
  return NextResponse.json({ success: true });
76
 
77
  default:
78
  return NextResponse.json({ error: "Invalid action" }, { status: 400 });
79
  }
80
  } catch (e: unknown) {
81
- const error = e instanceof Error ? e : new Error(String(e));
82
- return NextResponse.json({ error: error.message }, { status: 500 });
83
  }
84
  }
 
7
  pushBranch,
8
  pullBranch,
9
  checkoutBranch,
10
+ getGit
11
  } from "@/lib/git";
12
+ import { auth } from "@/auth";
13
+ import { resolveSafePath } from "@/lib/fs/isolation";
14
 
15
  export async function GET(req: NextRequest) {
16
+ const session = await auth();
17
+ if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
18
+
19
  const { searchParams } = new URL(req.url);
20
  const action = searchParams.get("action");
21
+ const workspaceName = searchParams.get("workspaceName");
22
+
23
+ if (!workspaceName) return NextResponse.json({ error: "workspaceName required" }, { status: 400 });
24
 
25
  try {
26
+ const userId = session.user.id;
27
+ const baseDir = await resolveSafePath(userId, workspaceName);
28
+
29
  switch (action) {
30
  case "status":
31
+ return NextResponse.json(await getGitStatus(baseDir));
32
  case "branches":
33
+ return NextResponse.json(await getBranchList(baseDir));
34
  case "diff":
35
  const file = searchParams.get("file");
36
  if (!file) return NextResponse.json({ error: "File required" }, { status: 400 });
37
+ return NextResponse.json({ diff: await getFileDiff(file, baseDir) });
38
  case "log":
39
+ const log = await getGit(baseDir).log({ maxCount: 50 });
40
  return NextResponse.json(log.all);
41
  case "checkConfig":
42
+ const instance = getGit(baseDir);
43
+ const name = await instance.getConfig("user.name", "local");
44
+ const email = await instance.getConfig("user.email", "local");
45
  return NextResponse.json({
46
  configured: !!(name.value && email.value),
47
  name: name.value,
 
51
  return NextResponse.json({ error: "Invalid action" }, { status: 400 });
52
  }
53
  } catch (e: unknown) {
54
+ return NextResponse.json({ error: (e as Error).message }, { status: 500 });
 
55
  }
56
  }
57
 
58
  export async function POST(req: NextRequest) {
59
+ const session = await auth();
60
+ if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
61
+
62
  try {
63
  const body = await req.json();
64
+ const { action, workspaceName } = body;
65
+ if (!workspaceName) return NextResponse.json({ error: "Workspace required" }, { status: 400 });
66
+
67
+ const baseDir = await resolveSafePath(session.user.id, workspaceName);
68
 
69
  switch (action) {
70
  case "commit":
71
  if (!body.message) return NextResponse.json({ error: "Message required" }, { status: 400 });
72
+ const res = await commitFiles(body.message, body.files, baseDir);
73
  return NextResponse.json({ success: true, commit: res.commit });
74
 
75
  case "push":
76
+ await pushBranch(baseDir);
77
  return NextResponse.json({ success: true });
78
 
79
  case "pull":
80
+ await pullBranch(baseDir);
81
  return NextResponse.json({ success: true });
82
 
83
  case "checkout":
84
  if (!body.branch) return NextResponse.json({ error: "Branch required" }, { status: 400 });
85
+ await checkoutBranch(body.branch, body.create, baseDir);
86
  return NextResponse.json({ success: true });
87
 
88
  case "setConfig":
89
  if (!body.name || !body.email) return NextResponse.json({ error: "Name and email required" }, { status: 400 });
90
+ const instance = getGit(baseDir);
91
+ await instance.addConfig("user.name", body.name, false, "local");
92
+ await instance.addConfig("user.email", body.email, false, "local");
93
  return NextResponse.json({ success: true });
94
 
95
  default:
96
  return NextResponse.json({ error: "Invalid action" }, { status: 400 });
97
  }
98
  } catch (e: unknown) {
99
+ return NextResponse.json({ error: (e as Error).message }, { status: 500 });
 
100
  }
101
  }
app/api/lsp/route.ts CHANGED
@@ -1,5 +1,7 @@
1
  import { NextRequest, NextResponse } from "next/server";
2
  import { spawn } from "child_process";
 
 
3
 
4
  // Language Server mapping: extension → command to start the language server
5
  const LSP_SERVERS: Record<string, { cmd: string; args: string[] }> = {
@@ -15,6 +17,10 @@ const LSP_SERVERS: Record<string, { cmd: string; args: string[] }> = {
15
  const activeServers = new Map<string, ReturnType<typeof spawn>>();
16
 
17
  export async function GET(req: NextRequest) {
 
 
 
 
18
  const { searchParams } = new URL(req.url);
19
  const lang = searchParams.get("lang");
20
 
@@ -22,7 +28,7 @@ export async function GET(req: NextRequest) {
22
  return NextResponse.json({
23
  available: Object.keys(LSP_SERVERS),
24
  status: Object.fromEntries(
25
- Object.keys(LSP_SERVERS).map(l => [l, activeServers.has(l) ? "running" : "idle"])
26
  )
27
  });
28
  }
@@ -40,49 +46,57 @@ export async function GET(req: NextRequest) {
40
  }
41
 
42
  export async function POST(req: NextRequest) {
 
 
 
 
43
  try {
44
- const { action, lang } = await req.json();
45
 
46
  if (action === "start") {
47
  if (!lang) return NextResponse.json({ error: "lang required" }, { status: 400 });
 
48
 
49
  const config = LSP_SERVERS[lang];
50
  if (!config) return NextResponse.json({ error: `No LSP for .${lang}` }, { status: 404 });
51
 
52
- if (activeServers.has(lang)) {
 
53
  return NextResponse.json({ status: "already_running" });
54
  }
55
 
56
  try {
 
57
  const proc = spawn(config.cmd, config.args, {
58
- cwd: process.cwd(),
59
  stdio: ["pipe", "pipe", "pipe"],
60
  shell: process.platform === "win32"
61
  });
62
 
63
- activeServers.set(lang, proc);
64
 
65
- proc.on("exit", () => activeServers.delete(lang));
66
  proc.on("error", (err) => {
67
- console.warn(`LSP ${lang}: ${err.message} — install ${config.cmd} to enable`);
68
- activeServers.delete(lang);
69
  });
70
 
71
  return NextResponse.json({ status: "started", pid: proc.pid });
72
- } catch {
73
  return NextResponse.json({
74
  status: "unavailable",
75
- message: `Install '${config.cmd}' to enable LSP for .${lang} files.`
76
  });
77
  }
78
  }
79
 
80
  if (action === "stop") {
81
  if (!lang) return NextResponse.json({ error: "lang required" }, { status: 400 });
82
- const proc = activeServers.get(lang);
 
83
  if (proc) {
84
  proc.kill();
85
- activeServers.delete(lang);
86
  }
87
  return NextResponse.json({ status: "stopped" });
88
  }
 
1
  import { NextRequest, NextResponse } from "next/server";
2
  import { spawn } from "child_process";
3
+ import { auth } from "@/auth";
4
+ import { resolveSafePath } from "@/lib/fs/isolation";
5
 
6
  // Language Server mapping: extension → command to start the language server
7
  const LSP_SERVERS: Record<string, { cmd: string; args: string[] }> = {
 
17
  const activeServers = new Map<string, ReturnType<typeof spawn>>();
18
 
19
  export async function GET(req: NextRequest) {
20
+ const session = await auth();
21
+ if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
22
+ const userId = session.user.id;
23
+
24
  const { searchParams } = new URL(req.url);
25
  const lang = searchParams.get("lang");
26
 
 
28
  return NextResponse.json({
29
  available: Object.keys(LSP_SERVERS),
30
  status: Object.fromEntries(
31
+ Object.keys(LSP_SERVERS).map(l => [l, activeServers.has(`${userId}:${l}`) ? "running" : "idle"])
32
  )
33
  });
34
  }
 
46
  }
47
 
48
  export async function POST(req: NextRequest) {
49
+ const session = await auth();
50
+ if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
51
+ const userId = session.user.id;
52
+
53
  try {
54
+ const { action, lang, workspaceName } = await req.json();
55
 
56
  if (action === "start") {
57
  if (!lang) return NextResponse.json({ error: "lang required" }, { status: 400 });
58
+ if (!workspaceName) return NextResponse.json({ error: "workspaceName required" }, { status: 400 });
59
 
60
  const config = LSP_SERVERS[lang];
61
  if (!config) return NextResponse.json({ error: `No LSP for .${lang}` }, { status: 404 });
62
 
63
+ const serverKey = `${userId}:${lang}`;
64
+ if (activeServers.has(serverKey)) {
65
  return NextResponse.json({ status: "already_running" });
66
  }
67
 
68
  try {
69
+ const projectRoot = await resolveSafePath(userId, workspaceName);
70
  const proc = spawn(config.cmd, config.args, {
71
+ cwd: projectRoot,
72
  stdio: ["pipe", "pipe", "pipe"],
73
  shell: process.platform === "win32"
74
  });
75
 
76
+ activeServers.set(serverKey, proc);
77
 
78
+ proc.on("exit", () => activeServers.delete(serverKey));
79
  proc.on("error", (err) => {
80
+ console.warn(`LSP ${lang} for ${userId}: ${err.message}`);
81
+ activeServers.delete(serverKey);
82
  });
83
 
84
  return NextResponse.json({ status: "started", pid: proc.pid });
85
+ } catch (e: unknown) {
86
  return NextResponse.json({
87
  status: "unavailable",
88
+ message: (e as Error).message
89
  });
90
  }
91
  }
92
 
93
  if (action === "stop") {
94
  if (!lang) return NextResponse.json({ error: "lang required" }, { status: 400 });
95
+ const serverKey = `${userId}:${lang}`;
96
+ const proc = activeServers.get(serverKey);
97
  if (proc) {
98
  proc.kill();
99
+ activeServers.delete(serverKey);
100
  }
101
  return NextResponse.json({ status: "stopped" });
102
  }
app/api/projects/route.ts CHANGED
@@ -1,5 +1,4 @@
1
  import { NextRequest, NextResponse } from "next/server";
2
- import path from "path";
3
  import fs from "fs/promises";
4
  import simpleGit from "simple-git";
5
  import { spawn } from "child_process";
@@ -8,11 +7,9 @@ import { auth } from "@/auth";
8
  import { db } from "@/lib/db";
9
  import { randomUUID } from "crypto";
10
 
11
- const WORKSPACE_ROOT = path.join(process.cwd(), "workspaces");
12
 
13
- async function ensureWorkspaceRoot() {
14
- await fs.mkdir(WORKSPACE_ROOT, { recursive: true });
15
- }
16
 
17
  // POST /api/projects
18
  export async function POST(req: NextRequest) {
@@ -37,7 +34,11 @@ export async function POST(req: NextRequest) {
37
  }
38
 
39
  export async function GET() {
40
- return handleList();
 
 
 
 
41
  }
42
 
43
  export async function DELETE(req: NextRequest) {
@@ -65,13 +66,9 @@ export async function DELETE(req: NextRequest) {
65
  }
66
 
67
  const projectName = res.rows[0].project_name as string;
68
- const safeName = projectName.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60);
69
-
70
- // Remove from filesystem
71
- const targetPath = path.join(WORKSPACE_ROOT, safeName);
72
- if (targetPath.startsWith(WORKSPACE_ROOT)) {
73
- await fs.rm(targetPath, { recursive: true, force: true });
74
- }
75
 
76
  // Remove from database
77
  await db.execute({
@@ -88,30 +85,29 @@ export async function DELETE(req: NextRequest) {
88
  }
89
  }
90
 
91
- async function handleList() {
92
- const session = await auth();
93
- if (!session?.user?.id) {
94
- return NextResponse.json({ projects: [] });
95
- }
96
-
97
  try {
98
  const res = await db.execute({
99
  sql: "SELECT * FROM workspaces WHERE user_id = ?",
100
- args: [session.user.id]
101
  });
102
 
103
- const projects = res.rows.map(row => ({
104
- id: row.id,
105
- name: row.project_name,
106
- path: path.join(WORKSPACE_ROOT, (row.project_name as string).replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60)),
107
- containerStatus: row.status,
108
- gitRemote: "", // We could fetch this on demand or ignore it for the DB view
109
- hasPackageJson: true,
110
- starred: false
 
 
 
111
  }));
112
 
113
  return NextResponse.json({ projects });
114
- } catch {
 
115
  return NextResponse.json({ projects: [] });
116
  }
117
  }
@@ -122,9 +118,8 @@ async function handleClone(req: NextRequest, userId: string) {
122
  return NextResponse.json({ error: "repoUrl and projectName are required" }, { status: 400 });
123
  }
124
 
125
- await ensureWorkspaceRoot();
126
  const safeName = projectName.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60);
127
- const dest = path.join(WORKSPACE_ROOT, safeName);
128
 
129
  // Stream progress via SSE
130
  const encoder = new TextEncoder();
@@ -173,9 +168,8 @@ async function handleScaffold(req: NextRequest, userId: string) {
173
  return NextResponse.json({ error: "templateId and projectName are required" }, { status: 400 });
174
  }
175
 
176
- await ensureWorkspaceRoot();
177
  const safeName = projectName.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60);
178
- const dest = path.join(WORKSPACE_ROOT, safeName);
179
  await fs.mkdir(dest, { recursive: true });
180
 
181
  // Template scaffold commands (IDs must exactly match TEMPLATE_REGISTRY ids in constants/extensions.ts)
 
1
  import { NextRequest, NextResponse } from "next/server";
 
2
  import fs from "fs/promises";
3
  import simpleGit from "simple-git";
4
  import { spawn } from "child_process";
 
7
  import { db } from "@/lib/db";
8
  import { randomUUID } from "crypto";
9
 
10
+ import { resolveSafePath } from "@/lib/fs/isolation";
11
 
12
+ // Removed global WORKSPACE_ROOT
 
 
13
 
14
  // POST /api/projects
15
  export async function POST(req: NextRequest) {
 
34
  }
35
 
36
  export async function GET() {
37
+ const session = await auth();
38
+ if (!session?.user?.id) {
39
+ return NextResponse.json({ projects: [] });
40
+ }
41
+ return handleList(session.user.id);
42
  }
43
 
44
  export async function DELETE(req: NextRequest) {
 
66
  }
67
 
68
  const projectName = res.rows[0].project_name as string;
69
+ const targetPath = await resolveSafePath(session.user.id, projectName.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60));
70
+
71
+ await fs.rm(targetPath, { recursive: true, force: true });
 
 
 
 
72
 
73
  // Remove from database
74
  await db.execute({
 
85
  }
86
  }
87
 
88
+ async function handleList(userId: string) {
 
 
 
 
 
89
  try {
90
  const res = await db.execute({
91
  sql: "SELECT * FROM workspaces WHERE user_id = ?",
92
+ args: [userId]
93
  });
94
 
95
+ const projects = await Promise.all(res.rows.map(async row => {
96
+ const safeName = (row.project_name as string).replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60);
97
+ return {
98
+ id: row.id,
99
+ name: row.project_name,
100
+ path: await resolveSafePath(userId, safeName),
101
+ containerStatus: row.status,
102
+ gitRemote: "",
103
+ hasPackageJson: true,
104
+ starred: false
105
+ };
106
  }));
107
 
108
  return NextResponse.json({ projects });
109
+ } catch (e) {
110
+ console.error("[PROJECTS_LIST_ERROR]", e);
111
  return NextResponse.json({ projects: [] });
112
  }
113
  }
 
118
  return NextResponse.json({ error: "repoUrl and projectName are required" }, { status: 400 });
119
  }
120
 
 
121
  const safeName = projectName.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60);
122
+ const dest = await resolveSafePath(userId, safeName);
123
 
124
  // Stream progress via SSE
125
  const encoder = new TextEncoder();
 
168
  return NextResponse.json({ error: "templateId and projectName are required" }, { status: 400 });
169
  }
170
 
 
171
  const safeName = projectName.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60);
172
+ const dest = await resolveSafePath(userId, safeName);
173
  await fs.mkdir(dest, { recursive: true });
174
 
175
  // Template scaffold commands (IDs must exactly match TEMPLATE_REGISTRY ids in constants/extensions.ts)
app/api/workspace/route.ts CHANGED
@@ -49,9 +49,9 @@ export async function POST(req: Request) {
49
  return NextResponse.json({ error: "Missing workspace id" }, { status: 400 });
50
  }
51
 
52
- // Verify ownership
53
  const verifyObj = await db.execute({
54
- sql: "SELECT id FROM workspaces WHERE id = ? AND user_id = ?",
55
  args: [id, session.user.id]
56
  });
57
 
@@ -59,9 +59,17 @@ export async function POST(req: Request) {
59
  return NextResponse.json({ error: "Workspace not found or unauthorized" }, { status: 404 });
60
  }
61
 
 
 
62
  if (action === "start") {
63
  const { withAndroidEmulator } = body;
64
- const result = await startWorkspaceContainer({ id, image, withAndroidEmulator });
 
 
 
 
 
 
65
  if (result.success) {
66
  await db.execute({
67
  sql: "UPDATE workspaces SET status = 'running', container_id = ?, android_container_id = ?, android_port = ? WHERE id = ?",
@@ -84,7 +92,13 @@ export async function POST(req: Request) {
84
  await stopWorkspaceContainer(id, true);
85
 
86
  // 2. Recreate them (this will pick up codeverse.json changes)
87
- const result = await startWorkspaceContainer({ id, image, withAndroidEmulator });
 
 
 
 
 
 
88
 
89
  if (result.success) {
90
  await db.execute({
 
49
  return NextResponse.json({ error: "Missing workspace id" }, { status: 400 });
50
  }
51
 
52
+ // Verify ownership and get project name
53
  const verifyObj = await db.execute({
54
+ sql: "SELECT id, project_name FROM workspaces WHERE id = ? AND user_id = ?",
55
  args: [id, session.user.id]
56
  });
57
 
 
59
  return NextResponse.json({ error: "Workspace not found or unauthorized" }, { status: 404 });
60
  }
61
 
62
+ const projectName = verifyObj.rows[0].project_name as string;
63
+
64
  if (action === "start") {
65
  const { withAndroidEmulator } = body;
66
+ const result = await startWorkspaceContainer({
67
+ id,
68
+ userId: session.user.id,
69
+ projectName,
70
+ image,
71
+ withAndroidEmulator
72
+ });
73
  if (result.success) {
74
  await db.execute({
75
  sql: "UPDATE workspaces SET status = 'running', container_id = ?, android_container_id = ?, android_port = ? WHERE id = ?",
 
92
  await stopWorkspaceContainer(id, true);
93
 
94
  // 2. Recreate them (this will pick up codeverse.json changes)
95
+ const result = await startWorkspaceContainer({
96
+ id,
97
+ userId: session.user.id,
98
+ projectName,
99
+ image,
100
+ withAndroidEmulator
101
+ });
102
 
103
  if (result.success) {
104
  await db.execute({
app/api/workspace/stream/route.ts CHANGED
@@ -1,9 +1,15 @@
1
  import { NextRequest } from 'next/server';
2
  import { startWorkspaceContainer } from '@/lib/docker/manager';
 
 
3
 
4
  export const dynamic = 'force-dynamic';
5
 
6
  export async function GET(req: NextRequest) {
 
 
 
 
7
  const searchParams = req.nextUrl.searchParams;
8
  const id = searchParams.get('id');
9
  const withAndroid = searchParams.get('withAndroid') === 'true';
@@ -12,6 +18,18 @@ export async function GET(req: NextRequest) {
12
  return new Response('Missing workspace id', { status: 400 });
13
  }
14
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  const encoder = new TextEncoder();
16
 
17
  const stream = new ReadableStream({
@@ -24,9 +42,13 @@ export async function GET(req: NextRequest) {
24
 
25
  try {
26
  // Initialize workspace and pipe logs directly from the Docker builder engine to SSE client
27
- const result = await startWorkspaceContainer(
28
- { id, withAndroidEmulator: withAndroid, onLog: (msg) => sendEvent('log', msg) }
29
- );
 
 
 
 
30
 
31
  // Completed
32
  sendEvent('ready', {
 
1
  import { NextRequest } from 'next/server';
2
  import { startWorkspaceContainer } from '@/lib/docker/manager';
3
+ import { auth } from '@/auth';
4
+ import { db } from '@/lib/db';
5
 
6
  export const dynamic = 'force-dynamic';
7
 
8
  export async function GET(req: NextRequest) {
9
+ const session = await auth();
10
+ if (!session?.user?.id) return new Response('Unauthorized', { status: 401 });
11
+ const userId = session.user.id;
12
+
13
  const searchParams = req.nextUrl.searchParams;
14
  const id = searchParams.get('id');
15
  const withAndroid = searchParams.get('withAndroid') === 'true';
 
18
  return new Response('Missing workspace id', { status: 400 });
19
  }
20
 
21
+ // Verify ownership and get project name
22
+ const verifyObj = await db.execute({
23
+ sql: "SELECT project_name FROM workspaces WHERE id = ? AND user_id = ?",
24
+ args: [id, session.user.id]
25
+ });
26
+
27
+ if (verifyObj.rows.length === 0) {
28
+ return new Response('Workspace not found or unauthorized', { status: 404 });
29
+ }
30
+
31
+ const projectName = verifyObj.rows[0].project_name as string;
32
+
33
  const encoder = new TextEncoder();
34
 
35
  const stream = new ReadableStream({
 
42
 
43
  try {
44
  // Initialize workspace and pipe logs directly from the Docker builder engine to SSE client
45
+ const result = await startWorkspaceContainer({
46
+ id: id as string,
47
+ userId: userId,
48
+ projectName: projectName,
49
+ withAndroidEmulator: withAndroid,
50
+ onLog: (msg) => sendEvent('log', msg)
51
+ });
52
 
53
  // Completed
54
  sendEvent('ready', {
auth.ts CHANGED
@@ -205,6 +205,7 @@ const TursoAdapter = {
205
  const authOptions = {
206
  adapter: TursoAdapter as unknown as Adapter,
207
  trustHost: true,
 
208
  providers: [
209
  GitHubProvider({
210
  clientId: process.env.GITHUB_ID ?? "",
 
205
  const authOptions = {
206
  adapter: TursoAdapter as unknown as Adapter,
207
  trustHost: true,
208
+ secret: process.env.AUTH_SECRET,
209
  providers: [
210
  GitHubProvider({
211
  clientId: process.env.GITHUB_ID ?? "",
components/dashboard/Dashboard.tsx CHANGED
@@ -190,8 +190,8 @@ export default function Dashboard() {
190
  <div className="h-4 w-px bg-(--border)" />
191
  <div className="flex items-center gap-3 pl-1">
192
  <div className="flex flex-col items-end">
193
- <span className="text-sm font-semibold text-(--text) leading-tight">{session?.user?.name || "Developer"}</span>
194
- <span className="text-[10px] text-(--text-muted) font-mono">{session?.user?.email || "studio@domain.com"}</span>
195
  </div>
196
  {session?.user?.image ? (
197
  <Image unoptimized src={session.user.image} alt="Profile" width={36} height={36} className="w-9 h-9 rounded-full ring-2 ring-(--border-subtle)" />
@@ -211,7 +211,7 @@ export default function Dashboard() {
211
  {/* Greeting */}
212
  <div>
213
  <h1 className="text-2xl font-bold text-(--text)">
214
- Hello, <span className="text-(--accent)">Developer</span>
215
  </h1>
216
  <p className="text-(--text-2) text-sm mt-1">Welcome back to CodeVerse Studio</p>
217
  </div>
 
190
  <div className="h-4 w-px bg-(--border)" />
191
  <div className="flex items-center gap-3 pl-1">
192
  <div className="flex flex-col items-end">
193
+ <span className="text-sm font-semibold text-(--text) leading-tight">{session?.user?.name || "User"}</span>
194
+ <span className="text-[10px] text-(--text-muted) font-mono">{session?.user?.email || "anonymous@codeverse.io"}</span>
195
  </div>
196
  {session?.user?.image ? (
197
  <Image unoptimized src={session.user.image} alt="Profile" width={36} height={36} className="w-9 h-9 rounded-full ring-2 ring-(--border-subtle)" />
 
211
  {/* Greeting */}
212
  <div>
213
  <h1 className="text-2xl font-bold text-(--text)">
214
+ Hello, <span className="text-(--accent)">{session?.user?.name?.split(" ")[0] || "User"}</span>
215
  </h1>
216
  <p className="text-(--text-2) text-sm mt-1">Welcome back to CodeVerse Studio</p>
217
  </div>
components/workspace/AIAssistantSidebar.tsx ADDED
@@ -0,0 +1,385 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useChat } from "@ai-sdk/react";
4
+ import React, { useState, useRef, useEffect, useMemo, ChangeEvent, FormEvent } from "react";
5
+ import { motion, AnimatePresence } from "framer-motion";
6
+ import {
7
+ Bot,
8
+ User,
9
+ Send,
10
+ X,
11
+ Plus,
12
+ Wand2,
13
+ Trash2,
14
+ Terminal,
15
+ FileText,
16
+ Search as SearchIcon,
17
+ Check,
18
+ MessageSquare,
19
+ } from "lucide-react";
20
+ import ReactMarkdown from "react-markdown";
21
+ import remarkGfm from "remark-gfm";
22
+ import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
23
+ import { vscDarkPlus } from "react-syntax-highlighter/dist/cjs/styles/prism";
24
+
25
+ // Standard AI Studio message shape for strict typing
26
+ export interface StudioMessage {
27
+ id: string;
28
+ role: "system" | "user" | "assistant" | "data" | "tool";
29
+ content: string;
30
+ toolInvocations?: StudioToolInvocation[];
31
+ createdAt?: Date;
32
+ }
33
+
34
+ export interface StudioToolInvocation {
35
+ state: "call" | "result";
36
+ toolCallId: string;
37
+ toolName: string;
38
+ args: Record<string, unknown>;
39
+ result?: unknown;
40
+ }
41
+
42
+ // Interface for the hook results to satisfy TSC without 'any'
43
+ interface ChatHelpers {
44
+ messages: StudioMessage[];
45
+ input: string;
46
+ handleInputChange: (e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => void;
47
+ handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
48
+ isLoading: boolean;
49
+ setMessages: (messages: StudioMessage[]) => void;
50
+ }
51
+
52
+ interface AIAssistantSidebarProps {
53
+ workspaceName: string;
54
+ isOpen: boolean;
55
+ onClose: () => void;
56
+ }
57
+
58
+ interface PlanStep {
59
+ id: string;
60
+ description: string;
61
+ filesData?: string[];
62
+ }
63
+
64
+ interface PlanContent {
65
+ goal: string;
66
+ steps: PlanStep[];
67
+ }
68
+
69
+ export function AIAssistantSidebar({ workspaceName, isOpen, onClose }: AIAssistantSidebarProps) {
70
+ const [isPlanMode, setIsPlanMode] = useState(false);
71
+ const scrollRef = useRef<HTMLDivElement>(null);
72
+
73
+ const welcomeMsg: StudioMessage = {
74
+ id: "welcome",
75
+ role: "assistant",
76
+ content: `Hi! I'm your CodeVerse AI Assistant. I can help you explore this project, write code, or run terminal commands. How can I help you with **${workspaceName}** today?`,
77
+ };
78
+
79
+ // Safe bridge to bypass AI SDK v3 type-brand mismatches while keeping internals strict
80
+ const chatOptions = {
81
+ api: "/api/agent",
82
+ body: {
83
+ workspaceName,
84
+ mode: isPlanMode ? "plan" : "execute",
85
+ },
86
+ };
87
+
88
+ // Casting the hook result to our strictly-typed internal interface
89
+ const chat = useChat(chatOptions as unknown as Parameters<typeof useChat>[0]) as unknown as ChatHelpers;
90
+
91
+ const {
92
+ messages: chatMessages,
93
+ input,
94
+ handleInputChange,
95
+ handleSubmit,
96
+ isLoading,
97
+ setMessages
98
+ } = chat;
99
+
100
+ // Fetch history on mount
101
+ useEffect(() => {
102
+ const fetchHistory = async () => {
103
+ try {
104
+ const res = await fetch(`/api/agent?workspaceName=${workspaceName}`);
105
+ if (res.ok) {
106
+ const data = await res.json() as { messages: StudioMessage[] };
107
+ if (data.messages && data.messages.length > 0) {
108
+ setMessages(data.messages);
109
+ }
110
+ }
111
+ } catch (err) {
112
+ console.error("Failed to fetch chat history:", err);
113
+ }
114
+ };
115
+ fetchHistory();
116
+ }, [workspaceName, setMessages]);
117
+
118
+ const clearHistory = () => {
119
+ setMessages([]);
120
+ };
121
+
122
+ const messages = useMemo<StudioMessage[]>(
123
+ () => (chatMessages.length === 0 ? [welcomeMsg] : chatMessages),
124
+ // eslint-disable-next-line react-hooks/exhaustive-deps
125
+ [chatMessages, workspaceName]
126
+ );
127
+
128
+ useEffect(() => {
129
+ if (scrollRef.current) {
130
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
131
+ }
132
+ }, [messages]);
133
+
134
+ const renderToolInvocation = (tool: StudioToolInvocation) => {
135
+ const isDone = tool.state === "result";
136
+ const iconMap: Record<string, React.ReactNode> = {
137
+ read_file: <FileText size={14} />,
138
+ write_file: <Plus size={14} />,
139
+ terminal_command: <Terminal size={14} />,
140
+ search_code: <SearchIcon size={14} />,
141
+ list_files: <MessageSquare size={14} />,
142
+ };
143
+ const icon = iconMap[tool.toolName] || <Wand2 size={14} />;
144
+
145
+ return (
146
+ <div key={tool.toolCallId} className="flex flex-col gap-2 p-3 my-2 rounded-xl bg-(--bg-2) border border-(--border-subtle) overflow-hidden shadow-sm">
147
+ <div className="flex items-center justify-between">
148
+ <div className="flex items-center gap-2 text-[10px] font-bold text-(--text-2) uppercase tracking-wider">
149
+ <span className={isDone ? "text-(--success)" : "text-(--accent) animate-pulse"}>
150
+ {icon}
151
+ </span>
152
+ <span>{tool.toolName.replace(/_/g, " ")}</span>
153
+ </div>
154
+ {isDone && <Check size={12} className="text-(--success)" />}
155
+ </div>
156
+
157
+ {!isDone && (
158
+ <div className="text-[10px] font-mono text-(--text-muted) truncate opacity-70 italic">
159
+ {JSON.stringify(tool.args)}
160
+ </div>
161
+ )}
162
+
163
+ {isDone && !!tool.result && (
164
+ <div className="text-[10px] font-mono bg-(--bg-3) p-2 rounded border border-(--border) text-(--text-muted) max-h-32 overflow-y-auto custom-scrollbar whitespace-pre-wrap">
165
+ {typeof tool.result === "string"
166
+ ? (tool.result.length > 500 ? tool.result.slice(0, 500) + "..." : tool.result)
167
+ : JSON.stringify(tool.result, null, 2).slice(0, 500) + (JSON.stringify(tool.result).length > 500 ? "..." : "")}
168
+ </div>
169
+ )}
170
+ </div>
171
+ );
172
+ };
173
+
174
+ return (
175
+ <AnimatePresence>
176
+ {isOpen && (
177
+ <motion.div
178
+ initial={{ x: "100%" }}
179
+ animate={{ x: 0 }}
180
+ exit={{ x: "100%" }}
181
+ transition={{ type: "spring", damping: 28, stiffness: 220 }}
182
+ className="fixed top-0 right-0 h-full w-[440px] bg-(--surface) border-l border-(--border) shadow-2xl z-50 flex flex-col glassmorphism overflow-hidden"
183
+ >
184
+ {/* Header */}
185
+ <div className="p-4 border-b border-(--border-subtle) flex items-center justify-between bg-(--bg-2) bg-opacity-70 backdrop-blur-md">
186
+ <div className="flex items-center gap-3">
187
+ <div className="w-9 h-9 rounded-xl bg-(--accent) bg-opacity-10 flex items-center justify-center text-(--accent) shadow-inner border border-(--accent) border-opacity-20">
188
+ <Bot size={22} />
189
+ </div>
190
+ <div className="flex flex-col">
191
+ <span className="text-sm font-black text-(--text) tracking-tight">AI STUDIO</span>
192
+ <span className="text-[10px] text-(--text-muted) flex items-center gap-1 uppercase font-bold tracking-tighter">
193
+ <div className="w-1.5 h-1.5 rounded-full bg-(--success) animate-pulse shadow-[0_0_8px_var(--success)]" />
194
+ Isolated • {workspaceName}
195
+ </span>
196
+ </div>
197
+ </div>
198
+ <div className="flex items-center gap-1.5">
199
+ <button
200
+ onClick={() => setIsPlanMode(!isPlanMode)}
201
+ className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full text-[10px] font-black transition-all border shadow-sm ${
202
+ isPlanMode
203
+ ? "bg-(--warning) bg-opacity-15 text-(--warning) border-(--warning) border-opacity-40"
204
+ : "bg-(--bg-3) text-(--text-muted) border-transparent hover:text-(--text) hover:bg-(--surface-hover)"
205
+ }`}
206
+ >
207
+ <Wand2 size={12} />
208
+ {isPlanMode ? "PLAN" : "DIRECT"}
209
+ </button>
210
+ <div className="w-px h-4 bg-(--border-subtle) mx-1" />
211
+ <button
212
+ onClick={clearHistory}
213
+ className="p-2 hover:bg-(--error) hover:bg-opacity-10 rounded-xl transition-all text-(--text-muted) hover:text-(--error)"
214
+ title="Clear History"
215
+ >
216
+ <Trash2 size={18} />
217
+ </button>
218
+ <button
219
+ onClick={onClose}
220
+ className="p-2 hover:bg-(--surface-hover) rounded-xl transition-all text-(--text-muted) hover:text-(--text)"
221
+ >
222
+ <X size={22} />
223
+ </button>
224
+ </div>
225
+ </div>
226
+
227
+ {/* Messages Container */}
228
+ <div ref={scrollRef} className="flex-1 overflow-y-auto p-5 space-y-8 custom-scrollbar scroll-smooth">
229
+ {messages.map((m) => (
230
+ <div key={m.id} className={`flex gap-4 ${m.role === "user" ? "flex-row-reverse" : ""}`}>
231
+ <div className={`w-9 h-9 rounded-xl shrink-0 flex items-center justify-center shadow-sm border ${
232
+ m.role === "user"
233
+ ? "bg-(--bg-3) text-(--text) border-(--border-subtle)"
234
+ : "bg-(--accent) bg-opacity-10 text-(--accent) border-(--accent) border-opacity-20"
235
+ }`}>
236
+ {m.role === "user" ? <User size={18} /> : <Bot size={18} />}
237
+ </div>
238
+
239
+ <div className={`flex flex-col gap-2 max-w-[85%] ${m.role === "user" ? "items-end" : ""}`}>
240
+ <div className={`px-4 py-3.5 rounded-2xl text-sm leading-relaxed shadow-md border animate-in fade-in slide-in-from-bottom-2 duration-300 ${
241
+ m.role === "user"
242
+ ? "bg-(--accent) text-white rounded-tr-sm border-transparent"
243
+ : "bg-(--bg-2) text-(--text) rounded-tl-sm border-(--border-subtle)"
244
+ }`}>
245
+ {m.role === "assistant" && m.content.startsWith("{") && m.content.includes("goal") ? (
246
+ <div className="flex flex-col gap-4 py-1">
247
+ <div className="flex items-center gap-2 text-(--accent) font-black uppercase tracking-[0.2em] text-[9px]">
248
+ <Check size={12} className="stroke-[3px]" /> System Plan Generated
249
+ </div>
250
+ {(() => {
251
+ try {
252
+ const plan = JSON.parse(m.content) as PlanContent;
253
+ return (
254
+ <div className="space-y-4">
255
+ <div className="font-bold text-sm text-(--text) bg-(--bg-3) p-3 rounded-xl border border-(--border-subtle) shadow-inner">
256
+ {String(plan.goal)}
257
+ </div>
258
+ <div className="space-y-2.5">
259
+ {(plan.steps || []).map((s, i) => (
260
+ <div key={s.id || String(i)} className="flex gap-3 p-3 rounded-xl bg-(--surface) bg-opacity-50 hover:bg-(--bg-3) transition-all border border-(--border-subtle) group">
261
+ <span className="flex items-center justify-center w-6 h-6 rounded-lg bg-(--accent) bg-opacity-10 text-(--accent) text-[11px] font-black shrink-0 group-hover:scale-110 transition-transform">
262
+ {i+1}
263
+ </span>
264
+ <span className="text-xs text-(--text-2) font-medium leading-[1.6]">{String(s.description)}</span>
265
+ </div>
266
+ ))}
267
+ </div>
268
+ </div>
269
+ );
270
+ } catch { return <ReactMarkdown remarkPlugins={[remarkGfm]}>{m.content}</ReactMarkdown>; }
271
+ })()}
272
+ </div>
273
+ ) : (
274
+ <div className="prose prose-sm prose-invert max-w-full">
275
+ <ReactMarkdown
276
+ remarkPlugins={[remarkGfm]}
277
+ components={{
278
+ code({ className, children, ...props }: React.ComponentPropsWithoutRef<'code'>) {
279
+ const match = /language-(\w+)/.exec(className || "");
280
+ const isInline = !match;
281
+ return isInline ? (
282
+ <code className="bg-(--bg-3) px-1.5 py-0.5 rounded-md text-(--accent) font-mono text-[0.9em]" {...props}>
283
+ {children}
284
+ </code>
285
+ ) : (
286
+ <div className="my-4 rounded-xl overflow-hidden border border-(--border-subtle) shadow-lg">
287
+ <div className="bg-(--bg-3) px-4 py-1.5 border-b border-(--border-subtle) flex items-center justify-between">
288
+ <span className="text-[10px] font-black text-(--text-muted) uppercase tracking-widest">{match[1]}</span>
289
+ </div>
290
+ <SyntaxHighlighter
291
+ style={vscDarkPlus}
292
+ language={match[1]}
293
+ PreTag="div"
294
+ className="m-0! text-xs! bg-(--bg-2)!"
295
+ >
296
+ {String(children).replace(/\n$/, "")}
297
+ </SyntaxHighlighter>
298
+ </div>
299
+ );
300
+ },
301
+ }}
302
+ >
303
+ {m.content}
304
+ </ReactMarkdown>
305
+ </div>
306
+ )}
307
+ </div>
308
+
309
+ {/* Tool Invocations */}
310
+ {m.toolInvocations?.map((tool: StudioToolInvocation) => renderToolInvocation(tool))}
311
+ </div>
312
+ </div>
313
+ ))}
314
+
315
+ {isLoading && (
316
+ <div className="flex items-center gap-4 animate-in fade-in duration-500">
317
+ <div className="w-9 h-9 rounded-xl bg-(--accent) bg-opacity-10 flex items-center justify-center text-(--accent) border border-(--accent) border-opacity-10">
318
+ <Bot size={18} className="animate-pulse" />
319
+ </div>
320
+ <div className="flex gap-1.5 p-3 px-4 bg-(--bg-2) rounded-2xl border border-(--border-subtle)">
321
+ <span className="w-1.5 h-1.5 rounded-full bg-(--accent) animate-bounce [animation-delay:-0.3s]" />
322
+ <span className="w-1.5 h-1.5 rounded-full bg-(--accent) animate-bounce [animation-delay:-0.15s]" />
323
+ <span className="w-1.5 h-1.5 rounded-full bg-(--accent) animate-bounce" />
324
+ </div>
325
+ </div>
326
+ )}
327
+ </div>
328
+
329
+ {/* Input Area */}
330
+ <div className="p-5 bg-(--bg-2) bg-opacity-80 backdrop-blur-md border-t border-(--border-subtle)">
331
+ <form
332
+ onSubmit={(e: FormEvent<HTMLFormElement>) => {
333
+ e.preventDefault();
334
+ handleSubmit(e);
335
+ }}
336
+ className="relative group"
337
+ >
338
+ <textarea
339
+ value={input}
340
+ onChange={(e: ChangeEvent<HTMLTextAreaElement>) => handleInputChange(e)}
341
+ onKeyDown={(e) => {
342
+ if (e.key === "Enter" && !e.shiftKey) {
343
+ e.preventDefault();
344
+ if (input.trim()) (e.currentTarget.form)?.requestSubmit();
345
+ }
346
+ }}
347
+ placeholder={isPlanMode ? "Define an architecture or complex goal..." : "How can I help you today?"}
348
+ rows={1}
349
+ className="w-full bg-(--surface) text-(--text) text-sm rounded-2xl pl-11 pr-14 py-4 border border-(--border) focus:border-(--accent) focus:ring-4 focus:ring-(--accent) focus:ring-opacity-5 outline-none resize-none min-h-[56px] max-h-[240px] transition-all scrollbar-hide shadow-lg group-hover:border-(--border-subtle)"
350
+ style={{ height: "auto" }}
351
+ onInput={(e) => {
352
+ const target = e.target as HTMLTextAreaElement;
353
+ target.style.height = "auto";
354
+ target.style.height = `${Math.min(target.scrollHeight, 240)}px`;
355
+ }}
356
+ />
357
+ <div className="absolute left-4 top-[17px] text-(--text-muted) transition-colors group-focus-within:text-(--accent)">
358
+ {isPlanMode ? <Wand2 size={20} className="text-(--warning)" /> : <MessageSquare size={20} />}
359
+ </div>
360
+ <button
361
+ type="submit"
362
+ disabled={isLoading || !input.trim()}
363
+ className="absolute right-2.5 top-2.5 w-[36px] h-[36px] rounded-xl bg-(--accent) text-white hover:scale-105 active:scale-95 disabled:opacity-20 disabled:grayscale disabled:cursor-not-allowed transition-all shadow-md flex items-center justify-center overflow-hidden"
364
+ >
365
+ {isLoading ? (
366
+ <div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
367
+ ) : (
368
+ <Send size={18} className="translate-x-px -translate-y-px" />
369
+ )}
370
+ </button>
371
+ </form>
372
+ <div className="flex items-center justify-between px-1 mt-3.5">
373
+ <p className="text-[9px] text-(--text-muted) flex items-center gap-1.5 font-bold uppercase tracking-widest opacity-60">
374
+ <Check size={10} className="text-(--success)" /> Context Captured
375
+ </p>
376
+ <p className="text-[9px] text-(--text-muted) flex items-center gap-1.5 font-bold uppercase tracking-widest opacity-60">
377
+ Shift+Enter for multi-line
378
+ </p>
379
+ </div>
380
+ </div>
381
+ </motion.div>
382
+ )}
383
+ </AnimatePresence>
384
+ );
385
+ }
components/workspace/IDEClient.tsx CHANGED
@@ -5,6 +5,8 @@ import dynamic from "next/dynamic";
5
  import { useSearchParams } from "next/navigation";
6
  import { Toaster, toast } from "sonner";
7
  import { VSCodeFrame } from "@/components/workspace/VSCodeFrame";
 
 
8
  import Link from "next/link";
9
  import type { Session } from "next-auth";
10
 
@@ -14,15 +16,28 @@ const Dashboard = dynamic(() => import("@/components/dashboard/Dashboard"), { ss
14
  export default function IDEClient({ session }: { session: Session | null }) {
15
  const searchParams = useSearchParams();
16
  const workspaceParam = searchParams?.get("workspace");
 
17
  const [theme, setTheme] = useState<"dark" | "light">("dark");
18
  const [refreshKey, setRefreshKey] = useState(0);
19
 
20
  // Apply theme globally
21
  useEffect(() => {
22
  document.documentElement.setAttribute("data-theme", theme);
23
- return () => setTheme("dark");
24
  }, [theme]);
25
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  // If no specific workspace is requested, render the Firebase-style Dashboard Control Plane
27
  if (!workspaceParam) {
28
  return (
@@ -60,18 +75,19 @@ export default function IDEClient({ session }: { session: Session | null }) {
60
 
61
  // Otherwise, render the dedicated VS Code Server instance mapped to this workspace
62
  return (
63
- <div data-theme={theme} className="h-dvh w-screen flex flex-col bg-(--bg) overflow-hidden">
64
- <div className="h-10 flex items-center justify-between px-4 bg-(--activity-bar) border-b border-(--border-subtle) shrink-0">
65
  <div className="flex items-center gap-3">
66
- <Link href="/" className="text-(--accent) font-bold text-sm tracking-wide hover:opacity-80 transition-opacity">
67
- CodeVerse
 
68
  </Link>
69
  <div className="h-4 w-px bg-(--border) mx-1" />
70
  <span className="text-xs text-(--text-muted) font-mono">Workspace: {workspaceParam}</span>
71
  </div>
72
  <div className="flex items-center gap-2">
73
  {session?.user && (
74
- <div className="flex items-center gap-2 mr-2 px-2 py-0.5 bg-(--border) rounded-full border border-(--border-subtle)">
75
  <div className="w-4 h-4 rounded-full bg-(--accent) flex items-center justify-center text-[10px] text-white font-bold">
76
  {session.user.name?.[0] || session.user.email?.[0] || "?"}
77
  </div>
@@ -80,18 +96,41 @@ export default function IDEClient({ session }: { session: Session | null }) {
80
  </span>
81
  </div>
82
  )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  <button
84
  onClick={handleRebuild}
85
  className="px-3 py-1 text-xs font-semibold bg-(--border) hover:bg-(--border-subtle) text-(--text) rounded-md transition-colors border border-(--border-subtle) shadow-sm"
86
  title="Apply codeverse.json changes and restart container"
87
  >
88
- ↻ Rebuild Environment
89
  </button>
90
  </div>
91
  </div>
92
 
93
- <div className="flex-1 relative w-full h-full">
94
- <VSCodeFrame key={refreshKey} workspaceId={workspaceParam} />
 
 
 
 
 
 
 
 
95
  </div>
96
 
97
  <Toaster position="bottom-right" theme={theme} richColors />
 
5
  import { useSearchParams } from "next/navigation";
6
  import { Toaster, toast } from "sonner";
7
  import { VSCodeFrame } from "@/components/workspace/VSCodeFrame";
8
+ import { AIAssistantSidebar } from "@/components/workspace/AIAssistantSidebar";
9
+ import { Sparkles } from "lucide-react";
10
  import Link from "next/link";
11
  import type { Session } from "next-auth";
12
 
 
16
  export default function IDEClient({ session }: { session: Session | null }) {
17
  const searchParams = useSearchParams();
18
  const workspaceParam = searchParams?.get("workspace");
19
+ const [isAiOpen, setIsAiOpen] = useState(false);
20
  const [theme, setTheme] = useState<"dark" | "light">("dark");
21
  const [refreshKey, setRefreshKey] = useState(0);
22
 
23
  // Apply theme globally
24
  useEffect(() => {
25
  document.documentElement.setAttribute("data-theme", theme);
26
+ // Cleanup if needed
27
  }, [theme]);
28
 
29
+ // Keyboard shortcut for AI
30
+ useEffect(() => {
31
+ const handleKeyDown = (e: KeyboardEvent) => {
32
+ if ((e.ctrlKey || e.metaKey) && e.key === "i") {
33
+ e.preventDefault();
34
+ setIsAiOpen(prev => !prev);
35
+ }
36
+ };
37
+ window.addEventListener("keydown", handleKeyDown);
38
+ return () => window.removeEventListener("keydown", handleKeyDown);
39
+ }, []);
40
+
41
  // If no specific workspace is requested, render the Firebase-style Dashboard Control Plane
42
  if (!workspaceParam) {
43
  return (
 
75
 
76
  // Otherwise, render the dedicated VS Code Server instance mapped to this workspace
77
  return (
78
+ <div data-theme={theme} className="h-dvh w-screen flex flex-col bg-(--bg) overflow-hidden relative">
79
+ <div className="h-10 flex items-center justify-between px-4 bg-(--activity-bar) border-b border-(--border-subtle) shrink-0 z-40">
80
  <div className="flex items-center gap-3">
81
+ <Link href="/" className="text-(--accent) font-bold text-sm tracking-wide hover:opacity-80 transition-opacity flex items-center gap-1.5">
82
+ <div className="w-5 h-5 rounded bg-(--accent) flex items-center justify-center text-white text-[10px]">⬡</div>
83
+ CodeVerse
84
  </Link>
85
  <div className="h-4 w-px bg-(--border) mx-1" />
86
  <span className="text-xs text-(--text-muted) font-mono">Workspace: {workspaceParam}</span>
87
  </div>
88
  <div className="flex items-center gap-2">
89
  {session?.user && (
90
+ <div className="hidden md:flex items-center gap-2 mr-2 px-2 py-0.5 bg-(--border) rounded-full border border-(--border-subtle)">
91
  <div className="w-4 h-4 rounded-full bg-(--accent) flex items-center justify-center text-[10px] text-white font-bold">
92
  {session.user.name?.[0] || session.user.email?.[0] || "?"}
93
  </div>
 
96
  </span>
97
  </div>
98
  )}
99
+
100
+ <button
101
+ onClick={() => setIsAiOpen(!isAiOpen)}
102
+ className={`flex items-center gap-1.5 px-3 py-1 text-xs font-bold rounded-md transition-all border ${
103
+ isAiOpen
104
+ ? "bg-(--accent) text-white border-(--accent) shadow-lg shadow-(--accent)/20"
105
+ : "bg-(--border) hover:bg-(--border-subtle) text-(--text) border-(--border-subtle)"
106
+ }`}
107
+ >
108
+ <Sparkles className={`w-3.5 h-3.5 ${isAiOpen ? "animate-pulse" : ""}`} />
109
+ {isAiOpen ? "AI Active" : "AI Studio"}
110
+ </button>
111
+
112
+ <div className="w-px h-4 bg-(--border) mx-1" />
113
+
114
  <button
115
  onClick={handleRebuild}
116
  className="px-3 py-1 text-xs font-semibold bg-(--border) hover:bg-(--border-subtle) text-(--text) rounded-md transition-colors border border-(--border-subtle) shadow-sm"
117
  title="Apply codeverse.json changes and restart container"
118
  >
119
+ ↻ Rebuild
120
  </button>
121
  </div>
122
  </div>
123
 
124
+ <div className="flex-1 flex relative w-full h-full overflow-hidden">
125
+ <main className={`flex-1 relative transition-all duration-300 ${isAiOpen ? "mr-0 md:mr-80 lg:mr-96" : "mr-0"}`}>
126
+ <VSCodeFrame key={refreshKey} workspaceId={workspaceParam} />
127
+ </main>
128
+
129
+ <AIAssistantSidebar
130
+ workspaceName={workspaceParam}
131
+ isOpen={isAiOpen}
132
+ onClose={() => setIsAiOpen(false)}
133
+ />
134
  </div>
135
 
136
  <Toaster position="bottom-right" theme={theme} richColors />
lib/db/schema.ts CHANGED
@@ -78,6 +78,18 @@ CREATE TABLE IF NOT EXISTS user_settings (
78
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
79
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
80
  );
 
 
 
 
 
 
 
 
 
 
 
 
81
  `;
82
 
83
  export async function initDb() {
 
78
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
79
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
80
  );
81
+
82
+ CREATE TABLE IF NOT EXISTS chat_history (
83
+ id TEXT PRIMARY KEY,
84
+ user_id TEXT NOT NULL,
85
+ workspace_id TEXT NOT NULL,
86
+ role TEXT NOT NULL,
87
+ content TEXT NOT NULL,
88
+ tool_invocations TEXT,
89
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
90
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
91
+ FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE
92
+ );
93
  `;
94
 
95
  export async function initDb() {
lib/docker/manager.ts CHANGED
@@ -7,7 +7,9 @@ const docker = new Docker({ socketPath: process.platform === 'win32' ? '//./pipe
7
 
8
  export interface WorkspaceConfig {
9
  id: string;
10
- image?: string; // e.g., 'codercom/code-server:latest'
 
 
11
  withAndroidEmulator?: boolean;
12
  onLog?: (msg: string) => void;
13
  }
@@ -17,7 +19,7 @@ export interface WorkspaceConfig {
17
  * Optionally spins up a sidecar Android emulator container.
18
  */
19
  export async function startWorkspaceContainer(config: WorkspaceConfig) {
20
- const { id, withAndroidEmulator = false, onLog = console.log } = config;
21
  const containerName = `codeverse-workspace-${id}`;
22
  const androidContainerName = `codeverse-android-${id}`;
23
 
@@ -44,7 +46,8 @@ export async function startWorkspaceContainer(config: WorkspaceConfig) {
44
  }
45
 
46
  // Map the local host path to the workspace
47
- const dataPath = process.env.DATA_PATH || path.resolve(process.cwd(), 'data/projects', id);
 
48
 
49
  // --- WORKSPACE CONFIG LOGIC AND IMAGE BUILDING ---
50
  const { buildWorkspaceImage } = await import('./builder');
@@ -161,7 +164,8 @@ export async function startWorkspaceContainer(config: WorkspaceConfig) {
161
  if (!appetizeUrl) {
162
  try {
163
  const fs = await import('fs/promises');
164
- const dataPath = process.env.DATA_PATH || path.resolve(process.cwd(), 'data/projects', id);
 
165
  const configPath = path.join(dataPath, 'codeverse.json');
166
  const configContent = await fs.readFile(configPath, 'utf8');
167
  const customConfig = JSON.parse(configContent);
 
7
 
8
  export interface WorkspaceConfig {
9
  id: string;
10
+ userId: string;
11
+ projectName: string;
12
+ image?: string;
13
  withAndroidEmulator?: boolean;
14
  onLog?: (msg: string) => void;
15
  }
 
19
  * Optionally spins up a sidecar Android emulator container.
20
  */
21
  export async function startWorkspaceContainer(config: WorkspaceConfig) {
22
+ const { id, userId, projectName, withAndroidEmulator = false, onLog = console.log } = config;
23
  const containerName = `codeverse-workspace-${id}`;
24
  const androidContainerName = `codeverse-android-${id}`;
25
 
 
46
  }
47
 
48
  // Map the local host path to the workspace
49
+ const safeName = projectName.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60);
50
+ const dataPath = process.env.DATA_PATH || path.resolve(process.cwd(), 'workspaces', userId, safeName);
51
 
52
  // --- WORKSPACE CONFIG LOGIC AND IMAGE BUILDING ---
53
  const { buildWorkspaceImage } = await import('./builder');
 
164
  if (!appetizeUrl) {
165
  try {
166
  const fs = await import('fs/promises');
167
+ const safeName = projectName.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60);
168
+ const dataPath = process.env.DATA_PATH || path.resolve(process.cwd(), 'workspaces', userId, safeName);
169
  const configPath = path.join(dataPath, 'codeverse.json');
170
  const configContent = await fs.readFile(configPath, 'utf8');
171
  const customConfig = JSON.parse(configContent);
lib/fs/isolation.ts ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import path from "path";
2
+ import fs from "fs/promises";
3
+
4
+ const WORKSPACE_BASE = path.join(process.cwd(), "workspaces");
5
+
6
+ /**
7
+ * Returns the root directory for a specific user's workspaces.
8
+ * e.g., /path/to/codeverse/workspaces/{userId}
9
+ */
10
+ export async function getUserWorkspaceRoot(userId: string): Promise<string> {
11
+ const userRoot = path.join(WORKSPACE_BASE, userId);
12
+ await fs.mkdir(userRoot, { recursive: true });
13
+ return userRoot;
14
+ }
15
+
16
+ /**
17
+ * Resolves a safe, isolated path within a specific project in a user's workspace.
18
+ * Prevents project-level directory traversal.
19
+ */
20
+ export async function resolveSafeProjectPath(userId: string, projectName: string, subPath: string = ""): Promise<string> {
21
+ const userRoot = await getUserWorkspaceRoot(userId);
22
+ const projectRoot = path.resolve(userRoot, projectName.replace(/[^a-zA-Z0-9-_]/g, "-").slice(0, 60));
23
+
24
+ // Normalize and resolve the absolute path
25
+ const targetPath = path.resolve(projectRoot, subPath);
26
+
27
+ // Security Check: Ensure the resolved path is still within the project root
28
+ if (!targetPath.startsWith(projectRoot)) {
29
+ throw new Error("Security Violation: Path traversal detected.");
30
+ }
31
+
32
+ return targetPath;
33
+ }
34
+
35
+ /**
36
+ * Resolves a safe, isolated path directly within a user's root workspace (e.g. for listing project names).
37
+ */
38
+ export async function resolveSafePath(userId: string, subPath: string): Promise<string> {
39
+ const userRoot = await getUserWorkspaceRoot(userId);
40
+
41
+ // Normalize and resolve the absolute path
42
+ const targetPath = path.resolve(userRoot, subPath);
43
+
44
+ // Security Check: Ensure the resolved path is still within the user's root
45
+ if (!targetPath.startsWith(userRoot)) {
46
+ throw new Error("Security Violation: Path traversal detected.");
47
+ }
48
+
49
+ return targetPath;
50
+ }
lib/git/index.ts CHANGED
@@ -1,21 +1,21 @@
1
  import simpleGit, { SimpleGit, SimpleGitOptions } from 'simple-git';
2
 
3
- const WORKSPACE_ROOT = process.cwd();
4
-
5
  const options: Partial<SimpleGitOptions> = {
6
- baseDir: WORKSPACE_ROOT,
7
  binary: 'git',
8
  maxConcurrentProcesses: 6,
9
  trimmed: false,
10
  };
11
 
12
- export const git: SimpleGit = simpleGit(options);
 
 
13
 
14
- export async function getGitStatus() {
15
- const isRepo = await git.checkIsRepo();
 
16
  if (!isRepo) return null;
17
 
18
- const status = await git.status();
19
  return {
20
  currentBranch: status.current,
21
  modified: status.modified,
@@ -28,38 +28,45 @@ export async function getGitStatus() {
28
  };
29
  }
30
 
31
- export async function getBranchList() {
32
- const branches = await git.branch();
 
33
  return {
34
  all: branches.all,
35
  current: branches.current
36
  };
37
  }
38
 
39
- export async function commitFiles(message: string, files: string[] = []) {
 
40
  if (files.length > 0) {
41
- await git.add(files);
42
  } else {
43
- await git.add('.');
44
  }
45
- return git.commit(message);
46
  }
47
 
48
- export async function getFileDiff(file: string) {
49
- return git.diff([file]);
50
  }
51
 
52
- export async function pushBranch() {
53
- return git.push();
54
  }
55
 
56
- export async function pullBranch() {
57
- return git.pull();
58
  }
59
 
60
- export async function checkoutBranch(branch: string, create: boolean = false) {
 
61
  if (create) {
62
- return git.checkoutLocalBranch(branch);
63
  }
64
- return git.checkout(branch);
65
  }
 
 
 
 
 
1
  import simpleGit, { SimpleGit, SimpleGitOptions } from 'simple-git';
2
 
 
 
3
  const options: Partial<SimpleGitOptions> = {
 
4
  binary: 'git',
5
  maxConcurrentProcesses: 6,
6
  trimmed: false,
7
  };
8
 
9
+ export function getGit(baseDir: string = process.cwd()): SimpleGit {
10
+ return simpleGit({ ...options, baseDir });
11
+ }
12
 
13
+ export async function getGitStatus(baseDir?: string) {
14
+ const instance = getGit(baseDir);
15
+ const isRepo = await instance.checkIsRepo();
16
  if (!isRepo) return null;
17
 
18
+ const status = await instance.status();
19
  return {
20
  currentBranch: status.current,
21
  modified: status.modified,
 
28
  };
29
  }
30
 
31
+ export async function getBranchList(baseDir?: string) {
32
+ const instance = getGit(baseDir);
33
+ const branches = await instance.branch();
34
  return {
35
  all: branches.all,
36
  current: branches.current
37
  };
38
  }
39
 
40
+ export async function commitFiles(message: string, files: string[] = [], baseDir?: string) {
41
+ const instance = getGit(baseDir);
42
  if (files.length > 0) {
43
+ await instance.add(files);
44
  } else {
45
+ await instance.add('.');
46
  }
47
+ return instance.commit(message);
48
  }
49
 
50
+ export async function getFileDiff(file: string, baseDir?: string) {
51
+ return getGit(baseDir).diff([file]);
52
  }
53
 
54
+ export async function pushBranch(baseDir?: string) {
55
+ return getGit(baseDir).push();
56
  }
57
 
58
+ export async function pullBranch(baseDir?: string) {
59
+ return getGit(baseDir).pull();
60
  }
61
 
62
+ export async function checkoutBranch(branch: string, create: boolean = false, baseDir?: string) {
63
+ const instance = getGit(baseDir);
64
  if (create) {
65
+ return instance.checkoutLocalBranch(branch);
66
  }
67
+ return instance.checkout(branch);
68
  }
69
+
70
+
71
+ // Export for legacy access if needed, but discouraged
72
+ // export { git } from 'simple-git';
lib/mcp/tools.ts CHANGED
@@ -1,89 +1,118 @@
1
-
2
- import { tool } from "ai";
3
  import { z } from "zod";
4
  import fs from "fs/promises";
5
  import path from "path";
6
  import { exec } from "child_process";
7
  import { promisify } from "util";
 
8
 
9
  const execAsync = promisify(exec);
10
- const cwd = process.cwd();
11
 
12
- export const coreTools = {
13
- read_file: tool({
14
- description: "Read the full content of a file",
15
- parameters: z.object({ filePath: z.string().describe("Absolute or relative path to the file") }),
16
- // @ts-expect-error - AI SDK generic limitation
17
- execute: async (args: { filePath: string }): Promise<Record<string, unknown>> => {
18
- const { filePath } = args;
19
- try {
20
- const fullPath = path.resolve(cwd, filePath);
21
- const content = await fs.readFile(fullPath, "utf-8");
22
- return { success: true, content };
23
- } catch (e: unknown) {
24
- return { success: false, error: (e as Error).message };
25
- }
 
 
 
 
 
 
 
 
 
 
26
  },
27
- }),
28
 
29
- write_file: tool({
30
- description: "Write content to a file (overwrites existing)",
31
- parameters: z.object({
32
- filePath: z.string(),
33
- content: z.string().describe("The full replacement content"),
34
- }),
35
- // @ts-expect-error - AI SDK generic limitation
36
- execute: async (args: { filePath: string, content: string }): Promise<Record<string, unknown>> => {
37
- const { filePath, content } = args;
38
- try {
39
- const fullPath = path.resolve(cwd, filePath);
40
- await fs.mkdir(path.dirname(fullPath), { recursive: true });
41
- await fs.writeFile(fullPath, content, "utf-8");
42
- return { success: true, path: fullPath };
43
- } catch (e: unknown) {
44
- return { success: false, error: (e as Error).message };
45
- }
46
  },
47
- }),
48
 
49
- terminal_command: tool({
50
- description: "Execute a bash/shell command strictly in the workspace",
51
- parameters: z.object({
52
- command: z.string().describe("The command to run e.g. 'npm run build' or 'tsc --noEmit'"),
53
- background: z.boolean().optional().describe("Run in background without waiting for finish"),
54
- }),
55
- // @ts-expect-error - AI SDK generic limitation
56
- execute: async (args: { command: string, background?: boolean }): Promise<Record<string, unknown>> => {
57
- const { command, background } = args;
58
- try {
59
- if (background) {
60
- exec(command, { cwd });
61
- return { success: true, output: "Command started in background" };
 
 
 
 
62
  }
63
- const { stdout, stderr } = await execAsync(command, { cwd });
64
- return { success: true, stdout, stderr };
65
- } catch (e: unknown) {
66
- return { success: false, error: (e as Error).message, stderr: (e as { stderr?: string }).stderr };
67
- }
68
  },
69
- }),
70
 
71
- search_code: tool({
72
- description: "Search for a regex pattern across all workspace files",
73
- parameters: z.object({
74
- pattern: z.string().describe("Regex or strict string to search"),
75
- glob: z.string().optional().describe("File restriction e.g. '*.ts'"),
76
- }),
77
- // @ts-expect-error - AI SDK generic limitation
78
- execute: async (args: { pattern: string }): Promise<Record<string, unknown>> => {
79
- const { pattern } = args;
80
- try {
81
- // Simple grep fallback
82
- const { stdout } = await execAsync(`git grep -n "${pattern}" || grep -rn "${pattern}" .`, { cwd });
83
- return { success: true, matches: stdout.split("\n").filter(Boolean) };
84
- } catch (e: unknown) {
85
- return { success: false, error: (e as Error).message };
86
- }
 
87
  },
88
- }),
89
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { zodSchema } from "ai";
2
+ import type { ToolSet } from "ai";
3
  import { z } from "zod";
4
  import fs from "fs/promises";
5
  import path from "path";
6
  import { exec } from "child_process";
7
  import { promisify } from "util";
8
+ import { resolveSafeProjectPath } from "../fs/isolation";
9
 
10
  const execAsync = promisify(exec);
 
11
 
12
+ /**
13
+ * Factory that creates user-isolated workspace tools for the AI agent.
14
+ * Each tool is sandboxed to workspaces/{userId}/{workspaceName}/
15
+ *
16
+ * Note: Tools are built as plain objects using `zodSchema()` for `inputSchema`
17
+ * instead of the `tool()` helper, because `tool()` in AI SDK v6 has broken
18
+ * TypeScript overloads when `execute` returns union types in strict mode.
19
+ */
20
+ export function createTools(userId: string, workspaceName: string): ToolSet {
21
+ return {
22
+ read_file: {
23
+ description: "Read the full content of a file within the workspace",
24
+ inputSchema: zodSchema(z.object({
25
+ filePath: z.string().describe("Relative path from workspace root"),
26
+ })),
27
+ execute: async ({ filePath }: { filePath: string }) => {
28
+ try {
29
+ const fullPath = await resolveSafeProjectPath(userId, workspaceName, filePath);
30
+ const content = await fs.readFile(fullPath, "utf-8");
31
+ return { success: true, content };
32
+ } catch (e) {
33
+ return { success: false, error: (e as Error).message };
34
+ }
35
+ },
36
  },
 
37
 
38
+ write_file: {
39
+ description: "Write/overwrite a file within the workspace",
40
+ inputSchema: zodSchema(z.object({
41
+ filePath: z.string().describe("Relative path to the file"),
42
+ content: z.string().describe("Full file content to write"),
43
+ })),
44
+ execute: async ({ filePath, content }: { filePath: string; content: string }) => {
45
+ try {
46
+ const fullPath = await resolveSafeProjectPath(userId, workspaceName, filePath);
47
+ await fs.mkdir(path.dirname(fullPath), { recursive: true });
48
+ await fs.writeFile(fullPath, content, "utf-8");
49
+ return { success: true, path: filePath };
50
+ } catch (e) {
51
+ return { success: false, error: (e as Error).message };
52
+ }
53
+ },
 
54
  },
 
55
 
56
+ terminal_command: {
57
+ description: "Run a shell command inside the workspace directory",
58
+ inputSchema: zodSchema(z.object({
59
+ command: z.string().describe("Shell command to execute"),
60
+ background: z.boolean().optional().describe("Run async without capturing output"),
61
+ })),
62
+ execute: async ({ command, background }: { command: string; background?: boolean }) => {
63
+ try {
64
+ const cwd = await resolveSafeProjectPath(userId, workspaceName);
65
+ if (background) {
66
+ exec(command, { cwd });
67
+ return { success: true, output: "Started in background" };
68
+ }
69
+ const { stdout, stderr } = await execAsync(command, { cwd });
70
+ return { success: true, stdout, stderr };
71
+ } catch (e) {
72
+ return { success: false, error: (e as Error).message };
73
  }
74
+ },
 
 
 
 
75
  },
 
76
 
77
+ search_code: {
78
+ description: "Search for a text pattern across all workspace files",
79
+ inputSchema: zodSchema(z.object({
80
+ pattern: z.string().describe("Literal string or regex pattern"),
81
+ })),
82
+ execute: async ({ pattern }: { pattern: string }) => {
83
+ try {
84
+ const cwd = await resolveSafeProjectPath(userId, workspaceName);
85
+ const { stdout } = await execAsync(
86
+ `git grep -n "${pattern}" 2>/dev/null || grep -rn "${pattern}" . 2>/dev/null || true`,
87
+ { cwd }
88
+ );
89
+ return { success: true, matches: stdout.split("\n").filter(Boolean) };
90
+ } catch (e) {
91
+ return { success: false, error: (e as Error).message };
92
+ }
93
+ },
94
  },
95
+
96
+ list_files: {
97
+ description: "List files in the project to understand its structure",
98
+ inputSchema: zodSchema(z.object({
99
+ recursive: z.boolean().optional().describe("Recursive listing (default: true)"),
100
+ })),
101
+ execute: async ({ recursive }: { recursive?: boolean }) => {
102
+ const isRecursive = recursive !== false;
103
+ try {
104
+ const cwd = await resolveSafeProjectPath(userId, workspaceName);
105
+ const cmd = isRecursive
106
+ ? "find . -maxdepth 4 -not -path '*/\\.*' -not -path '*/node_modules/*' -not -path '*/.next/*'"
107
+ : "ls -F";
108
+ const { stdout } = await execAsync(cmd, { cwd });
109
+ return { success: true, files: stdout.split("\n").filter(Boolean) };
110
+ } catch (e) {
111
+ return { success: false, error: (e as Error).message };
112
+ }
113
+ },
114
+ },
115
+ } as ToolSet;
116
+ }
117
+
118
+ export type WorkspaceTools = ReturnType<typeof createTools>;
package-lock.json CHANGED
@@ -45,6 +45,7 @@
45
  "react": "19.2.3",
46
  "react-dom": "19.2.3",
47
  "react-markdown": "^10.1.0",
 
48
  "recharts": "^3.7.0",
49
  "remark-gfm": "^4.0.1",
50
  "simple-git": "^3.32.3",
@@ -67,6 +68,7 @@
67
  "@types/node": "^20",
68
  "@types/react": "^19",
69
  "@types/react-dom": "^19",
 
70
  "@types/tar-fs": "^2.0.4",
71
  "eslint": "^9",
72
  "eslint-config-next": "16.1.6",
@@ -437,6 +439,15 @@
437
  "node": ">=6.0.0"
438
  }
439
  },
 
 
 
 
 
 
 
 
 
440
  "node_modules/@babel/template": {
441
  "version": "7.28.6",
442
  "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
@@ -3726,6 +3737,12 @@
3726
  "undici-types": "~6.21.0"
3727
  }
3728
  },
 
 
 
 
 
 
3729
  "node_modules/@types/react": {
3730
  "version": "19.2.14",
3731
  "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
@@ -3747,6 +3764,16 @@
3747
  "@types/react": "^19.2.0"
3748
  }
3749
  },
 
 
 
 
 
 
 
 
 
 
3750
  "node_modules/@types/ssh2": {
3751
  "version": "1.15.5",
3752
  "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.5.tgz",
@@ -6933,6 +6960,19 @@
6933
  "reusify": "^1.0.4"
6934
  }
6935
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
6936
  "node_modules/fetch-blob": {
6937
  "version": "3.2.0",
6938
  "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
@@ -7057,6 +7097,14 @@
7057
  "url": "https://github.com/sponsors/ljharb"
7058
  }
7059
  },
 
 
 
 
 
 
 
 
7060
  "node_modules/formdata-polyfill": {
7061
  "version": "4.0.10",
7062
  "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
@@ -7434,6 +7482,19 @@
7434
  "node": ">= 0.4"
7435
  }
7436
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
7437
  "node_modules/hast-util-to-jsx-runtime": {
7438
  "version": "2.3.6",
7439
  "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
@@ -7474,6 +7535,23 @@
7474
  "url": "https://opencollective.com/unified"
7475
  }
7476
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7477
  "node_modules/hermes-estree": {
7478
  "version": "0.25.1",
7479
  "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
@@ -7491,6 +7569,21 @@
7491
  "hermes-estree": "0.25.1"
7492
  }
7493
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7494
  "node_modules/hono": {
7495
  "version": "4.12.3",
7496
  "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.3.tgz",
@@ -8749,6 +8842,20 @@
8749
  "loose-envify": "cli.js"
8750
  }
8751
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8752
  "node_modules/lru-cache": {
8753
  "version": "5.1.1",
8754
  "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -10504,6 +10611,15 @@
10504
  "node": ">= 0.8.0"
10505
  }
10506
  },
 
 
 
 
 
 
 
 
 
10507
  "node_modules/promise-limit": {
10508
  "version": "2.7.0",
10509
  "resolved": "https://registry.npmjs.org/promise-limit/-/promise-limit-2.7.0.tgz",
@@ -10799,6 +10915,26 @@
10799
  }
10800
  }
10801
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10802
  "node_modules/readable-stream": {
10803
  "version": "3.6.2",
10804
  "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
@@ -10882,6 +11018,22 @@
10882
  "url": "https://github.com/sponsors/ljharb"
10883
  }
10884
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10885
  "node_modules/regexp.prototype.flags": {
10886
  "version": "1.5.4",
10887
  "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
 
45
  "react": "19.2.3",
46
  "react-dom": "19.2.3",
47
  "react-markdown": "^10.1.0",
48
+ "react-syntax-highlighter": "^16.1.1",
49
  "recharts": "^3.7.0",
50
  "remark-gfm": "^4.0.1",
51
  "simple-git": "^3.32.3",
 
68
  "@types/node": "^20",
69
  "@types/react": "^19",
70
  "@types/react-dom": "^19",
71
+ "@types/react-syntax-highlighter": "^15.5.13",
72
  "@types/tar-fs": "^2.0.4",
73
  "eslint": "^9",
74
  "eslint-config-next": "16.1.6",
 
439
  "node": ">=6.0.0"
440
  }
441
  },
442
+ "node_modules/@babel/runtime": {
443
+ "version": "7.29.2",
444
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
445
+ "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
446
+ "license": "MIT",
447
+ "engines": {
448
+ "node": ">=6.9.0"
449
+ }
450
+ },
451
  "node_modules/@babel/template": {
452
  "version": "7.28.6",
453
  "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
 
3737
  "undici-types": "~6.21.0"
3738
  }
3739
  },
3740
+ "node_modules/@types/prismjs": {
3741
+ "version": "1.26.6",
3742
+ "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz",
3743
+ "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==",
3744
+ "license": "MIT"
3745
+ },
3746
  "node_modules/@types/react": {
3747
  "version": "19.2.14",
3748
  "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
 
3764
  "@types/react": "^19.2.0"
3765
  }
3766
  },
3767
+ "node_modules/@types/react-syntax-highlighter": {
3768
+ "version": "15.5.13",
3769
+ "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz",
3770
+ "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==",
3771
+ "dev": true,
3772
+ "license": "MIT",
3773
+ "dependencies": {
3774
+ "@types/react": "*"
3775
+ }
3776
+ },
3777
  "node_modules/@types/ssh2": {
3778
  "version": "1.15.5",
3779
  "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.5.tgz",
 
6960
  "reusify": "^1.0.4"
6961
  }
6962
  },
6963
+ "node_modules/fault": {
6964
+ "version": "1.0.4",
6965
+ "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz",
6966
+ "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==",
6967
+ "license": "MIT",
6968
+ "dependencies": {
6969
+ "format": "^0.2.0"
6970
+ },
6971
+ "funding": {
6972
+ "type": "github",
6973
+ "url": "https://github.com/sponsors/wooorm"
6974
+ }
6975
+ },
6976
  "node_modules/fetch-blob": {
6977
  "version": "3.2.0",
6978
  "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
 
7097
  "url": "https://github.com/sponsors/ljharb"
7098
  }
7099
  },
7100
+ "node_modules/format": {
7101
+ "version": "0.2.2",
7102
+ "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
7103
+ "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==",
7104
+ "engines": {
7105
+ "node": ">=0.4.x"
7106
+ }
7107
+ },
7108
  "node_modules/formdata-polyfill": {
7109
  "version": "4.0.10",
7110
  "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
 
7482
  "node": ">= 0.4"
7483
  }
7484
  },
7485
+ "node_modules/hast-util-parse-selector": {
7486
+ "version": "4.0.0",
7487
+ "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz",
7488
+ "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==",
7489
+ "license": "MIT",
7490
+ "dependencies": {
7491
+ "@types/hast": "^3.0.0"
7492
+ },
7493
+ "funding": {
7494
+ "type": "opencollective",
7495
+ "url": "https://opencollective.com/unified"
7496
+ }
7497
+ },
7498
  "node_modules/hast-util-to-jsx-runtime": {
7499
  "version": "2.3.6",
7500
  "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
 
7535
  "url": "https://opencollective.com/unified"
7536
  }
7537
  },
7538
+ "node_modules/hastscript": {
7539
+ "version": "9.0.1",
7540
+ "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz",
7541
+ "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==",
7542
+ "license": "MIT",
7543
+ "dependencies": {
7544
+ "@types/hast": "^3.0.0",
7545
+ "comma-separated-tokens": "^2.0.0",
7546
+ "hast-util-parse-selector": "^4.0.0",
7547
+ "property-information": "^7.0.0",
7548
+ "space-separated-tokens": "^2.0.0"
7549
+ },
7550
+ "funding": {
7551
+ "type": "opencollective",
7552
+ "url": "https://opencollective.com/unified"
7553
+ }
7554
+ },
7555
  "node_modules/hermes-estree": {
7556
  "version": "0.25.1",
7557
  "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
 
7569
  "hermes-estree": "0.25.1"
7570
  }
7571
  },
7572
+ "node_modules/highlight.js": {
7573
+ "version": "10.7.3",
7574
+ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
7575
+ "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==",
7576
+ "license": "BSD-3-Clause",
7577
+ "engines": {
7578
+ "node": "*"
7579
+ }
7580
+ },
7581
+ "node_modules/highlightjs-vue": {
7582
+ "version": "1.0.0",
7583
+ "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz",
7584
+ "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==",
7585
+ "license": "CC0-1.0"
7586
+ },
7587
  "node_modules/hono": {
7588
  "version": "4.12.3",
7589
  "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.3.tgz",
 
8842
  "loose-envify": "cli.js"
8843
  }
8844
  },
8845
+ "node_modules/lowlight": {
8846
+ "version": "1.20.0",
8847
+ "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz",
8848
+ "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==",
8849
+ "license": "MIT",
8850
+ "dependencies": {
8851
+ "fault": "^1.0.0",
8852
+ "highlight.js": "~10.7.0"
8853
+ },
8854
+ "funding": {
8855
+ "type": "github",
8856
+ "url": "https://github.com/sponsors/wooorm"
8857
+ }
8858
+ },
8859
  "node_modules/lru-cache": {
8860
  "version": "5.1.1",
8861
  "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
 
10611
  "node": ">= 0.8.0"
10612
  }
10613
  },
10614
+ "node_modules/prismjs": {
10615
+ "version": "1.30.0",
10616
+ "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
10617
+ "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
10618
+ "license": "MIT",
10619
+ "engines": {
10620
+ "node": ">=6"
10621
+ }
10622
+ },
10623
  "node_modules/promise-limit": {
10624
  "version": "2.7.0",
10625
  "resolved": "https://registry.npmjs.org/promise-limit/-/promise-limit-2.7.0.tgz",
 
10915
  }
10916
  }
10917
  },
10918
+ "node_modules/react-syntax-highlighter": {
10919
+ "version": "16.1.1",
10920
+ "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-16.1.1.tgz",
10921
+ "integrity": "sha512-PjVawBGy80C6YbC5DDZJeUjBmC7skaoEUdvfFQediQHgCL7aKyVHe57SaJGfQsloGDac+gCpTfRdtxzWWKmCXA==",
10922
+ "license": "MIT",
10923
+ "dependencies": {
10924
+ "@babel/runtime": "^7.28.4",
10925
+ "highlight.js": "^10.4.1",
10926
+ "highlightjs-vue": "^1.0.0",
10927
+ "lowlight": "^1.17.0",
10928
+ "prismjs": "^1.30.0",
10929
+ "refractor": "^5.0.0"
10930
+ },
10931
+ "engines": {
10932
+ "node": ">= 16.20.2"
10933
+ },
10934
+ "peerDependencies": {
10935
+ "react": ">= 0.14.0"
10936
+ }
10937
+ },
10938
  "node_modules/readable-stream": {
10939
  "version": "3.6.2",
10940
  "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
 
11018
  "url": "https://github.com/sponsors/ljharb"
11019
  }
11020
  },
11021
+ "node_modules/refractor": {
11022
+ "version": "5.0.0",
11023
+ "resolved": "https://registry.npmjs.org/refractor/-/refractor-5.0.0.tgz",
11024
+ "integrity": "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==",
11025
+ "license": "MIT",
11026
+ "dependencies": {
11027
+ "@types/hast": "^3.0.0",
11028
+ "@types/prismjs": "^1.0.0",
11029
+ "hastscript": "^9.0.0",
11030
+ "parse-entities": "^4.0.0"
11031
+ },
11032
+ "funding": {
11033
+ "type": "github",
11034
+ "url": "https://github.com/sponsors/wooorm"
11035
+ }
11036
+ },
11037
  "node_modules/regexp.prototype.flags": {
11038
  "version": "1.5.4",
11039
  "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
package.json CHANGED
@@ -46,6 +46,7 @@
46
  "react": "19.2.3",
47
  "react-dom": "19.2.3",
48
  "react-markdown": "^10.1.0",
 
49
  "recharts": "^3.7.0",
50
  "remark-gfm": "^4.0.1",
51
  "simple-git": "^3.32.3",
@@ -68,6 +69,7 @@
68
  "@types/node": "^20",
69
  "@types/react": "^19",
70
  "@types/react-dom": "^19",
 
71
  "@types/tar-fs": "^2.0.4",
72
  "eslint": "^9",
73
  "eslint-config-next": "16.1.6",
 
46
  "react": "19.2.3",
47
  "react-dom": "19.2.3",
48
  "react-markdown": "^10.1.0",
49
+ "react-syntax-highlighter": "^16.1.1",
50
  "recharts": "^3.7.0",
51
  "remark-gfm": "^4.0.1",
52
  "simple-git": "^3.32.3",
 
69
  "@types/node": "^20",
70
  "@types/react": "^19",
71
  "@types/react-dom": "^19",
72
+ "@types/react-syntax-highlighter": "^15.5.13",
73
  "@types/tar-fs": "^2.0.4",
74
  "eslint": "^9",
75
  "eslint-config-next": "16.1.6",