@jiangxiaosheng/digital-agent-platform 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -0
- package/eslint.config.mjs +20 -0
- package/messages/en.json +184 -0
- package/messages/zh.json +184 -0
- package/middleware.ts +9 -0
- package/next-env.d.ts +5 -0
- package/next.config.ts +16 -0
- package/package.json +59 -0
- package/postcss.config.mjs +5 -0
- package/src/app/[locale]/agents/[id]/chat/page.tsx +833 -0
- package/src/app/[locale]/agents/[id]/edit/page.tsx +58 -0
- package/src/app/[locale]/agents/new/page.tsx +52 -0
- package/src/app/[locale]/agents/page.tsx +122 -0
- package/src/app/[locale]/knowledge/page.tsx +305 -0
- package/src/app/[locale]/layout.tsx +46 -0
- package/src/app/[locale]/page.tsx +6 -0
- package/src/app/[locale]/settings/page.tsx +1204 -0
- package/src/app/api/agents/[id]/route.ts +27 -0
- package/src/app/api/agents/route.ts +27 -0
- package/src/app/api/auth-config/test/route.ts +69 -0
- package/src/app/api/bash-output/route.ts +57 -0
- package/src/app/api/chat/[agentId]/route.ts +164 -0
- package/src/app/api/env-vars/[key]/route.ts +22 -0
- package/src/app/api/env-vars/route.ts +26 -0
- package/src/app/api/env-vars/scan/route.ts +66 -0
- package/src/app/api/file-serve/route.ts +58 -0
- package/src/app/api/fs/browse/route.ts +47 -0
- package/src/app/api/knowledge/[id]/documents/[docId]/route.ts +22 -0
- package/src/app/api/knowledge/[id]/route.ts +21 -0
- package/src/app/api/knowledge/[id]/upload/route.ts +90 -0
- package/src/app/api/knowledge/route.ts +15 -0
- package/src/app/api/knowledge/search/route.ts +26 -0
- package/src/app/api/models/route.ts +160 -0
- package/src/app/api/models-config/route.ts +41 -0
- package/src/app/api/models-config/test/route.ts +84 -0
- package/src/app/api/sessions/[agentId]/[sessionId]/route.ts +12 -0
- package/src/app/api/sessions/[agentId]/route.ts +19 -0
- package/src/app/api/sessions/history/route.ts +166 -0
- package/src/app/api/settings/route.ts +45 -0
- package/src/app/api/skills/[name]/route.ts +40 -0
- package/src/app/api/skills/install/route.ts +35 -0
- package/src/app/api/skills/route.ts +54 -0
- package/src/app/api/skills/search/route.ts +60 -0
- package/src/app/api/tts/route.ts +235 -0
- package/src/app/api/voice-provider/route.ts +98 -0
- package/src/app/api/workspace-files/route.ts +151 -0
- package/src/app/globals.css +176 -0
- package/src/app/layout.tsx +13 -0
- package/src/app/page.tsx +6 -0
- package/src/components/agents/agent-form.tsx +457 -0
- package/src/components/agents/model-selector.tsx +290 -0
- package/src/components/agents/skills-manager.tsx +347 -0
- package/src/components/chat/input-bar.tsx +561 -0
- package/src/components/chat/message-bubble.tsx +395 -0
- package/src/components/layout/sidebar.tsx +304 -0
- package/src/components/providers.tsx +19 -0
- package/src/components/ui/badge.tsx +26 -0
- package/src/components/ui/button.tsx +43 -0
- package/src/components/ui/card.tsx +46 -0
- package/src/components/ui/dialog.tsx +74 -0
- package/src/components/ui/directory-picker.tsx +185 -0
- package/src/components/ui/input.tsx +18 -0
- package/src/components/ui/label.tsx +16 -0
- package/src/components/ui/scroll-area.tsx +41 -0
- package/src/components/ui/select.tsx +93 -0
- package/src/components/ui/slider.tsx +35 -0
- package/src/components/ui/switch.tsx +35 -0
- package/src/components/ui/tabs.tsx +48 -0
- package/src/components/ui/textarea.tsx +17 -0
- package/src/components/ui/toaster.tsx +50 -0
- package/src/hooks/use-toast.ts +43 -0
- package/src/i18n/request.ts +13 -0
- package/src/i18n/routing.ts +7 -0
- package/src/lib/agent-factory.ts +200 -0
- package/src/lib/agents/store.ts +95 -0
- package/src/lib/db/client.ts +25 -0
- package/src/lib/db/schema.ts +66 -0
- package/src/lib/env-vars.ts +33 -0
- package/src/lib/knowledge/chunker.ts +34 -0
- package/src/lib/knowledge/embedder.ts +48 -0
- package/src/lib/knowledge/retriever.ts +94 -0
- package/src/lib/knowledge/store.ts +74 -0
- package/src/lib/models-config.ts +55 -0
- package/src/lib/settings.ts +33 -0
- package/src/lib/skills.ts +79 -0
- package/src/lib/system-prompt.ts +52 -0
- package/src/lib/tool-builder.ts +19 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/voice-providers-config.ts +71 -0
- package/src/middleware.ts +9 -0
- package/src/types/agent.ts +124 -0
- package/src/types/knowledge.ts +39 -0
- package/src/types/settings.ts +31 -0
- package/tsconfig.json +40 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { agentStore } from "@/lib/agents/store";
|
|
3
|
+
import type { UpdateAgentInput } from "@/types/agent";
|
|
4
|
+
|
|
5
|
+
type Params = { params: Promise<{ id: string }> };
|
|
6
|
+
|
|
7
|
+
export async function GET(_req: NextRequest, { params }: Params) {
|
|
8
|
+
const { id } = await params;
|
|
9
|
+
const agent = agentStore.getById(id);
|
|
10
|
+
if (!agent) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
11
|
+
return NextResponse.json(agent);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function PUT(req: NextRequest, { params }: Params) {
|
|
15
|
+
const { id } = await params;
|
|
16
|
+
const body = (await req.json()) as UpdateAgentInput;
|
|
17
|
+
const updated = agentStore.update(id, body);
|
|
18
|
+
if (!updated) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
19
|
+
return NextResponse.json(updated);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function DELETE(_req: NextRequest, { params }: Params) {
|
|
23
|
+
const { id } = await params;
|
|
24
|
+
const ok = agentStore.delete(id);
|
|
25
|
+
if (!ok) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
26
|
+
return NextResponse.json({ success: true });
|
|
27
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { agentStore } from "@/lib/agents/store";
|
|
3
|
+
import { initSchema } from "@/lib/db/schema";
|
|
4
|
+
import type { CreateAgentInput } from "@/types/agent";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export async function GET() {
|
|
8
|
+
try {
|
|
9
|
+
const agents = agentStore.list();
|
|
10
|
+
return NextResponse.json(agents);
|
|
11
|
+
} catch (e) {
|
|
12
|
+
return NextResponse.json({ error: String(e) }, { status: 500 });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function POST(req: NextRequest) {
|
|
17
|
+
try {
|
|
18
|
+
const body = (await req.json()) as CreateAgentInput;
|
|
19
|
+
if (!body.name?.trim()) {
|
|
20
|
+
return NextResponse.json({ error: "Name is required" }, { status: 400 });
|
|
21
|
+
}
|
|
22
|
+
const agent = agentStore.create(body);
|
|
23
|
+
return NextResponse.json(agent, { status: 201 });
|
|
24
|
+
} catch (e) {
|
|
25
|
+
return NextResponse.json({ error: String(e) }, { status: 500 });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
export const dynamic = "force-dynamic";
|
|
4
|
+
|
|
5
|
+
const PROVIDER_TEST: Record<string, { url: string; authHeader: (key: string) => Record<string, string>; body: object }> = {
|
|
6
|
+
anthropic: {
|
|
7
|
+
url: "https://api.anthropic.com/v1/messages",
|
|
8
|
+
authHeader: (key) => ({ "x-api-key": key, "anthropic-version": "2023-06-01" }),
|
|
9
|
+
body: { model: "claude-haiku-4-5-20251001", max_tokens: 5, messages: [{ role: "user", content: "hi" }] },
|
|
10
|
+
},
|
|
11
|
+
openai: {
|
|
12
|
+
url: "https://api.openai.com/v1/chat/completions",
|
|
13
|
+
authHeader: (key) => ({ Authorization: `Bearer ${key}` }),
|
|
14
|
+
body: { model: "gpt-4o-mini", max_tokens: 5, messages: [{ role: "user", content: "hi" }] },
|
|
15
|
+
},
|
|
16
|
+
google: {
|
|
17
|
+
url: "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent",
|
|
18
|
+
authHeader: (key) => ({ "x-goog-api-key": key }),
|
|
19
|
+
body: { contents: [{ parts: [{ text: "hi" }] }] },
|
|
20
|
+
},
|
|
21
|
+
groq: {
|
|
22
|
+
url: "https://api.groq.com/openai/v1/chat/completions",
|
|
23
|
+
authHeader: (key) => ({ Authorization: `Bearer ${key}` }),
|
|
24
|
+
body: { model: "llama-3.1-8b-instant", max_tokens: 5, messages: [{ role: "user", content: "hi" }] },
|
|
25
|
+
},
|
|
26
|
+
deepseek: {
|
|
27
|
+
url: "https://api.deepseek.com/chat/completions",
|
|
28
|
+
authHeader: (key) => ({ Authorization: `Bearer ${key}` }),
|
|
29
|
+
body: { model: "deepseek-chat", max_tokens: 5, messages: [{ role: "user", content: "hi" }] },
|
|
30
|
+
},
|
|
31
|
+
xai: {
|
|
32
|
+
url: "https://api.x.ai/v1/chat/completions",
|
|
33
|
+
authHeader: (key) => ({ Authorization: `Bearer ${key}` }),
|
|
34
|
+
body: { model: "grok-2-latest", max_tokens: 5, messages: [{ role: "user", content: "hi" }] },
|
|
35
|
+
},
|
|
36
|
+
mistral: {
|
|
37
|
+
url: "https://api.mistral.ai/v1/chat/completions",
|
|
38
|
+
authHeader: (key) => ({ Authorization: `Bearer ${key}` }),
|
|
39
|
+
body: { model: "mistral-small-latest", max_tokens: 5, messages: [{ role: "user", content: "hi" }] },
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export async function POST(req: NextRequest) {
|
|
44
|
+
const start = Date.now();
|
|
45
|
+
try {
|
|
46
|
+
const { provider, key } = await req.json() as { provider: string; key: string };
|
|
47
|
+
if (!provider || !key) return NextResponse.json({ ok: false, error: "provider and key required" }, { status: 400 });
|
|
48
|
+
|
|
49
|
+
const cfg = PROVIDER_TEST[provider];
|
|
50
|
+
if (!cfg) return NextResponse.json({ ok: true, message: "No test defined for this provider, assuming OK" });
|
|
51
|
+
|
|
52
|
+
const res = await fetch(cfg.url, {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: { "Content-Type": "application/json", ...cfg.authHeader(key) },
|
|
55
|
+
body: JSON.stringify(cfg.body),
|
|
56
|
+
signal: AbortSignal.timeout(12000),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const latencyMs = Date.now() - start;
|
|
60
|
+
if (res.ok || res.status === 400) {
|
|
61
|
+
// 400 can mean bad params but auth was accepted
|
|
62
|
+
return NextResponse.json({ ok: true, latencyMs, status: res.status });
|
|
63
|
+
}
|
|
64
|
+
const errText = await res.text().catch(() => "");
|
|
65
|
+
return NextResponse.json({ ok: false, latencyMs, status: res.status, error: errText.slice(0, 200) });
|
|
66
|
+
} catch (e) {
|
|
67
|
+
return NextResponse.json({ ok: false, latencyMs: Date.now() - start, error: String(e) }, { status: 500 });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
|
|
6
|
+
export const dynamic = "force-dynamic";
|
|
7
|
+
|
|
8
|
+
const MAX_BYTES = 5 * 1024 * 1024; // 5 MB
|
|
9
|
+
const BASH_FILE_RE = /^pi-bash-[A-Za-z0-9_-]+\.log$/;
|
|
10
|
+
|
|
11
|
+
/** Validate path is a legitimate bash output file in tmpdir */
|
|
12
|
+
function validatePath(filePath: string): { ok: true } | { ok: false; error: string; status: number } {
|
|
13
|
+
const tmpDir = os.tmpdir();
|
|
14
|
+
const resolved = path.resolve(filePath);
|
|
15
|
+
// Must live directly inside tmpdir (no subdirectories)
|
|
16
|
+
if (path.dirname(resolved) !== tmpDir) {
|
|
17
|
+
return { ok: false, error: "forbidden", status: 403 };
|
|
18
|
+
}
|
|
19
|
+
// Must match the pi-bash-*.log naming pattern
|
|
20
|
+
if (!BASH_FILE_RE.test(path.basename(resolved))) {
|
|
21
|
+
return { ok: false, error: "forbidden", status: 403 };
|
|
22
|
+
}
|
|
23
|
+
return { ok: true };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function GET(req: NextRequest) {
|
|
27
|
+
const filePath = req.nextUrl.searchParams.get("path");
|
|
28
|
+
if (!filePath) {
|
|
29
|
+
return NextResponse.json({ error: "path required" }, { status: 400 });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const check = validatePath(filePath);
|
|
33
|
+
if (!check.ok) {
|
|
34
|
+
return NextResponse.json({ error: check.error }, { status: check.status });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const stat = fs.statSync(filePath);
|
|
39
|
+
if (!stat.isFile()) {
|
|
40
|
+
return NextResponse.json({ error: "not a file" }, { status: 404 });
|
|
41
|
+
}
|
|
42
|
+
if (stat.size > MAX_BYTES) {
|
|
43
|
+
return NextResponse.json(
|
|
44
|
+
{ error: `Output too large (${stat.size} bytes, limit ${MAX_BYTES})`, truncated: true },
|
|
45
|
+
{ status: 413 }
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const output = fs.readFileSync(filePath, "utf8");
|
|
49
|
+
return NextResponse.json({ output });
|
|
50
|
+
} catch (e: unknown) {
|
|
51
|
+
const code = (e as NodeJS.ErrnoException).code;
|
|
52
|
+
if (code === "ENOENT") {
|
|
53
|
+
return NextResponse.json({ error: "file not found" }, { status: 404 });
|
|
54
|
+
}
|
|
55
|
+
return NextResponse.json({ error: String(e) }, { status: 500 });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { NextRequest } from "next/server";
|
|
2
|
+
import { agentStore, sessionStore } from "@/lib/agents/store";
|
|
3
|
+
import { createSession, getAgentDir } from "@/lib/agent-factory";
|
|
4
|
+
import { readAuthConfig } from "@/lib/models-config";
|
|
5
|
+
import path from "path";
|
|
6
|
+
|
|
7
|
+
export const dynamic = "force-dynamic";
|
|
8
|
+
export const maxDuration = 300;
|
|
9
|
+
|
|
10
|
+
export async function POST(
|
|
11
|
+
req: NextRequest,
|
|
12
|
+
{ params }: { params: Promise<{ agentId: string }> }
|
|
13
|
+
) {
|
|
14
|
+
const { agentId } = await params;
|
|
15
|
+
const body = (await req.json()) as {
|
|
16
|
+
message: string;
|
|
17
|
+
sessionFile?: string;
|
|
18
|
+
sessionId?: string;
|
|
19
|
+
overrideSkills?: string[];
|
|
20
|
+
overrideProvider?: string;
|
|
21
|
+
overrideModelId?: string;
|
|
22
|
+
images?: { data: string; mimeType: string }[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const agent = agentStore.getById(agentId);
|
|
26
|
+
if (!agent) {
|
|
27
|
+
return new Response(
|
|
28
|
+
`data: ${JSON.stringify({ type: "error", message: "Agent not found" })}\n\n`,
|
|
29
|
+
{ status: 404, headers: { "Content-Type": "text/event-stream" } }
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Apply session-level overrides
|
|
34
|
+
const effectiveAgent = {
|
|
35
|
+
...agent,
|
|
36
|
+
...(body.overrideSkills !== undefined && { enabledSkills: body.overrideSkills }),
|
|
37
|
+
...(body.overrideProvider && body.overrideModelId && {
|
|
38
|
+
model: { ...agent.model, provider: body.overrideProvider, modelId: body.overrideModelId },
|
|
39
|
+
}),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const encoder = new TextEncoder();
|
|
43
|
+
let sessionFileSaved = false;
|
|
44
|
+
let sessionInstance: Awaited<ReturnType<typeof createSession>> | null = null;
|
|
45
|
+
|
|
46
|
+
const stream = new ReadableStream({
|
|
47
|
+
async start(controller) {
|
|
48
|
+
const send = (obj: object) => {
|
|
49
|
+
try { controller.enqueue(encoder.encode(`data: ${JSON.stringify(obj)}\n\n`)); } catch { /* closed */ }
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
req.signal.addEventListener("abort", () => {
|
|
53
|
+
send({ type: "aborted" });
|
|
54
|
+
sessionInstance?.dispose();
|
|
55
|
+
try { controller.close(); } catch { /* already closed */ }
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
sessionInstance = await createSession(effectiveAgent, body.message, body.sessionFile);
|
|
60
|
+
|
|
61
|
+
const unsub = sessionInstance.subscribe((event) => {
|
|
62
|
+
if (req.signal.aborted) { unsub(); return; }
|
|
63
|
+
try {
|
|
64
|
+
switch (event.type) {
|
|
65
|
+
case "message_update":
|
|
66
|
+
if (event.assistantMessageEvent?.type === "text_delta")
|
|
67
|
+
send({ type: "message_update", delta: event.assistantMessageEvent.delta });
|
|
68
|
+
else if (event.assistantMessageEvent?.type === "thinking_delta")
|
|
69
|
+
send({ type: "thinking_delta", delta: event.assistantMessageEvent.delta });
|
|
70
|
+
break;
|
|
71
|
+
case "tool_execution_start":
|
|
72
|
+
send({ type: "tool_start", toolName: event.toolName, toolCallId: event.toolCallId ?? event.toolName });
|
|
73
|
+
break;
|
|
74
|
+
case "tool_execution_update": {
|
|
75
|
+
// For bash, surface the command string directly (not raw JSON)
|
|
76
|
+
const args = (event as Record<string, unknown>).args as Record<string, unknown> | undefined;
|
|
77
|
+
const partial = (event as Record<string, unknown>).partialResult;
|
|
78
|
+
const content = typeof args?.command === "string"
|
|
79
|
+
? args.command
|
|
80
|
+
: JSON.stringify(partial ?? args ?? "");
|
|
81
|
+
send({ type: "tool_update", toolCallId: event.toolCallId ?? "", content });
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
case "tool_execution_end": {
|
|
85
|
+
// result for bash is BashToolDetails: { output, exitCode, truncated, fullOutputPath? }
|
|
86
|
+
const result = (event as Record<string, unknown>).result as Record<string, unknown> | undefined;
|
|
87
|
+
const isError = !!((event as Record<string, unknown>).isError);
|
|
88
|
+
// Inline output: bash provides result.output, other tools may return a string or object
|
|
89
|
+
const output: string | undefined = result
|
|
90
|
+
? (typeof result.output === "string" ? result.output
|
|
91
|
+
: typeof result === "string" ? result
|
|
92
|
+
: result.content !== undefined ? String(result.content)
|
|
93
|
+
: undefined)
|
|
94
|
+
: undefined;
|
|
95
|
+
send({
|
|
96
|
+
type: "tool_end",
|
|
97
|
+
toolCallId: event.toolCallId ?? "",
|
|
98
|
+
toolName: event.toolName,
|
|
99
|
+
isError,
|
|
100
|
+
output,
|
|
101
|
+
fullOutputPath: typeof result?.fullOutputPath === "string" ? result.fullOutputPath : undefined,
|
|
102
|
+
truncated: typeof result?.truncated === "boolean" ? result.truncated : false,
|
|
103
|
+
});
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
case "compaction_start": send({ type: "compaction_start" }); break;
|
|
107
|
+
case "compaction_end": send({ type: "compaction_end" }); break;
|
|
108
|
+
case "agent_settled": {
|
|
109
|
+
// Send session stats
|
|
110
|
+
try {
|
|
111
|
+
const stats = sessionInstance!.getSessionStats();
|
|
112
|
+
send({ type: "session_stats", stats: {
|
|
113
|
+
tokens: stats.tokens,
|
|
114
|
+
cost: stats.cost,
|
|
115
|
+
contextUsage: stats.contextUsage,
|
|
116
|
+
userMessages: stats.userMessages,
|
|
117
|
+
assistantMessages: stats.assistantMessages,
|
|
118
|
+
}});
|
|
119
|
+
} catch { /* stats not available */ }
|
|
120
|
+
|
|
121
|
+
const sf = sessionInstance!.sessionFile;
|
|
122
|
+
if (sf && !sessionFileSaved) {
|
|
123
|
+
sessionFileSaved = true;
|
|
124
|
+
send({ type: "session_file", path: sf });
|
|
125
|
+
if (!body.sessionId) sessionStore.create(agentId, sf, body.message.slice(0, 60));
|
|
126
|
+
}
|
|
127
|
+
send({ type: "done" });
|
|
128
|
+
unsub();
|
|
129
|
+
sessionInstance!.dispose();
|
|
130
|
+
sessionInstance = null;
|
|
131
|
+
try { controller.close(); } catch { /* already closed */ }
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} catch { /* ignore subscriber errors */ }
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (!req.signal.aborted) {
|
|
139
|
+
// Build prompt options with optional image attachments
|
|
140
|
+
const promptImages = body.images?.length
|
|
141
|
+
? body.images.map(img => ({ type: "image" as const, data: img.data, mimeType: img.mimeType }))
|
|
142
|
+
: undefined;
|
|
143
|
+
await sessionInstance.prompt(body.message, promptImages ? { images: promptImages } : undefined);
|
|
144
|
+
}
|
|
145
|
+
} catch (e) {
|
|
146
|
+
send({ type: "error", message: String(e) });
|
|
147
|
+
try { controller.close(); } catch { /* already closed */ }
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
cancel() {
|
|
151
|
+
sessionInstance?.dispose();
|
|
152
|
+
sessionInstance = null;
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
return new Response(stream, {
|
|
157
|
+
headers: {
|
|
158
|
+
"Content-Type": "text/event-stream",
|
|
159
|
+
"Cache-Control": "no-cache",
|
|
160
|
+
Connection: "keep-alive",
|
|
161
|
+
"X-Accel-Buffering": "no",
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { setEnvVar, deleteEnvVar } from "@/lib/env-vars";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
|
|
6
|
+
type Params = { params: Promise<{ key: string }> };
|
|
7
|
+
|
|
8
|
+
// PUT — update a single var
|
|
9
|
+
export async function PUT(req: NextRequest, { params }: Params) {
|
|
10
|
+
const { key } = await params;
|
|
11
|
+
const { value } = await req.json() as { value: string };
|
|
12
|
+
setEnvVar(decodeURIComponent(key), value ?? "");
|
|
13
|
+
return NextResponse.json({ success: true });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// DELETE — remove a single var
|
|
17
|
+
export async function DELETE(_req: NextRequest, { params }: Params) {
|
|
18
|
+
const { key } = await params;
|
|
19
|
+
const ok = deleteEnvVar(decodeURIComponent(key));
|
|
20
|
+
if (!ok) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
21
|
+
return NextResponse.json({ success: true });
|
|
22
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { readEnvVars, writeEnvVars, setEnvVar } from "@/lib/env-vars";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
|
|
6
|
+
// GET — list all env vars (values masked for secrets)
|
|
7
|
+
export async function GET() {
|
|
8
|
+
const vars = readEnvVars();
|
|
9
|
+
return NextResponse.json(vars);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// POST — add/update a var { key, value }
|
|
13
|
+
export async function POST(req: NextRequest) {
|
|
14
|
+
const { key, value } = await req.json() as { key: string; value: string };
|
|
15
|
+
if (!key?.trim()) return NextResponse.json({ error: "Key required" }, { status: 400 });
|
|
16
|
+
const validKey = /^[A-Za-z_][A-Za-z0-9_]*$/.test(key.trim());
|
|
17
|
+
if (!validKey) return NextResponse.json({ error: "Key must be a valid identifier (A-Z, a-z, 0-9, _)" }, { status: 400 });
|
|
18
|
+
setEnvVar(key.trim(), value ?? "");
|
|
19
|
+
return NextResponse.json({ success: true });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// DELETE — clear all vars
|
|
23
|
+
export async function DELETE() {
|
|
24
|
+
writeEnvVars({});
|
|
25
|
+
return NextResponse.json({ success: true });
|
|
26
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { readEnvVars } from "@/lib/env-vars";
|
|
3
|
+
import { listInstalledSkills, getSkillsDir } from "@/lib/skills";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
|
|
7
|
+
export const dynamic = "force-dynamic";
|
|
8
|
+
|
|
9
|
+
/** Extract $VAR and ${VAR} references from skill content, skipping common shell vars */
|
|
10
|
+
const SKIP_VARS = new Set([
|
|
11
|
+
"HOME","PATH","USER","SHELL","PWD","TERM","TMPDIR","LANG",
|
|
12
|
+
"LOGNAME","SHLVL","IFS","MANPATH","OLDPWD","RESPONSE",
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
function extractEnvRefs(content: string): string[] {
|
|
16
|
+
// Match $VAR and ${VAR} usage patterns
|
|
17
|
+
const usageMatches = content.match(/\$\{?([A-Z_][A-Z0-9_]+)\}?/g) ?? [];
|
|
18
|
+
const usageVars = usageMatches.map(m => m.replace(/[${}]/g, ""));
|
|
19
|
+
|
|
20
|
+
// Also match `export VAR=` and `VAR=` assignment patterns (left side, no $)
|
|
21
|
+
const assignMatches = content.match(/(?:^|\n)\s*(?:export\s+)?([A-Z_][A-Z0-9_]+)=/gm) ?? [];
|
|
22
|
+
const assignVars = assignMatches.map(m => m.replace(/.*?([A-Z_][A-Z0-9_]+)=.*/, "$1").trim());
|
|
23
|
+
|
|
24
|
+
return [...new Set([...usageVars, ...assignVars].filter(v => !SKIP_VARS.has(v)))].sort();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function GET() {
|
|
28
|
+
const stored = readEnvVars();
|
|
29
|
+
const skills = listInstalledSkills();
|
|
30
|
+
const skillsDir = getSkillsDir();
|
|
31
|
+
|
|
32
|
+
const results: {
|
|
33
|
+
skillName: string;
|
|
34
|
+
vars: { name: string; stored: boolean }[];
|
|
35
|
+
}[] = [];
|
|
36
|
+
|
|
37
|
+
for (const skill of skills) {
|
|
38
|
+
const mdPath = fs.statSync(skill.filePath).isDirectory()
|
|
39
|
+
? path.join(skill.filePath, "SKILL.md")
|
|
40
|
+
: skill.filePath;
|
|
41
|
+
if (!fs.existsSync(mdPath)) continue;
|
|
42
|
+
const content = fs.readFileSync(mdPath, "utf8");
|
|
43
|
+
const refs = extractEnvRefs(content);
|
|
44
|
+
if (refs.length === 0) continue;
|
|
45
|
+
results.push({
|
|
46
|
+
skillName: skill.name,
|
|
47
|
+
vars: refs.map(name => ({ name, stored: name in stored })),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Summary: all vars referenced across all skills
|
|
52
|
+
const allVars = new Map<string, { skills: string[]; stored: boolean }>();
|
|
53
|
+
for (const { skillName, vars } of results) {
|
|
54
|
+
for (const { name, stored } of vars) {
|
|
55
|
+
if (!allVars.has(name)) allVars.set(name, { skills: [], stored });
|
|
56
|
+
allVars.get(name)!.skills.push(skillName);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return NextResponse.json({
|
|
61
|
+
bySkill: results,
|
|
62
|
+
summary: [...allVars.entries()].map(([name, { skills, stored }]) => ({
|
|
63
|
+
name, skills, stored,
|
|
64
|
+
})).sort((a, b) => (a.stored ? 1 : 0) - (b.stored ? 1 : 0) || a.name.localeCompare(b.name)),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import os from "os";
|
|
5
|
+
|
|
6
|
+
export const dynamic = "force-dynamic";
|
|
7
|
+
|
|
8
|
+
const MIME_MAP: Record<string, string> = {
|
|
9
|
+
png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg",
|
|
10
|
+
gif: "image/gif", webp: "image/webp", svg: "image/svg+xml",
|
|
11
|
+
mp4: "video/mp4", webm: "video/webm", mov: "video/quicktime",
|
|
12
|
+
mp3: "audio/mpeg", wav: "audio/wav",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const ALLOWED_EXTS = new Set(Object.keys(MIME_MAP));
|
|
16
|
+
|
|
17
|
+
// Block access outside home directory or to sensitive paths
|
|
18
|
+
const BLOCKED_PREFIXES = ["/etc", "/usr", "/System", "/bin", "/sbin", "/var/log"];
|
|
19
|
+
|
|
20
|
+
function isSafePath(filePath: string): boolean {
|
|
21
|
+
const resolved = path.resolve(filePath.replace(/^~/, os.homedir()));
|
|
22
|
+
if (BLOCKED_PREFIXES.some(p => resolved.startsWith(p))) return false;
|
|
23
|
+
// Must be within home directory
|
|
24
|
+
if (!resolved.startsWith(os.homedir())) return false;
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function GET(req: NextRequest) {
|
|
29
|
+
const rawPath = req.nextUrl.searchParams.get("path");
|
|
30
|
+
if (!rawPath) return NextResponse.json({ error: "path required" }, { status: 400 });
|
|
31
|
+
|
|
32
|
+
const filePath = rawPath.replace(/^~/, os.homedir());
|
|
33
|
+
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
34
|
+
|
|
35
|
+
if (!ALLOWED_EXTS.has(ext)) {
|
|
36
|
+
return NextResponse.json({ error: "file type not allowed" }, { status: 403 });
|
|
37
|
+
}
|
|
38
|
+
if (!isSafePath(filePath)) {
|
|
39
|
+
return NextResponse.json({ error: "access denied" }, { status: 403 });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
|
44
|
+
return NextResponse.json({ error: "file not found" }, { status: 404 });
|
|
45
|
+
}
|
|
46
|
+
const buffer = fs.readFileSync(filePath);
|
|
47
|
+
const mime = MIME_MAP[ext] ?? "application/octet-stream";
|
|
48
|
+
return new NextResponse(buffer, {
|
|
49
|
+
headers: {
|
|
50
|
+
"Content-Type": mime,
|
|
51
|
+
"Cache-Control": "public, max-age=3600",
|
|
52
|
+
"Content-Disposition": `inline; filename="${path.basename(filePath)}"`,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
} catch {
|
|
56
|
+
return NextResponse.json({ error: "read failed" }, { status: 500 });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import os from "os";
|
|
5
|
+
|
|
6
|
+
export const dynamic = "force-dynamic";
|
|
7
|
+
|
|
8
|
+
export async function GET(req: NextRequest) {
|
|
9
|
+
const rawPath = req.nextUrl.searchParams.get("path") || os.homedir();
|
|
10
|
+
// Expand ~ to home
|
|
11
|
+
const target = rawPath.replace(/^~/, os.homedir());
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const stat = fs.statSync(target);
|
|
15
|
+
if (!stat.isDirectory()) {
|
|
16
|
+
return NextResponse.json({ error: "Not a directory" }, { status: 400 });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const entries = fs.readdirSync(target, { withFileTypes: true })
|
|
20
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith("."))
|
|
21
|
+
.map((e) => ({
|
|
22
|
+
name: e.name,
|
|
23
|
+
path: path.join(target, e.name),
|
|
24
|
+
}))
|
|
25
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
26
|
+
|
|
27
|
+
// Build breadcrumb
|
|
28
|
+
const parts = target.replace(os.homedir(), "~").split(path.sep).filter(Boolean);
|
|
29
|
+
const breadcrumb = parts.map((p, i) => ({
|
|
30
|
+
label: p,
|
|
31
|
+
path: i === 0 && p === "~"
|
|
32
|
+
? os.homedir()
|
|
33
|
+
: parts.slice(0, i + 1).join(path.sep).replace(/^~/, os.homedir()),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
return NextResponse.json({
|
|
37
|
+
current: target,
|
|
38
|
+
display: target.replace(os.homedir(), "~"),
|
|
39
|
+
breadcrumb,
|
|
40
|
+
entries,
|
|
41
|
+
canGoUp: target !== "/" && target !== os.homedir(),
|
|
42
|
+
parent: path.dirname(target),
|
|
43
|
+
});
|
|
44
|
+
} catch (e) {
|
|
45
|
+
return NextResponse.json({ error: String(e) }, { status: 500 });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { kbStore } from "@/lib/knowledge/store";
|
|
3
|
+
import { deleteChunksByDoc } from "@/lib/knowledge/retriever";
|
|
4
|
+
import { initSchema } from "@/lib/db/schema";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
|
|
7
|
+
initSchema();
|
|
8
|
+
|
|
9
|
+
type Params = { params: Promise<{ id: string; docId: string }> };
|
|
10
|
+
|
|
11
|
+
export async function DELETE(_req: NextRequest, { params }: Params) {
|
|
12
|
+
const { docId } = await params;
|
|
13
|
+
const docs = kbStore.listDocuments((await params).id);
|
|
14
|
+
const doc = docs.find((d) => d.id === docId);
|
|
15
|
+
if (doc?.filePath) {
|
|
16
|
+
try { fs.unlinkSync(doc.filePath); } catch { /* ignore */ }
|
|
17
|
+
}
|
|
18
|
+
deleteChunksByDoc(docId);
|
|
19
|
+
const ok = kbStore.deleteDocument(docId);
|
|
20
|
+
if (!ok) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
21
|
+
return NextResponse.json({ success: true });
|
|
22
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { kbStore } from "@/lib/knowledge/store";
|
|
3
|
+
import { deleteChunksByKb } from "@/lib/knowledge/retriever";
|
|
4
|
+
|
|
5
|
+
type Params = { params: Promise<{ id: string }> };
|
|
6
|
+
|
|
7
|
+
export async function GET(_req: NextRequest, { params }: Params) {
|
|
8
|
+
const { id } = await params;
|
|
9
|
+
const kb = kbStore.getById(id);
|
|
10
|
+
if (!kb) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
11
|
+
const docs = kbStore.listDocuments(id);
|
|
12
|
+
return NextResponse.json({ ...kb, documents: docs });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function DELETE(_req: NextRequest, { params }: Params) {
|
|
16
|
+
const { id } = await params;
|
|
17
|
+
deleteChunksByKb(id);
|
|
18
|
+
const ok = kbStore.delete(id);
|
|
19
|
+
if (!ok) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
20
|
+
return NextResponse.json({ success: true });
|
|
21
|
+
}
|