@frumu/tandem-panel 0.4.31 → 0.4.33
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/.env.example +13 -0
- package/bin/setup.js +319 -58
- package/dist/assets/{index-Bk_1J3q1.css → index-B2g55O4_.css} +1 -1
- package/dist/assets/{index-Cm325d0n.js → index-DlJxMgEA.js} +53 -53
- package/dist/index.html +2 -2
- package/lib/setup/paths.js +11 -0
- package/package.json +3 -3
- package/server/routes/knowledgebase.js +157 -0
package/dist/index.html
CHANGED
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Rubik:wght@500;700;800&family=Manrope:wght@400;500;600;700&display=swap"
|
|
12
12
|
rel="stylesheet"
|
|
13
13
|
/>
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-DlJxMgEA.js"></script>
|
|
15
15
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-Dgq8h090.js">
|
|
16
16
|
<link rel="modulepreload" crossorigin href="/assets/motion-CDTIPPIC.js">
|
|
17
17
|
<link rel="modulepreload" crossorigin href="/assets/react-query-KP80i6J5.js">
|
|
18
18
|
<link rel="modulepreload" crossorigin href="/assets/preact-vendor-DunqJIIk.js">
|
|
19
19
|
<link rel="modulepreload" crossorigin href="/assets/vendor-DwUHlh0G.js">
|
|
20
20
|
<link rel="modulepreload" crossorigin href="/assets/markdown-Dd89TVib.js">
|
|
21
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
21
|
+
<link rel="stylesheet" crossorigin href="/assets/index-B2g55O4_.css">
|
|
22
22
|
</head>
|
|
23
23
|
<body>
|
|
24
24
|
<div id="app"></div>
|
package/lib/setup/paths.js
CHANGED
|
@@ -5,6 +5,9 @@ function defaultConfigBase(platform, home, env) {
|
|
|
5
5
|
if (platform === "darwin") {
|
|
6
6
|
return env.XDG_CONFIG_HOME || join(home, "Library", "Application Support");
|
|
7
7
|
}
|
|
8
|
+
if (platform === "win32") {
|
|
9
|
+
return env.XDG_CONFIG_HOME || env.APPDATA || join(home, "AppData", "Roaming");
|
|
10
|
+
}
|
|
8
11
|
return env.XDG_CONFIG_HOME || join(home, ".config");
|
|
9
12
|
}
|
|
10
13
|
|
|
@@ -12,6 +15,14 @@ function defaultDataBase(platform, home, env) {
|
|
|
12
15
|
if (platform === "darwin") {
|
|
13
16
|
return env.XDG_DATA_HOME || join(home, "Library", "Application Support");
|
|
14
17
|
}
|
|
18
|
+
if (platform === "win32") {
|
|
19
|
+
return (
|
|
20
|
+
env.XDG_DATA_HOME ||
|
|
21
|
+
env.LOCALAPPDATA ||
|
|
22
|
+
env.APPDATA ||
|
|
23
|
+
join(home, "AppData", "Local")
|
|
24
|
+
);
|
|
25
|
+
}
|
|
15
26
|
return env.XDG_DATA_HOME || join(home, ".local", "share");
|
|
16
27
|
}
|
|
17
28
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frumu/tandem-panel",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.33",
|
|
4
4
|
"description": "Full web control center for Tandem Engine (chat, routines, swarm, memory, channels, and ops)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"@fullcalendar/interaction": "6.1.20",
|
|
47
47
|
"@fullcalendar/react": "6.1.20",
|
|
48
48
|
"@fullcalendar/timegrid": "6.1.20",
|
|
49
|
-
"@frumu/tandem": "^0.4.
|
|
50
|
-
"@frumu/tandem-client": "^0.4.
|
|
49
|
+
"@frumu/tandem": "^0.4.33",
|
|
50
|
+
"@frumu/tandem-client": "^0.4.33",
|
|
51
51
|
"@tanstack/react-query": "^5.90.21",
|
|
52
52
|
"dompurify": "^3.3.1",
|
|
53
53
|
"lucide": "^0.575.0",
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
function readOptionalTokenFile(pathname) {
|
|
4
|
+
const target = String(pathname || "").trim();
|
|
5
|
+
if (!target) return "";
|
|
6
|
+
try {
|
|
7
|
+
return readFileSync(target, "utf8").trim();
|
|
8
|
+
} catch {
|
|
9
|
+
return "";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalizeBaseUrl(raw) {
|
|
14
|
+
return String(raw || "").trim().replace(/\/+$/, "");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function copyForwardHeaders(req) {
|
|
18
|
+
const headers = new Headers();
|
|
19
|
+
for (const [key, value] of Object.entries(req.headers || {})) {
|
|
20
|
+
if (value == null || value === "") continue;
|
|
21
|
+
const lower = key.toLowerCase();
|
|
22
|
+
if (["host", "cookie", "authorization", "content-length", "connection", "transfer-encoding"].includes(lower)) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (Array.isArray(value)) headers.set(key, value.join(", "));
|
|
26
|
+
else headers.set(key, String(value));
|
|
27
|
+
}
|
|
28
|
+
return headers;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function proxyResponse(res, upstream) {
|
|
32
|
+
const responseHeaders = {};
|
|
33
|
+
upstream.headers.forEach((value, key) => {
|
|
34
|
+
const lower = key.toLowerCase();
|
|
35
|
+
if (["content-encoding", "transfer-encoding", "connection"].includes(lower)) return;
|
|
36
|
+
responseHeaders[key] = value;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
res.writeHead(upstream.status, responseHeaders);
|
|
41
|
+
if (!upstream.body) {
|
|
42
|
+
res.end();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
for await (const chunk of upstream.body) {
|
|
46
|
+
if (res.writableEnded || res.destroyed) break;
|
|
47
|
+
res.write(chunk);
|
|
48
|
+
}
|
|
49
|
+
if (!res.writableEnded && !res.destroyed) res.end();
|
|
50
|
+
} catch (error) {
|
|
51
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
52
|
+
if (res.headersSent) {
|
|
53
|
+
if (!res.writableEnded && !res.destroyed) res.end();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
res.writeHead(502, { "content-type": "application/json" });
|
|
57
|
+
res.end(JSON.stringify({ ok: false, error: `KB proxy stream failed: ${message}` }));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readKbAdminToken(deps) {
|
|
62
|
+
return (
|
|
63
|
+
String(deps.TANDEM_KB_ADMIN_API_KEY || deps.KB_ADMIN_API_KEY || "").trim() ||
|
|
64
|
+
readOptionalTokenFile(deps.TANDEM_KB_ADMIN_API_KEY_FILE || deps.KB_ADMIN_API_KEY_FILE || "")
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function resolveTargetPath(pathname) {
|
|
69
|
+
const parts = String(pathname || "").split("/").filter(Boolean);
|
|
70
|
+
if (parts[0] !== "api" || parts[1] !== "knowledgebase") return "";
|
|
71
|
+
const rest = parts.slice(2);
|
|
72
|
+
if (!rest.length) return "";
|
|
73
|
+
|
|
74
|
+
if (rest[0] === "collections") return "/admin/collections";
|
|
75
|
+
if (rest[0] === "documents" && rest.length === 1) return "/admin/documents";
|
|
76
|
+
if (rest[0] === "documents" && rest.length >= 3) {
|
|
77
|
+
return `/admin/documents/${encodeURIComponent(rest[1])}/${encodeURIComponent(rest[2])}`;
|
|
78
|
+
}
|
|
79
|
+
if (rest[0] === "reindex") return "/admin/reindex";
|
|
80
|
+
if (rest[0] === "config") return "/admin/config";
|
|
81
|
+
return "";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function createKnowledgebaseApiHandler(deps) {
|
|
85
|
+
const {
|
|
86
|
+
PORTAL_PORT,
|
|
87
|
+
TANDEM_KB_ADMIN_URL,
|
|
88
|
+
sendJson,
|
|
89
|
+
} = deps;
|
|
90
|
+
|
|
91
|
+
return async function handleKnowledgebaseApi(req, res) {
|
|
92
|
+
const incoming = new URL(req.url, `http://127.0.0.1:${PORTAL_PORT}`);
|
|
93
|
+
const targetPath = resolveTargetPath(incoming.pathname);
|
|
94
|
+
if (!targetPath) {
|
|
95
|
+
sendJson(res, 404, { ok: false, error: "Unknown knowledgebase route." });
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const baseUrl = normalizeBaseUrl(TANDEM_KB_ADMIN_URL || deps.KB_ADMIN_URL || "");
|
|
100
|
+
if (!baseUrl) {
|
|
101
|
+
sendJson(res, 503, {
|
|
102
|
+
ok: false,
|
|
103
|
+
error: "Knowledgebase admin URL is not configured. Set TANDEM_KB_ADMIN_URL to enable KB uploads.",
|
|
104
|
+
});
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (incoming.pathname === "/api/knowledgebase/config" && req.method === "GET") {
|
|
109
|
+
sendJson(res, 200, {
|
|
110
|
+
ok: true,
|
|
111
|
+
configured: true,
|
|
112
|
+
admin_url: baseUrl,
|
|
113
|
+
default_collection_id: String(deps.TANDEM_KB_DEFAULT_COLLECTION_ID || deps.KB_DEFAULT_COLLECTION_ID || "").trim(),
|
|
114
|
+
});
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const method = req.method || "GET";
|
|
119
|
+
if (
|
|
120
|
+
!["GET", "POST", "PUT", "DELETE", "PATCH"].includes(method) ||
|
|
121
|
+
(incoming.pathname === "/api/knowledgebase/collections" && method !== "GET") ||
|
|
122
|
+
(incoming.pathname === "/api/knowledgebase/documents" && !["GET", "POST"].includes(method)) ||
|
|
123
|
+
(incoming.pathname.startsWith("/api/knowledgebase/documents/") && !["GET", "PUT", "DELETE"].includes(method)) ||
|
|
124
|
+
(incoming.pathname === "/api/knowledgebase/reindex" && method !== "POST")
|
|
125
|
+
) {
|
|
126
|
+
sendJson(res, 405, { ok: false, error: "Method not allowed." });
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const targetUrl = `${baseUrl}${targetPath}${incoming.search}`;
|
|
131
|
+
const headers = copyForwardHeaders(req);
|
|
132
|
+
const token = readKbAdminToken(deps);
|
|
133
|
+
if (token) headers.set("authorization", `Bearer ${token}`);
|
|
134
|
+
if (!headers.has("accept")) headers.set("accept", "application/json");
|
|
135
|
+
|
|
136
|
+
const hasBody = !["GET", "HEAD"].includes(method);
|
|
137
|
+
|
|
138
|
+
let upstream;
|
|
139
|
+
try {
|
|
140
|
+
upstream = await fetch(targetUrl, {
|
|
141
|
+
method,
|
|
142
|
+
headers,
|
|
143
|
+
body: hasBody ? req : undefined,
|
|
144
|
+
duplex: hasBody ? "half" : undefined,
|
|
145
|
+
});
|
|
146
|
+
} catch (error) {
|
|
147
|
+
sendJson(res, 502, {
|
|
148
|
+
ok: false,
|
|
149
|
+
error: `Knowledgebase admin unreachable: ${error instanceof Error ? error.message : String(error)}`,
|
|
150
|
+
});
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
await proxyResponse(res, upstream);
|
|
155
|
+
return true;
|
|
156
|
+
};
|
|
157
|
+
}
|