@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.
Files changed (94) hide show
  1. package/README.md +37 -0
  2. package/eslint.config.mjs +20 -0
  3. package/messages/en.json +184 -0
  4. package/messages/zh.json +184 -0
  5. package/middleware.ts +9 -0
  6. package/next-env.d.ts +5 -0
  7. package/next.config.ts +16 -0
  8. package/package.json +59 -0
  9. package/postcss.config.mjs +5 -0
  10. package/src/app/[locale]/agents/[id]/chat/page.tsx +833 -0
  11. package/src/app/[locale]/agents/[id]/edit/page.tsx +58 -0
  12. package/src/app/[locale]/agents/new/page.tsx +52 -0
  13. package/src/app/[locale]/agents/page.tsx +122 -0
  14. package/src/app/[locale]/knowledge/page.tsx +305 -0
  15. package/src/app/[locale]/layout.tsx +46 -0
  16. package/src/app/[locale]/page.tsx +6 -0
  17. package/src/app/[locale]/settings/page.tsx +1204 -0
  18. package/src/app/api/agents/[id]/route.ts +27 -0
  19. package/src/app/api/agents/route.ts +27 -0
  20. package/src/app/api/auth-config/test/route.ts +69 -0
  21. package/src/app/api/bash-output/route.ts +57 -0
  22. package/src/app/api/chat/[agentId]/route.ts +164 -0
  23. package/src/app/api/env-vars/[key]/route.ts +22 -0
  24. package/src/app/api/env-vars/route.ts +26 -0
  25. package/src/app/api/env-vars/scan/route.ts +66 -0
  26. package/src/app/api/file-serve/route.ts +58 -0
  27. package/src/app/api/fs/browse/route.ts +47 -0
  28. package/src/app/api/knowledge/[id]/documents/[docId]/route.ts +22 -0
  29. package/src/app/api/knowledge/[id]/route.ts +21 -0
  30. package/src/app/api/knowledge/[id]/upload/route.ts +90 -0
  31. package/src/app/api/knowledge/route.ts +15 -0
  32. package/src/app/api/knowledge/search/route.ts +26 -0
  33. package/src/app/api/models/route.ts +160 -0
  34. package/src/app/api/models-config/route.ts +41 -0
  35. package/src/app/api/models-config/test/route.ts +84 -0
  36. package/src/app/api/sessions/[agentId]/[sessionId]/route.ts +12 -0
  37. package/src/app/api/sessions/[agentId]/route.ts +19 -0
  38. package/src/app/api/sessions/history/route.ts +166 -0
  39. package/src/app/api/settings/route.ts +45 -0
  40. package/src/app/api/skills/[name]/route.ts +40 -0
  41. package/src/app/api/skills/install/route.ts +35 -0
  42. package/src/app/api/skills/route.ts +54 -0
  43. package/src/app/api/skills/search/route.ts +60 -0
  44. package/src/app/api/tts/route.ts +235 -0
  45. package/src/app/api/voice-provider/route.ts +98 -0
  46. package/src/app/api/workspace-files/route.ts +151 -0
  47. package/src/app/globals.css +176 -0
  48. package/src/app/layout.tsx +13 -0
  49. package/src/app/page.tsx +6 -0
  50. package/src/components/agents/agent-form.tsx +457 -0
  51. package/src/components/agents/model-selector.tsx +290 -0
  52. package/src/components/agents/skills-manager.tsx +347 -0
  53. package/src/components/chat/input-bar.tsx +561 -0
  54. package/src/components/chat/message-bubble.tsx +395 -0
  55. package/src/components/layout/sidebar.tsx +304 -0
  56. package/src/components/providers.tsx +19 -0
  57. package/src/components/ui/badge.tsx +26 -0
  58. package/src/components/ui/button.tsx +43 -0
  59. package/src/components/ui/card.tsx +46 -0
  60. package/src/components/ui/dialog.tsx +74 -0
  61. package/src/components/ui/directory-picker.tsx +185 -0
  62. package/src/components/ui/input.tsx +18 -0
  63. package/src/components/ui/label.tsx +16 -0
  64. package/src/components/ui/scroll-area.tsx +41 -0
  65. package/src/components/ui/select.tsx +93 -0
  66. package/src/components/ui/slider.tsx +35 -0
  67. package/src/components/ui/switch.tsx +35 -0
  68. package/src/components/ui/tabs.tsx +48 -0
  69. package/src/components/ui/textarea.tsx +17 -0
  70. package/src/components/ui/toaster.tsx +50 -0
  71. package/src/hooks/use-toast.ts +43 -0
  72. package/src/i18n/request.ts +13 -0
  73. package/src/i18n/routing.ts +7 -0
  74. package/src/lib/agent-factory.ts +200 -0
  75. package/src/lib/agents/store.ts +95 -0
  76. package/src/lib/db/client.ts +25 -0
  77. package/src/lib/db/schema.ts +66 -0
  78. package/src/lib/env-vars.ts +33 -0
  79. package/src/lib/knowledge/chunker.ts +34 -0
  80. package/src/lib/knowledge/embedder.ts +48 -0
  81. package/src/lib/knowledge/retriever.ts +94 -0
  82. package/src/lib/knowledge/store.ts +74 -0
  83. package/src/lib/models-config.ts +55 -0
  84. package/src/lib/settings.ts +33 -0
  85. package/src/lib/skills.ts +79 -0
  86. package/src/lib/system-prompt.ts +52 -0
  87. package/src/lib/tool-builder.ts +19 -0
  88. package/src/lib/utils.ts +6 -0
  89. package/src/lib/voice-providers-config.ts +71 -0
  90. package/src/middleware.ts +9 -0
  91. package/src/types/agent.ts +124 -0
  92. package/src/types/knowledge.ts +39 -0
  93. package/src/types/settings.ts +31 -0
  94. package/tsconfig.json +40 -0
