@frumu/tandem-panel 0.4.7 → 0.4.8
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 +11 -0
- package/README.md +36 -2
- package/bin/setup.js +188 -0
- package/dist/assets/index-BIn54WIy.js +2460 -0
- package/dist/assets/index-CCT37xUj.css +1 -0
- package/dist/assets/{markdown-CscybTAD.js → markdown-DMcD1LHz.js} +13 -13
- package/dist/assets/{motion-1UTHZOUD.js → motion-BCvrfAt1.js} +2 -2
- package/dist/assets/preact-vendor-jo0muZ28.js +1 -0
- package/dist/assets/{react-query-tfsRN0su.js → react-query-CeeFMKtE.js} +1 -1
- package/dist/assets/vendor-UXzYZoAT.js +180 -0
- package/dist/index.html +7 -7
- package/package.json +5 -4
- package/dist/assets/index-BklSdD7w.js +0 -558
- package/dist/assets/index-CGq1XaYq.css +0 -1
- package/dist/assets/preact-vendor-CErzQRjo.js +0 -1
- package/dist/assets/vendor-B5SLbWPm.js +0 -42
package/.env.example
CHANGED
|
@@ -36,6 +36,17 @@ TANDEM_PROVIDER_STREAM_IDLE_TIMEOUT_MS=90000
|
|
|
36
36
|
TANDEM_PERMISSION_WAIT_TIMEOUT_MS=15000
|
|
37
37
|
TANDEM_TOOL_EXEC_TIMEOUT_MS=45000
|
|
38
38
|
TANDEM_BASH_TIMEOUT_MS=30000
|
|
39
|
+
|
|
40
|
+
# Built-in websearch defaults. `auto` can try configured backends in order
|
|
41
|
+
# (managed Tandem URL, SearxNG, Brave, Exa) before falling back.
|
|
42
|
+
TANDEM_SEARCH_BACKEND=auto
|
|
43
|
+
TANDEM_SEARCH_URL=https://search.tandem.frumu.ai
|
|
44
|
+
TANDEM_SEARCH_TIMEOUT_MS=10000
|
|
45
|
+
# Optional direct-provider overrides
|
|
46
|
+
# TANDEM_BRAVE_SEARCH_API_KEY=
|
|
47
|
+
# TANDEM_EXA_API_KEY=
|
|
48
|
+
# TANDEM_SEARXNG_URL=http://127.0.0.1:8080
|
|
49
|
+
|
|
39
50
|
# Bug Monitor (disabled by default)
|
|
40
51
|
# TANDEM_BUG_MONITOR_ENABLED=0
|
|
41
52
|
# TANDEM_BUG_MONITOR_REPO=owner/repo
|
package/README.md
CHANGED
|
@@ -8,6 +8,16 @@ Full web control center for Tandem Engine (non-desktop entry point).
|
|
|
8
8
|
npm i -g @frumu/tandem-panel
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
## Editable App Scaffold
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm create tandem-panel@latest my-panel
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Use the global install when you want the official ready-to-run panel.
|
|
18
|
+
|
|
19
|
+
Use the scaffold when you want the actual app source in your own folder so you can edit routes, pages, themes, styles, and runtime behavior without customizing files inside `node_modules`.
|
|
20
|
+
|
|
11
21
|
## Official Bootstrap
|
|
12
22
|
|
|
13
23
|
```bash
|
|
@@ -85,6 +95,15 @@ Variables:
|
|
|
85
95
|
- `TANDEM_CONTROL_PANEL_ENGINE_TOKEN` (token injected when panel auto-starts engine)
|
|
86
96
|
- `TANDEM_API_TOKEN` (backward-compatible alias for engine token)
|
|
87
97
|
- `TANDEM_CONTROL_PANEL_SESSION_TTL_MINUTES` (default `1440`)
|
|
98
|
+
- `TANDEM_SEARCH_BACKEND` (`auto`, `tandem`, `brave`, `exa`, `searxng`, or `none`; default official installs use `auto`)
|
|
99
|
+
- `TANDEM_SEARCH_URL` (hosted Tandem search endpoint or compatible router URL)
|
|
100
|
+
- `TANDEM_SEARCH_TIMEOUT_MS` (default `10000`)
|
|
101
|
+
- `TANDEM_BRAVE_SEARCH_API_KEY` (optional direct Brave override when `TANDEM_SEARCH_BACKEND=brave`)
|
|
102
|
+
- `TANDEM_EXA_API_KEY` (optional direct Exa override when `TANDEM_SEARCH_BACKEND=exa`)
|
|
103
|
+
- `TANDEM_SEARXNG_URL` (optional self-hosted override when `TANDEM_SEARCH_BACKEND=searxng`)
|
|
104
|
+
|
|
105
|
+
The desktop app now exposes these search settings directly in Settings, and the control panel exposes them under Settings -> Web Search when it is connected to a local engine host.
|
|
106
|
+
|
|
88
107
|
- `TANDEM_DISABLE_TOOL_GUARD_BUDGETS` (`1` disables per-run guard budgets; default in installer/service env is `1`)
|
|
89
108
|
- `TANDEM_PROMPT_CONTEXT_HOOK_TIMEOUT_MS` (default `5000`)
|
|
90
109
|
- `TANDEM_PROVIDER_STREAM_CONNECT_TIMEOUT_MS` (default `30000`)
|
|
@@ -144,17 +163,32 @@ npm run build
|
|
|
144
163
|
|
|
145
164
|
### Repo Source Workflow (No Global npm Install)
|
|
146
165
|
|
|
147
|
-
If you run from
|
|
166
|
+
If you run from the repo root, use:
|
|
148
167
|
|
|
149
168
|
```bash
|
|
150
169
|
node packages/tandem-control-panel/bin/cli.js init --no-service
|
|
151
170
|
node packages/tandem-control-panel/bin/cli.js run
|
|
152
171
|
```
|
|
153
172
|
|
|
154
|
-
|
|
173
|
+
If you are already inside `packages/tandem-control-panel`, use:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
node bin/cli.js init --no-service
|
|
177
|
+
node bin/cli.js run
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Service install/ops from source from the repo root:
|
|
155
181
|
|
|
156
182
|
```bash
|
|
157
183
|
sudo node packages/tandem-control-panel/bin/cli.js service install
|
|
158
184
|
node packages/tandem-control-panel/bin/cli.js service status
|
|
159
185
|
sudo node packages/tandem-control-panel/bin/cli.js service restart
|
|
160
186
|
```
|
|
187
|
+
|
|
188
|
+
Service install/ops from inside `packages/tandem-control-panel`:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
sudo node bin/cli.js service install
|
|
192
|
+
node bin/cli.js service status
|
|
193
|
+
sudo node bin/cli.js service restart
|
|
194
|
+
```
|
package/bin/setup.js
CHANGED
|
@@ -34,6 +34,10 @@ function parseDotEnv(content) {
|
|
|
34
34
|
return out;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
function serializeEnv(entries) {
|
|
38
|
+
return `${entries.map(([key, value]) => `${key}=${value}`).join("\n")}\n`;
|
|
39
|
+
}
|
|
40
|
+
|
|
37
41
|
function loadDotEnvFile(pathname) {
|
|
38
42
|
if (!existsSync(pathname)) return false;
|
|
39
43
|
const parsed = parseDotEnv(readFileSync(pathname, "utf8"));
|
|
@@ -173,6 +177,9 @@ const ENGINE_PORT = Number.parseInt(process.env.TANDEM_ENGINE_PORT || "39731", 1
|
|
|
173
177
|
const ENGINE_URL = (
|
|
174
178
|
process.env.TANDEM_ENGINE_URL || `http://${ENGINE_HOST}:${ENGINE_PORT}`
|
|
175
179
|
).replace(/\/+$/, "");
|
|
180
|
+
const DEFAULT_TANDEM_SEARCH_URL = (
|
|
181
|
+
process.env.TANDEM_SEARCH_URL || "https://search.tandem.frumu.ai"
|
|
182
|
+
).replace(/\/+$/, "");
|
|
176
183
|
const SWARM_RUNS_PATH = resolve(homedir(), ".tandem", "control-panel", "swarm-runs.json");
|
|
177
184
|
const SWARM_HIDDEN_RUNS_PATH = resolve(
|
|
178
185
|
homedir(),
|
|
@@ -745,10 +752,47 @@ async function installServices() {
|
|
|
745
752
|
? parseDotEnv(readFileSync(engineEnvPath, "utf8"))
|
|
746
753
|
: {};
|
|
747
754
|
const { TANDEM_MEMORY_DB_PATH: _legacyMemoryDbPath, ...engineEnvBase } = existingEngineEnv;
|
|
755
|
+
const searchEnv =
|
|
756
|
+
existingEngineEnv.TANDEM_SEARCH_BACKEND ||
|
|
757
|
+
existingEngineEnv.TANDEM_SEARCH_URL ||
|
|
758
|
+
existingEngineEnv.TANDEM_SEARCH_TIMEOUT_MS ||
|
|
759
|
+
existingEngineEnv.TANDEM_BRAVE_SEARCH_API_KEY ||
|
|
760
|
+
existingEngineEnv.BRAVE_SEARCH_API_KEY ||
|
|
761
|
+
existingEngineEnv.TANDEM_EXA_API_KEY ||
|
|
762
|
+
existingEngineEnv.EXA_API_KEY ||
|
|
763
|
+
existingEngineEnv.TANDEM_SEARXNG_URL
|
|
764
|
+
? {
|
|
765
|
+
...(existingEngineEnv.TANDEM_SEARCH_BACKEND
|
|
766
|
+
? { TANDEM_SEARCH_BACKEND: existingEngineEnv.TANDEM_SEARCH_BACKEND }
|
|
767
|
+
: {}),
|
|
768
|
+
...(existingEngineEnv.TANDEM_SEARCH_URL
|
|
769
|
+
? { TANDEM_SEARCH_URL: existingEngineEnv.TANDEM_SEARCH_URL }
|
|
770
|
+
: {}),
|
|
771
|
+
...(existingEngineEnv.TANDEM_SEARCH_TIMEOUT_MS
|
|
772
|
+
? { TANDEM_SEARCH_TIMEOUT_MS: existingEngineEnv.TANDEM_SEARCH_TIMEOUT_MS }
|
|
773
|
+
: {}),
|
|
774
|
+
...(existingEngineEnv.TANDEM_BRAVE_SEARCH_API_KEY
|
|
775
|
+
? { TANDEM_BRAVE_SEARCH_API_KEY: existingEngineEnv.TANDEM_BRAVE_SEARCH_API_KEY }
|
|
776
|
+
: {}),
|
|
777
|
+
...(existingEngineEnv.BRAVE_SEARCH_API_KEY
|
|
778
|
+
? { BRAVE_SEARCH_API_KEY: existingEngineEnv.BRAVE_SEARCH_API_KEY }
|
|
779
|
+
: {}),
|
|
780
|
+
...(existingEngineEnv.TANDEM_EXA_API_KEY
|
|
781
|
+
? { TANDEM_EXA_API_KEY: existingEngineEnv.TANDEM_EXA_API_KEY }
|
|
782
|
+
: {}),
|
|
783
|
+
...(existingEngineEnv.EXA_API_KEY
|
|
784
|
+
? { EXA_API_KEY: existingEngineEnv.EXA_API_KEY }
|
|
785
|
+
: {}),
|
|
786
|
+
...(existingEngineEnv.TANDEM_SEARXNG_URL
|
|
787
|
+
? { TANDEM_SEARXNG_URL: existingEngineEnv.TANDEM_SEARXNG_URL }
|
|
788
|
+
: {}),
|
|
789
|
+
}
|
|
790
|
+
: {};
|
|
748
791
|
const engineEnv = {
|
|
749
792
|
...engineEnvBase,
|
|
750
793
|
TANDEM_API_TOKEN: token,
|
|
751
794
|
TANDEM_STATE_DIR: stateDir,
|
|
795
|
+
...searchEnv,
|
|
752
796
|
TANDEM_ENABLE_GLOBAL_MEMORY: existingEngineEnv.TANDEM_ENABLE_GLOBAL_MEMORY || "1",
|
|
753
797
|
TANDEM_DISABLE_TOOL_GUARD_BUDGETS: existingEngineEnv.TANDEM_DISABLE_TOOL_GUARD_BUDGETS || "1",
|
|
754
798
|
TANDEM_TOOL_ROUTER_ENABLED: existingEngineEnv.TANDEM_TOOL_ROUTER_ENABLED || "0",
|
|
@@ -967,6 +1011,127 @@ async function readJsonBody(req) {
|
|
|
967
1011
|
return JSON.parse(raw);
|
|
968
1012
|
}
|
|
969
1013
|
|
|
1014
|
+
function normalizeSearchBackend(raw) {
|
|
1015
|
+
switch (String(raw || "").trim().toLowerCase()) {
|
|
1016
|
+
case "":
|
|
1017
|
+
case "auto":
|
|
1018
|
+
return "auto";
|
|
1019
|
+
case "tandem":
|
|
1020
|
+
case "brave":
|
|
1021
|
+
case "exa":
|
|
1022
|
+
case "searxng":
|
|
1023
|
+
return String(raw).trim().toLowerCase();
|
|
1024
|
+
case "none":
|
|
1025
|
+
case "disabled":
|
|
1026
|
+
return "none";
|
|
1027
|
+
default:
|
|
1028
|
+
return "auto";
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function normalizeSearchUrl(raw) {
|
|
1033
|
+
const value = String(raw || "").trim().replace(/\/+$/, "");
|
|
1034
|
+
return value || "";
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function getManagedEngineEnvPath() {
|
|
1038
|
+
return "/etc/tandem/engine.env";
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function readManagedSearchSettings() {
|
|
1042
|
+
const envPath = getManagedEngineEnvPath();
|
|
1043
|
+
const localEngine = isLocalEngineUrl(ENGINE_URL);
|
|
1044
|
+
const env = existsSync(envPath) ? parseDotEnv(readFileSync(envPath, "utf8")) : {};
|
|
1045
|
+
const timeoutRaw = Number.parseInt(String(env.TANDEM_SEARCH_TIMEOUT_MS || "10000"), 10);
|
|
1046
|
+
const timeoutMs = Number.isFinite(timeoutRaw)
|
|
1047
|
+
? Math.min(Math.max(timeoutRaw, 1000), 120000)
|
|
1048
|
+
: 10000;
|
|
1049
|
+
return {
|
|
1050
|
+
available: localEngine,
|
|
1051
|
+
local_engine: localEngine,
|
|
1052
|
+
writable: localEngine,
|
|
1053
|
+
managed_env_path: envPath,
|
|
1054
|
+
restart_required: false,
|
|
1055
|
+
restart_hint: "Restart tandem-engine after saving search settings.",
|
|
1056
|
+
settings: {
|
|
1057
|
+
backend: normalizeSearchBackend(env.TANDEM_SEARCH_BACKEND || "auto"),
|
|
1058
|
+
tandem_url: normalizeSearchUrl(env.TANDEM_SEARCH_URL || ""),
|
|
1059
|
+
searxng_url: normalizeSearchUrl(env.TANDEM_SEARXNG_URL || ""),
|
|
1060
|
+
timeout_ms: timeoutMs,
|
|
1061
|
+
has_brave_key: !!String(
|
|
1062
|
+
env.TANDEM_BRAVE_SEARCH_API_KEY || env.BRAVE_SEARCH_API_KEY || ""
|
|
1063
|
+
).trim(),
|
|
1064
|
+
has_exa_key: !!String(env.TANDEM_EXA_API_KEY || env.EXA_API_KEY || "").trim(),
|
|
1065
|
+
},
|
|
1066
|
+
reason: localEngine
|
|
1067
|
+
? ""
|
|
1068
|
+
: "Search settings can only be edited here when the control panel points at a local engine host.",
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
async function writeManagedSearchSettings(payload = {}) {
|
|
1073
|
+
const current = readManagedSearchSettings();
|
|
1074
|
+
if (!current.local_engine) {
|
|
1075
|
+
const error = new Error(current.reason || "Search settings are not editable for this engine.");
|
|
1076
|
+
error.statusCode = 400;
|
|
1077
|
+
throw error;
|
|
1078
|
+
}
|
|
1079
|
+
const envPath = current.managed_env_path;
|
|
1080
|
+
const existingEnv = existsSync(envPath) ? parseDotEnv(readFileSync(envPath, "utf8")) : {};
|
|
1081
|
+
const nextEnv = { ...existingEnv };
|
|
1082
|
+
|
|
1083
|
+
nextEnv.TANDEM_SEARCH_BACKEND = normalizeSearchBackend(payload.backend || "auto");
|
|
1084
|
+
const timeoutRaw = Number.parseInt(String(payload.timeout_ms || payload.timeoutMs || "10000"), 10);
|
|
1085
|
+
nextEnv.TANDEM_SEARCH_TIMEOUT_MS = String(
|
|
1086
|
+
Number.isFinite(timeoutRaw) ? Math.min(Math.max(timeoutRaw, 1000), 120000) : 10000
|
|
1087
|
+
);
|
|
1088
|
+
|
|
1089
|
+
const tandemUrl = normalizeSearchUrl(payload.tandem_url || payload.tandemUrl || "");
|
|
1090
|
+
if (tandemUrl) nextEnv.TANDEM_SEARCH_URL = tandemUrl;
|
|
1091
|
+
else delete nextEnv.TANDEM_SEARCH_URL;
|
|
1092
|
+
|
|
1093
|
+
const searxngUrl = normalizeSearchUrl(payload.searxng_url || payload.searxngUrl || "");
|
|
1094
|
+
if (searxngUrl) nextEnv.TANDEM_SEARXNG_URL = searxngUrl;
|
|
1095
|
+
else delete nextEnv.TANDEM_SEARXNG_URL;
|
|
1096
|
+
|
|
1097
|
+
const braveKey = String(payload.brave_api_key || payload.braveApiKey || "").trim();
|
|
1098
|
+
if (braveKey) nextEnv.TANDEM_BRAVE_SEARCH_API_KEY = braveKey;
|
|
1099
|
+
else if (payload.clear_brave_key || payload.clearBraveKey) {
|
|
1100
|
+
delete nextEnv.TANDEM_BRAVE_SEARCH_API_KEY;
|
|
1101
|
+
delete nextEnv.BRAVE_SEARCH_API_KEY;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
const exaKey = String(payload.exa_api_key || payload.exaApiKey || "").trim();
|
|
1105
|
+
if (exaKey) nextEnv.TANDEM_EXA_API_KEY = exaKey;
|
|
1106
|
+
else if (payload.clear_exa_key || payload.clearExaKey) {
|
|
1107
|
+
delete nextEnv.TANDEM_EXA_API_KEY;
|
|
1108
|
+
delete nextEnv.EXA_API_KEY;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
const preferredKeys = [
|
|
1112
|
+
"TANDEM_API_TOKEN",
|
|
1113
|
+
"TANDEM_STATE_DIR",
|
|
1114
|
+
"TANDEM_SEARCH_BACKEND",
|
|
1115
|
+
"TANDEM_SEARCH_URL",
|
|
1116
|
+
"TANDEM_SEARXNG_URL",
|
|
1117
|
+
"TANDEM_SEARCH_TIMEOUT_MS",
|
|
1118
|
+
"TANDEM_BRAVE_SEARCH_API_KEY",
|
|
1119
|
+
"TANDEM_EXA_API_KEY",
|
|
1120
|
+
];
|
|
1121
|
+
const ordered = [];
|
|
1122
|
+
for (const key of preferredKeys) {
|
|
1123
|
+
if (nextEnv[key] !== undefined) ordered.push([key, nextEnv[key]]);
|
|
1124
|
+
}
|
|
1125
|
+
for (const [key, value] of Object.entries(nextEnv)) {
|
|
1126
|
+
if (!preferredKeys.includes(key)) ordered.push([key, value]);
|
|
1127
|
+
}
|
|
1128
|
+
await writeFile(envPath, serializeEnv(ordered), "utf8");
|
|
1129
|
+
return {
|
|
1130
|
+
...readManagedSearchSettings(),
|
|
1131
|
+
restart_required: true,
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
|
|
970
1135
|
function sendJson(res, code, payload) {
|
|
971
1136
|
if (res.headersSent || res.writableEnded || res.destroyed) return;
|
|
972
1137
|
const body = JSON.stringify(payload);
|
|
@@ -4369,6 +4534,29 @@ async function handleApi(req, res) {
|
|
|
4369
4534
|
return true;
|
|
4370
4535
|
}
|
|
4371
4536
|
|
|
4537
|
+
if (pathname === "/api/system/search-settings" && req.method === "GET") {
|
|
4538
|
+
const session = requireSession(req, res);
|
|
4539
|
+
if (!session) return true;
|
|
4540
|
+
sendJson(res, 200, readManagedSearchSettings());
|
|
4541
|
+
return true;
|
|
4542
|
+
}
|
|
4543
|
+
|
|
4544
|
+
if (pathname === "/api/system/search-settings" && req.method === "PATCH") {
|
|
4545
|
+
const session = requireSession(req, res);
|
|
4546
|
+
if (!session) return true;
|
|
4547
|
+
try {
|
|
4548
|
+
const payload = await readJsonBody(req);
|
|
4549
|
+
const saved = await writeManagedSearchSettings(payload);
|
|
4550
|
+
sendJson(res, 200, saved);
|
|
4551
|
+
} catch (error) {
|
|
4552
|
+
sendJson(res, Number(error?.statusCode || 500), {
|
|
4553
|
+
ok: false,
|
|
4554
|
+
error: error instanceof Error ? error.message : String(error),
|
|
4555
|
+
});
|
|
4556
|
+
}
|
|
4557
|
+
return true;
|
|
4558
|
+
}
|
|
4559
|
+
|
|
4372
4560
|
if (pathname === "/api/auth/login" && req.method === "POST") {
|
|
4373
4561
|
await handleAuthLogin(req, res);
|
|
4374
4562
|
return true;
|