@@ -0,0 +1,90 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { kbStore } from "@/lib/knowledge/store";
3
+ import { chunkText } from "@/lib/knowledge/chunker";
4
+ import { embedText } from "@/lib/knowledge/embedder";
5
+ import { storeChunk } from "@/lib/knowledge/retriever";
6
+ import { initSchema } from "@/lib/db/schema";
7
+ import path from "path";
8
+ import fs from "fs";
9
+ import os from "os";
10
+
11
+ export const dynamic = "force-dynamic";
12
+
13
+ initSchema();
14
+
15
+ export async function POST(
16
+ req: NextRequest,
17
+ { params }: { params: Promise<{ id: string }> }
18
+ ) {
19
+ const { id: kbId } = await params;
20
+
21
+ const kb = kbStore.getById(kbId);
22
+ if (!kb) return NextResponse.json({ error: "Not found" }, { status: 404 });
23
+
24
+ const formData = await req.formData();
25
+ const file = formData.get("file") as File | null;
26
+ if (!file) return NextResponse.json({ error: "No file" }, { status: 400 });
27
+
28
+ // Save file to disk
29
+ const uploadDir = path.join(os.homedir(), ".digital-agents", "uploads", kbId);
30
+ if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
31
+ const filePath = path.join(uploadDir, file.name);
32
+ const bytes = await file.arrayBuffer();
33
+ fs.writeFileSync(filePath, Buffer.from(bytes));
34
+
35
+ // Register document
36
+ const docId = kbStore.createDocument(kbId, file.name, filePath, file.type);
37
+
38
+ // Process async (don't await — respond immediately, process in background)
39
+ processDocument(kbId, docId, filePath, file.name, file.type).catch((e) => {
40
+ kbStore.updateDocumentStatus(docId, "error", 0, String(e));
41
+ });
42
+
43
+ return NextResponse.json({ docId, status: "processing" }, { status: 202 });
44
+ }
45
+
46
+ async function processDocument(
47
+ kbId: string,
48
+ docId: string,
49
+ filePath: string,
50
+ filename: string,
51
+ mimeType: string
52
+ ) {
53
+ // Parse document
54
+ const text = await parseDocument(filePath, mimeType);
55
+
56
+ // Chunk
57
+ const chunks = chunkText(text, { filename });
58
+
59
+ // Embed and store
60
+ let stored = 0;
61
+ for (const chunk of chunks) {
62
+ const embedding = await embedText(chunk.content);
63
+ storeChunk(kbId, docId, chunk.content, embedding, chunk.metadata);
64
+ stored++;
65
+ }
66
+
67
+ kbStore.updateDocumentStatus(docId, "ready", stored);
68
+ }
69
+
70
+ async function parseDocument(filePath: string, mimeType: string): Promise<string> {
71
+ const ext = path.extname(filePath).toLowerCase();
72
+
73
+ if (ext === ".pdf") {
74
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
75
+ const pdfParse = require("pdf-parse");
76
+ const buffer = fs.readFileSync(filePath);
77
+ const data = await pdfParse(buffer);
78
+ return data.text as string;
79
+ }
80
+
81
+ if (ext === ".docx" || mimeType.includes("wordprocessingml")) {
82
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
83
+ const mammoth = require("mammoth");
84
+ const result = await mammoth.extractRawText({ path: filePath });
85
+ return result.value as string;
86
+ }
87
+
88
+ // TXT / Markdown
89
+ return fs.readFileSync(filePath, "utf-8");
90
+ }
@@ -0,0 +1,15 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { kbStore } from "@/lib/knowledge/store";
3
+ import { initSchema } from "@/lib/db/schema";
4
+
5
+
6
+ export async function GET() {
7
+ return NextResponse.json(kbStore.list());
8
+ }
9
+
10
+ export async function POST(req: NextRequest) {
11
+ const { name, description } = (await req.json()) as { name: string; description?: string };
12
+ if (!name?.trim()) return NextResponse.json({ error: "Name required" }, { status: 400 });
13
+ const kb = kbStore.create(name, description);
14
+ return NextResponse.json(kb, { status: 201 });
15
+ }
@@ -0,0 +1,26 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { searchChunks } from "@/lib/knowledge/retriever";
3
+ import { embedText } from "@/lib/knowledge/embedder";
4
+ import { initSchema } from "@/lib/db/schema";
5
+
6
+
7
+ export async function POST(req: NextRequest) {
8
+ const { kbIds, query, topK = 5 } = (await req.json()) as {
9
+ kbIds: string[];
10
+ query: string;
11
+ topK?: number;
12
+ };
13
+ if (!query || !kbIds?.length) return NextResponse.json([]);
14
+
15
+ const queryVec = await embedText(query);
16
+ const results = await searchChunks(kbIds, queryVec, topK);
17
+
18
+ return NextResponse.json(
19
+ results.map((r) => ({
20
+ content: r.chunk.content,
21
+ score: r.score,
22
+ filename: r.chunk.metadata.filename,
23
+ kbId: r.chunk.kbId,
24
+ }))
25
+ );
26
+ }
@@ -0,0 +1,160 @@
1
+ import { NextResponse } from "next/server";
2
+ import path from "path";
3
+ import os from "os";
4
+
5
+ export const dynamic = "force-dynamic";
6
+
7
+ export type ModelInfo = {
8
+ id: string;
9
+ name: string;
10
+ providerId: string;
11
+ providerName: string;
12
+ api: string;
13
+ contextWindow: number;
14
+ maxTokens: number;
15
+ supportsThinking: boolean;
16
+ supportsTemperature: boolean;
17
+ thinkingLevels: string[];
18
+ inputTypes: string[];
19
+ costInput?: number;
20
+ costOutput?: number;
21
+ isCustom?: boolean;
22
+ };
23
+
24
+ export type ProviderGroup = {
25
+ id: string;
26
+ name: string;
27
+ models: ModelInfo[];
28
+ isCustom?: boolean;
29
+ };
30
+
31
+ interface SdkModel {
32
+ id: string;
33
+ name?: string;
34
+ api?: string;
35
+ provider: string;
36
+ contextWindow?: number;
37
+ maxTokens?: number;
38
+ reasoning?: boolean;
39
+ input?: string[];
40
+ cost?: { input?: number; output?: number };
41
+ thinkingLevelMap?: Record<string, string | null>;
42
+ compat?: { supportsTemperature?: boolean; [k: string]: unknown };
43
+ }
44
+
45
+ function getAgentDir() {
46
+ return process.env.PI_AGENT_DIR ?? path.join(os.homedir(), ".pi", "agent");
47
+ }
48
+
49
+ const PRIORITY = [
50
+ "anthropic", "openai", "google", "mistral", "groq",
51
+ "deepseek", "xai", "amazon-bedrock", "google-vertex",
52
+ ];
53
+
54
+ export async function GET() {
55
+ try {
56
+ const { ModelRuntime } = await import("@earendil-works/pi-coding-agent");
57
+
58
+ const agentDir = getAgentDir();
59
+ const modelsStorePath = path.join(agentDir, "models.json");
60
+ const authPath = path.join(agentDir, "auth.json");
61
+
62
+ const runtime = await ModelRuntime.create({ authPath, modelsStorePath });
63
+
64
+ const sdkProviders = runtime.getProviders();
65
+ const providerNameMap = new Map<string, string>(sdkProviders.map((p) => [p.id, p.name]));
66
+
67
+ const allModels = runtime.getModels() as unknown as SdkModel[];
68
+ const groupMap = new Map<string, ProviderGroup>();
69
+
70
+ // Read custom provider names from models.json for isCustom flag
71
+ const { readModelsConfig } = await import("@/lib/models-config");
72
+ const modelsConfig = readModelsConfig();
73
+ const customProviderIds = new Set(Object.keys(modelsConfig.providers));
74
+
75
+ // Read configured built-in providers from auth.json
76
+ const { readAuthConfig } = await import("@/lib/models-config");
77
+ const authConfig = readAuthConfig();
78
+ const configuredBuiltinIds = new Set(Object.keys(authConfig));
79
+
80
+ // A provider is "available" if it has a key in auth.json OR is a custom provider in models.json
81
+ const isProviderConfigured = (id: string) =>
82
+ customProviderIds.has(id) || configuredBuiltinIds.has(id);
83
+
84
+ for (const m of allModels) {
85
+ const providerId = m.provider ?? "unknown";
86
+
87
+ // Skip unconfigured built-in providers (show only configured or custom)
88
+ if (!isProviderConfigured(providerId)) continue;
89
+
90
+ const providerName = providerNameMap.get(providerId) ?? providerId;
91
+ const isCustom = customProviderIds.has(providerId);
92
+
93
+ const rawMap = m.thinkingLevelMap ?? {};
94
+ const thinkingLevels = Object.entries(rawMap)
95
+ .filter(([, v]) => v !== null)
96
+ .map(([k]) => k);
97
+
98
+ const supportsThinking = m.reasoning === true || thinkingLevels.length > 0;
99
+ const supportsTemperature = m.compat?.supportsTemperature !== false;
100
+
101
+ const info: ModelInfo = {
102
+ id: m.id,
103
+ name: m.name ?? m.id,
104
+ providerId,
105
+ providerName,
106
+ api: m.api ?? "",
107
+ contextWindow: m.contextWindow ?? 200000,
108
+ maxTokens: m.maxTokens ?? 8192,
109
+ supportsThinking,
110
+ supportsTemperature,
111
+ thinkingLevels,
112
+ inputTypes: m.input ?? ["text"],
113
+ costInput: m.cost?.input,
114
+ costOutput: m.cost?.output,
115
+ isCustom,
116
+ };
117
+
118
+ if (!groupMap.has(providerId)) {
119
+ groupMap.set(providerId, { id: providerId, name: providerName, models: [], isCustom });
120
+ }
121
+ groupMap.get(providerId)!.models.push(info);
122
+ }
123
+
124
+ const groups = [...groupMap.values()]
125
+ .filter((g) => g.models.length > 0)
126
+ .sort((a, b) => {
127
+ if (a.isCustom && !b.isCustom) return -1;
128
+ if (!a.isCustom && b.isCustom) return 1;
129
+ const ai = PRIORITY.indexOf(a.id), bi = PRIORITY.indexOf(b.id);
130
+ if (ai !== -1 && bi !== -1) return ai - bi;
131
+ if (ai !== -1) return -1;
132
+ if (bi !== -1) return 1;
133
+ return a.name.localeCompare(b.name);
134
+ });
135
+
136
+ return NextResponse.json(groups);
137
+ } catch (e) {
138
+ console.error("ModelRuntime error:", e);
139
+ return NextResponse.json(getFallback());
140
+ }
141
+ }
142
+
143
+ function getFallback(): ProviderGroup[] {
144
+ return [
145
+ {
146
+ id: "anthropic", name: "Anthropic", models: [
147
+ { id: "claude-opus-4-8", name: "Claude Opus 4.8", providerId: "anthropic", providerName: "Anthropic", api: "anthropic-messages", contextWindow: 1000000, maxTokens: 128000, supportsThinking: true, supportsTemperature: false, thinkingLevels: ["xhigh", "max"], inputTypes: ["text", "image"], costInput: 15, costOutput: 75 },
148
+ { id: "claude-sonnet-5", name: "Claude Sonnet 5", providerId: "anthropic", providerName: "Anthropic", api: "anthropic-messages", contextWindow: 1000000, maxTokens: 128000, supportsThinking: true, supportsTemperature: false, thinkingLevels: ["xhigh", "max"], inputTypes: ["text", "image"], costInput: 3, costOutput: 15 },
149
+ { id: "claude-opus-4-5", name: "Claude Opus 4.5", providerId: "anthropic", providerName: "Anthropic", api: "anthropic-messages", contextWindow: 200000, maxTokens: 64000, supportsThinking: false, supportsTemperature: true, thinkingLevels: [], inputTypes: ["text", "image"], costInput: 5, costOutput: 25 },
150
+ { id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5", providerId: "anthropic", providerName: "Anthropic", api: "anthropic-messages", contextWindow: 200000, maxTokens: 64000, supportsThinking: false, supportsTemperature: true, thinkingLevels: [], inputTypes: ["text", "image"], costInput: 1, costOutput: 5 },
151
+ ],
152
+ },
153
+ {
154
+ id: "openai", name: "OpenAI", models: [
155
+ { id: "gpt-4o", name: "GPT-4o", providerId: "openai", providerName: "OpenAI", api: "openai-responses", contextWindow: 128000, maxTokens: 16384, supportsThinking: false, supportsTemperature: true, thinkingLevels: [], inputTypes: ["text", "image"], costInput: 2.5, costOutput: 10 },
156
+ { id: "o3", name: "o3", providerId: "openai", providerName: "OpenAI", api: "openai-responses", contextWindow: 200000, maxTokens: 100000, supportsThinking: true, supportsTemperature: false, thinkingLevels: ["low", "medium", "high"], inputTypes: ["text", "image"], costInput: 10, costOutput: 40 },
157
+ ],
158
+ },
159
+ ];
160
+ }
@@ -0,0 +1,41 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import {
3
+ readModelsConfig, writeModelsConfig,
4
+ readAuthConfig, writeAuthConfig,
5
+ } from "@/lib/models-config";
6
+ import type { ModelsConfig } from "@/lib/models-config";
7
+
8
+ export const dynamic = "force-dynamic";
9
+
10
+ // GET — returns { modelsConfig, auth (masked) }
11
+ export async function GET() {
12
+ const modelsConfig = readModelsConfig();
13
+ const authRaw = readAuthConfig();
14
+ const auth: Record<string, string> = {};
15
+ for (const [k, v] of Object.entries(authRaw)) {
16
+ auth[k] = v.key ? "***" + v.key.slice(-4) : "";
17
+ }
18
+ return NextResponse.json({ modelsConfig, auth });
19
+ }
20
+
21
+ // PUT — body: { modelsConfig?, auth? }
22
+ export async function PUT(req: NextRequest) {
23
+ try {
24
+ const body = await req.json() as {
25
+ modelsConfig?: ModelsConfig;
26
+ auth?: Record<string, string>;
27
+ };
28
+ if (body.modelsConfig) writeModelsConfig(body.modelsConfig);
29
+ if (body.auth) {
30
+ const current = readAuthConfig();
31
+ for (const [provider, key] of Object.entries(body.auth)) {
32
+ if (!key) delete current[provider];
33
+ else if (!key.startsWith("***")) current[provider] = { type: "apiKey", key };
34
+ }
35
+ writeAuthConfig(current);
36
+ }
37
+ return NextResponse.json({ success: true });
38
+ } catch (e) {
39
+ return NextResponse.json({ error: String(e) }, { status: 500 });
40
+ }
41
+ }
@@ -0,0 +1,84 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { readAuthConfig } from "@/lib/models-config";
3
+
4
+ export const dynamic = "force-dynamic";
5
+
6
+ export async function POST(req: NextRequest) {
7
+ const start = Date.now();
8
+ try {
9
+ const { providerName, provider, model } = await req.json() as {
10
+ providerName: string;
11
+ provider: { baseUrl?: string; api?: string; apiKey?: string };
12
+ model: { id: string };
13
+ };
14
+
15
+ if (!model.id?.trim()) {
16
+ return NextResponse.json({ ok: false, error: "Model ID required" }, { status: 400 });
17
+ }
18
+
19
+ const baseUrl = provider.baseUrl?.replace(/\/$/, "") || "https://api.openai.com/v1";
20
+ let apiKey = provider.apiKey ?? "";
21
+
22
+ // Resolve API key: env var or shell command
23
+ if (apiKey.startsWith("!")) {
24
+ const { execSync } = await import("child_process");
25
+ apiKey = execSync(apiKey.slice(1)).toString().trim();
26
+ } else if (apiKey && !apiKey.startsWith("sk-") && !apiKey.includes(" ") && apiKey === apiKey.toUpperCase()) {
27
+ apiKey = process.env[apiKey] ?? apiKey;
28
+ }
29
+
30
+ // Also try from auth.json if no key provided
31
+ if (!apiKey) {
32
+ const auth = readAuthConfig();
33
+ apiKey = auth[providerName]?.key ?? "";
34
+ }
35
+
36
+ const api = provider.api ?? "openai-responses";
37
+ let status = 0;
38
+ let responseText = "";
39
+
40
+ if (api === "anthropic-messages") {
41
+ const res = await fetch(`${baseUrl}/v1/messages`, {
42
+ method: "POST",
43
+ headers: {
44
+ "Content-Type": "application/json",
45
+ "x-api-key": apiKey,
46
+ "anthropic-version": "2023-06-01",
47
+ },
48
+ body: JSON.stringify({
49
+ model: model.id,
50
+ max_tokens: 10,
51
+ messages: [{ role: "user", content: "ping" }],
52
+ }),
53
+ signal: AbortSignal.timeout(15000),
54
+ });
55
+ status = res.status;
56
+ const data = await res.json();
57
+ if (!res.ok) return NextResponse.json({ ok: false, error: data.error?.message ?? JSON.stringify(data), latencyMs: Date.now() - start, status });
58
+ responseText = data.content?.[0]?.text ?? "";
59
+ } else {
60
+ // OpenAI-compatible
61
+ const res = await fetch(`${baseUrl}/chat/completions`, {
62
+ method: "POST",
63
+ headers: {
64
+ "Content-Type": "application/json",
65
+ Authorization: `Bearer ${apiKey}`,
66
+ },
67
+ body: JSON.stringify({
68
+ model: model.id,
69
+ max_tokens: 10,
70
+ messages: [{ role: "user", content: "ping" }],
71
+ }),
72
+ signal: AbortSignal.timeout(15000),
73
+ });
74
+ status = res.status;
75
+ const data = await res.json();
76
+ if (!res.ok) return NextResponse.json({ ok: false, error: data.error?.message ?? JSON.stringify(data), latencyMs: Date.now() - start, status });
77
+ responseText = data.choices?.[0]?.message?.content ?? "";
78
+ }
79
+
80
+ return NextResponse.json({ ok: true, latencyMs: Date.now() - start, status, responseText });
81
+ } catch (e) {
82
+ return NextResponse.json({ ok: false, error: String(e), latencyMs: Date.now() - start }, { status: 500 });
83
+ }
84
+ }
@@ -0,0 +1,12 @@
1
+ import { NextResponse } from "next/server";
2
+ import { sessionStore } from "@/lib/agents/store";
3
+
4
+ type Params = { params: Promise<{ agentId: string; sessionId: string }> };
5
+
6
+ // DELETE — remove a single session record (DB only, JSONL file untouched)
7
+ export async function DELETE(_req: Request, { params }: Params) {
8
+ const { sessionId } = await params;
9
+ const ok = sessionStore.delete(sessionId);
10
+ if (!ok) return NextResponse.json({ error: "Not found" }, { status: 404 });
11
+ return NextResponse.json({ success: true });
12
+ }
@@ -0,0 +1,19 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { sessionStore } from "@/lib/agents/store";
3
+ import { initSchema } from "@/lib/db/schema";
4
+
5
+
6
+ type Params = { params: Promise<{ agentId: string }> };
7
+
8
+ // GET — list all sessions for an agent
9
+ export async function GET(_req: NextRequest, { params }: Params) {
10
+ const { agentId } = await params;
11
+ return NextResponse.json(sessionStore.list(agentId));
12
+ }
13
+
14
+ // DELETE — clear ALL sessions for an agent
15
+ export async function DELETE(_req: NextRequest, { params }: Params) {
16
+ const { agentId } = await params;
17
+ const count = sessionStore.deleteByAgent(agentId);
18
+ return NextResponse.json({ success: true, deleted: count });
19
+ }
@@ -0,0 +1,166 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import fs from "fs";
3
+
4
+ export const dynamic = "force-dynamic";
5
+
6
+ interface ContentBlock {
7
+ type: string;
8
+ text?: string;
9
+ thinking?: string;
10
+ name?: string; // tool_use: tool name
11
+ id?: string; // tool_use: call id
12
+ input?: unknown; // tool_use: args
13
+ content?: unknown; // tool_result: result
14
+ is_error?: boolean; // tool_result: error flag
15
+ }
16
+
17
+ interface JournalEntry {
18
+ type: string;
19
+ id: string;
20
+ parentId: string | null;
21
+ timestamp: string;
22
+ message?: {
23
+ role: "user" | "assistant";
24
+ content: string | ContentBlock[];
25
+ stopReason?: string;
26
+ errorMessage?: string;
27
+ };
28
+ }
29
+
30
+ interface HistoryToolCall {
31
+ id: string;
32
+ name: string;
33
+ input?: string;
34
+ output?: string;
35
+ isError?: boolean;
36
+ }
37
+
38
+ interface HistoryMessage {
39
+ id: string;
40
+ role: "user" | "assistant";
41
+ content: string;
42
+ thinking?: string;
43
+ toolCalls?: HistoryToolCall[];
44
+ createdAt: string;
45
+ }
46
+
47
+ /** Extract text, thinking, and tool calls from a message content block array */
48
+ function extractContent(content: string | ContentBlock[]): {
49
+ text: string;
50
+ thinking?: string;
51
+ toolUses: Map<string, HistoryToolCall>;
52
+ } {
53
+ if (typeof content === "string") return { text: content, toolUses: new Map() };
54
+
55
+ const textParts: string[] = [];
56
+ const thinkingParts: string[] = [];
57
+ const toolUses = new Map<string, HistoryToolCall>();
58
+
59
+ for (const block of content) {
60
+ if (block.type === "text" && block.text) textParts.push(block.text);
61
+ else if (block.type === "thinking" && block.thinking) thinkingParts.push(block.thinking);
62
+ else if (block.type === "tool_use" && block.id && block.name) {
63
+ toolUses.set(block.id, {
64
+ id: block.id,
65
+ name: block.name,
66
+ input: block.input !== undefined ? JSON.stringify(block.input, null, 2) : undefined,
67
+ });
68
+ }
69
+ }
70
+
71
+ return {
72
+ text: textParts.join(""),
73
+ thinking: thinkingParts.join("") || undefined,
74
+ toolUses,
75
+ };
76
+ }
77
+
78
+ function readSessionMessages(sessionFile: string): HistoryMessage[] {
79
+ if (!fs.existsSync(sessionFile)) return [];
80
+
81
+ const lines = fs.readFileSync(sessionFile, "utf8").split("\n").filter(Boolean);
82
+ const entries: JournalEntry[] = [];
83
+ for (const line of lines) {
84
+ try { entries.push(JSON.parse(line) as JournalEntry); } catch { /* skip */ }
85
+ }
86
+
87
+ // Build id → entry map and hasChild set
88
+ const idMap = new Map(entries.map((e) => [e.id, e]));
89
+ const hasChild = new Set<string>();
90
+ for (const e of entries) { if (e.parentId) hasChild.add(e.parentId); }
91
+
92
+ // Find latest leaf
93
+ const leaves = entries.filter((e) => !hasChild.has(e.id));
94
+ if (!leaves.length) return [];
95
+ const leaf = leaves[leaves.length - 1];
96
+
97
+ // Walk leaf → root
98
+ const path: JournalEntry[] = [];
99
+ let cur: JournalEntry | undefined = leaf;
100
+ while (cur) {
101
+ path.unshift(cur);
102
+ cur = cur.parentId ? idMap.get(cur.parentId) : undefined;
103
+ }
104
+
105
+ // Collect tool_result outputs keyed by tool_call_id from user messages
106
+ const toolResults = new Map<string, { output: string; isError: boolean }>();
107
+ for (const entry of path) {
108
+ if (entry.type !== "message" || !entry.message || entry.message.role !== "user") continue;
109
+ const blocks = Array.isArray(entry.message.content) ? entry.message.content : [];
110
+ for (const block of blocks) {
111
+ if (block.type === "tool_result" && block.id) {
112
+ const out = typeof block.content === "string"
113
+ ? block.content
114
+ : Array.isArray(block.content)
115
+ ? (block.content as ContentBlock[]).filter(b => b.type === "text").map(b => b.text ?? "").join("")
116
+ : "";
117
+ toolResults.set(block.id, { output: out, isError: !!block.is_error });
118
+ }
119
+ }
120
+ }
121
+
122
+ const messages: HistoryMessage[] = [];
123
+ for (const entry of path) {
124
+ if (entry.type !== "message" || !entry.message) continue;
125
+ const { role, content, stopReason, errorMessage } = entry.message;
126
+ if (role === "assistant" && (stopReason === "error" || errorMessage)) continue;
127
+
128
+ const { text, thinking, toolUses } = extractContent(content);
129
+ const visibleText = text.trim();
130
+ const hasToolUses = toolUses.size > 0;
131
+
132
+ if (!visibleText && !hasToolUses) continue;
133
+ // Skip user messages that are pure tool_result blocks
134
+ if (role === "user" && !visibleText && Array.isArray(content) &&
135
+ content.every(b => b.type === "tool_result")) continue;
136
+
137
+ // Merge tool outputs into tool calls
138
+ const toolCalls: HistoryToolCall[] = [];
139
+ for (const [id, tc] of toolUses.entries()) {
140
+ const result = toolResults.get(id);
141
+ toolCalls.push({ ...tc, output: result?.output, isError: result?.isError });
142
+ }
143
+
144
+ messages.push({
145
+ id: entry.id,
146
+ role,
147
+ content: visibleText,
148
+ thinking: thinking || undefined,
149
+ toolCalls: toolCalls.length ? toolCalls : undefined,
150
+ createdAt: entry.timestamp,
151
+ });
152
+ }
153
+
154
+ return messages;
155
+ }
156
+
157
+ export async function POST(req: NextRequest) {
158
+ try {
159
+ const { sessionFile } = await req.json() as { sessionFile: string };
160
+ if (!sessionFile) return NextResponse.json({ error: "sessionFile required" }, { status: 400 });
161
+ const messages = readSessionMessages(sessionFile);
162
+ return NextResponse.json({ messages });
163
+ } catch (e) {
164
+ return NextResponse.json({ error: String(e) }, { status: 500 });
165
+ }
166
+ }
@@ -0,0 +1,45 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { getSettings, saveSettings } from "@/lib/settings";
3
+ import { initSchema } from "@/lib/db/schema";
4
+ import type { AppSettings } from "@/types/settings";
5
+
6
+
7
+ export async function GET() {
8
+ const settings = getSettings();
9
+ // Mask API keys in response
10
+ const masked = {
11
+ ...settings,
12
+ apiKeys: Object.fromEntries(
13
+ Object.entries(settings.apiKeys).map(([k, v]) => [
14
+ k,
15
+ v ? "***" + v.slice(-4) : "",
16
+ ])
17
+ ),
18
+ };
19
+ return NextResponse.json(masked);
20
+ }
21
+
22
+ export async function PUT(req: NextRequest) {
23
+ try {
24
+ const body = (await req.json()) as Partial<AppSettings>;
25
+ const current = getSettings();
26
+ // Merge — if API key value starts with ***, keep existing
27
+ const apiKeys = { ...current.apiKeys };
28
+ if (body.apiKeys) {
29
+ for (const [provider, val] of Object.entries(body.apiKeys)) {
30
+ if (val && !val.startsWith("***")) {
31
+ (apiKeys as Record<string, string>)[provider] = val;
32
+ }
33
+ }
34
+ }
35
+ const merged: AppSettings = {
36
+ ...current,
37
+ ...body,
38
+ apiKeys,
39
+ };
40
+ saveSettings(merged);
41
+ return NextResponse.json({ success: true });
42
+ } catch (e) {
43
+ return NextResponse.json({ error: String(e) }, { status: 500 });
44
+ }
45
+ }