@frumu/tandem-panel 0.4.15 → 0.4.17
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 +10 -3
- package/README.md +27 -8
- package/bin/cli.js +3 -1
- package/bin/setup.js +323 -13
- package/dist/assets/index-BsS1Z15-.css +1 -0
- package/dist/assets/index-DtaAHVxs.js +2495 -0
- package/dist/assets/{motion-BCvrfAt1.js → motion-m8lxAefi.js} +1 -1
- package/dist/assets/preact-vendor-CWXGD9A4.js +1 -0
- package/dist/assets/{react-query-wD0mx2Xi.js → react-query-BiFBqyAt.js} +1 -1
- package/dist/assets/vendor-Q0KoFXrG.js +180 -0
- package/dist/index.html +6 -6
- package/lib/setup/control-panel-config.js +196 -0
- package/lib/setup/env.js +1 -1
- package/package.json +10 -4
- package/server/routes/aca.js +97 -0
- package/server/routes/capabilities.js +51 -12
- package/server/routes/control-panel-config.js +106 -0
- package/src/generated/agent-catalog.json +2254 -0
- package/dist/assets/index-DAtDe1Vc.js +0 -2460
- package/dist/assets/index-DzX1-UXX.css +0 -1
- package/dist/assets/preact-vendor-jo0muZ28.js +0 -1
- package/dist/assets/vendor-BB3fzNns.js +0 -180
package/.env.example
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Control panel bind port
|
|
2
2
|
TANDEM_CONTROL_PANEL_PORT=39732
|
|
3
3
|
|
|
4
|
-
# Tip: run `tandem
|
|
4
|
+
# Tip: run `tandem panel init` to auto-generate this file and token.
|
|
5
5
|
|
|
6
6
|
# Control panel bind host (loopback by default)
|
|
7
7
|
TANDEM_CONTROL_PANEL_HOST=127.0.0.1
|
|
@@ -31,7 +31,7 @@ TANDEM_CONTROL_PANEL_ENGINE_TOKEN=tk_change_me
|
|
|
31
31
|
TANDEM_DISABLE_TOOL_GUARD_BUDGETS=1
|
|
32
32
|
TANDEM_TOOL_ROUTER_ENABLED=0
|
|
33
33
|
TANDEM_PROMPT_CONTEXT_HOOK_TIMEOUT_MS=5000
|
|
34
|
-
TANDEM_PROVIDER_STREAM_CONNECT_TIMEOUT_MS=
|
|
34
|
+
TANDEM_PROVIDER_STREAM_CONNECT_TIMEOUT_MS=90000
|
|
35
35
|
TANDEM_PROVIDER_STREAM_IDLE_TIMEOUT_MS=90000
|
|
36
36
|
TANDEM_PERMISSION_WAIT_TIMEOUT_MS=15000
|
|
37
37
|
TANDEM_TOOL_EXEC_TIMEOUT_MS=45000
|
|
@@ -40,7 +40,7 @@ TANDEM_BASH_TIMEOUT_MS=30000
|
|
|
40
40
|
# Built-in websearch defaults. `auto` can try configured backends in order
|
|
41
41
|
# (managed Tandem URL, SearxNG, Brave, Exa) before falling back.
|
|
42
42
|
TANDEM_SEARCH_BACKEND=auto
|
|
43
|
-
TANDEM_SEARCH_URL=https://search.tandem.
|
|
43
|
+
TANDEM_SEARCH_URL=https://search.tandem.ac
|
|
44
44
|
TANDEM_SEARCH_TIMEOUT_MS=10000
|
|
45
45
|
# Optional direct-provider overrides
|
|
46
46
|
# TANDEM_BRAVE_SEARCH_API_KEY=
|
|
@@ -78,6 +78,13 @@ TANDEM_CONTROL_PANEL_SESSION_TTL_MINUTES=1440
|
|
|
78
78
|
# Example: ACA_BASE_URL=http://127.0.0.1:39735
|
|
79
79
|
ACA_BASE_URL=
|
|
80
80
|
|
|
81
|
+
# ACA bearer token. Required for ACA project, run, and log APIs.
|
|
82
|
+
# If both ACA_API_TOKEN and ACA_API_TOKEN_FILE are set, ACA_API_TOKEN wins.
|
|
83
|
+
ACA_API_TOKEN=
|
|
84
|
+
|
|
85
|
+
# Optional path to a file containing the ACA bearer token.
|
|
86
|
+
ACA_API_TOKEN_FILE=
|
|
87
|
+
|
|
81
88
|
# Path on ACA_BASE_URL to use for health/status probe (default: /health)
|
|
82
89
|
ACA_HEALTH_PATH=/health
|
|
83
90
|
|
package/README.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
Full web control center for Tandem Engine (non-desktop entry point).
|
|
4
4
|
|
|
5
|
+
This package remains the control-panel add-on during migration. The canonical
|
|
6
|
+
master CLI now lives in `@frumu/tandem` as `tandem`, and this package keeps
|
|
7
|
+
`tandem-setup` / `tandem-control-panel` as compatibility shims.
|
|
8
|
+
|
|
9
|
+
## Fastest Ways To Start
|
|
10
|
+
|
|
11
|
+
- Web control panel: `npm i -g @frumu/tandem && tandem install panel && tandem panel init`
|
|
12
|
+
- Raw local engine: `npm i -g @frumu/tandem && tandem-engine serve --hostname 127.0.0.1 --port 39731`
|
|
13
|
+
- Terminal UI: `npm i -g @frumu/tandem-tui && tandem-tui`
|
|
14
|
+
- SDK usage: `npm install @frumu/tandem-client` or `pip install tandem-client`
|
|
15
|
+
|
|
16
|
+
If you are not sure which path to take, start with the web control panel. It gives you the engine, panel, token setup, and service install flow in one place.
|
|
17
|
+
|
|
5
18
|
## Install
|
|
6
19
|
|
|
7
20
|
```bash
|
|
@@ -41,7 +54,9 @@ Use the scaffold when you want the actual app source in your own folder so you c
|
|
|
41
54
|
## Official Bootstrap
|
|
42
55
|
|
|
43
56
|
```bash
|
|
44
|
-
|
|
57
|
+
npm i -g @frumu/tandem
|
|
58
|
+
tandem install panel
|
|
59
|
+
tandem panel init
|
|
45
60
|
```
|
|
46
61
|
|
|
47
62
|
This creates a canonical env file, bootstraps engine state, and installs services on Linux/macOS when run with the privileges needed for service registration.
|
|
@@ -49,10 +64,13 @@ This creates a canonical env file, bootstraps engine state, and installs service
|
|
|
49
64
|
Useful follow-up commands:
|
|
50
65
|
|
|
51
66
|
```bash
|
|
52
|
-
tandem
|
|
53
|
-
tandem
|
|
54
|
-
tandem
|
|
55
|
-
tandem
|
|
67
|
+
tandem doctor
|
|
68
|
+
tandem status
|
|
69
|
+
tandem service install
|
|
70
|
+
tandem service status
|
|
71
|
+
tandem service restart
|
|
72
|
+
tandem panel doctor
|
|
73
|
+
tandem panel open
|
|
56
74
|
```
|
|
57
75
|
|
|
58
76
|
## Run Foreground
|
|
@@ -76,7 +94,8 @@ tandem-setup service restart
|
|
|
76
94
|
tandem-setup service logs
|
|
77
95
|
```
|
|
78
96
|
|
|
79
|
-
Legacy flag mode is still supported for compatibility
|
|
97
|
+
Legacy flag mode is still supported for compatibility, but new installs should
|
|
98
|
+
prefer `tandem` and the panel add-on commands:
|
|
80
99
|
|
|
81
100
|
`tandem-control-panel --init`, `--install-services`, and `--service-op=...`
|
|
82
101
|
|
|
@@ -168,8 +187,8 @@ Notes:
|
|
|
168
187
|
|
|
169
188
|
## Setup Flow
|
|
170
189
|
|
|
171
|
-
1. Run `tandem-setup init`.
|
|
172
|
-
2. Verify with `tandem-setup doctor`.
|
|
190
|
+
1. Run `tandem panel init` or `tandem-setup init`.
|
|
191
|
+
2. Verify with `tandem panel doctor` or `tandem-setup doctor`.
|
|
173
192
|
3. If running foreground, start `tandem-control-panel`.
|
|
174
193
|
4. Sign in with the printed `TANDEM_CONTROL_PANEL_ENGINE_TOKEN`.
|
|
175
194
|
|
package/bin/cli.js
CHANGED
|
@@ -57,7 +57,9 @@ async function main() {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
if (first.startsWith("--")) {
|
|
60
|
-
console.warn(
|
|
60
|
+
console.warn(
|
|
61
|
+
"[Tandem Setup] Legacy flag mode is deprecated. Use `tandem panel init|service|doctor`."
|
|
62
|
+
);
|
|
61
63
|
process.exit(await runLegacy(argv));
|
|
62
64
|
}
|
|
63
65
|
|
package/bin/setup.js
CHANGED
|
@@ -12,8 +12,16 @@ import { fileURLToPath } from "url";
|
|
|
12
12
|
import { createRequire } from "module";
|
|
13
13
|
import { homedir } from "os";
|
|
14
14
|
import { ensureBootstrapEnv, resolveEnvLoadOrder } from "../lib/setup/env.js";
|
|
15
|
+
import {
|
|
16
|
+
readControlPanelConfig,
|
|
17
|
+
resolveControlPanelConfigPath,
|
|
18
|
+
resolveControlPanelMode,
|
|
19
|
+
summarizeControlPanelConfig,
|
|
20
|
+
} from "../lib/setup/control-panel-config.js";
|
|
15
21
|
import { createSwarmApiHandler, getOrchestratorMetrics } from "../server/routes/swarm.js";
|
|
22
|
+
import { createAcaApiHandler } from "../server/routes/aca.js";
|
|
16
23
|
import { createCapabilitiesHandler, getCapabilitiesMetrics } from "../server/routes/capabilities.js";
|
|
24
|
+
import { createControlPanelConfigHandler } from "../server/routes/control-panel-config.js";
|
|
17
25
|
|
|
18
26
|
function parseDotEnv(content) {
|
|
19
27
|
const out = {};
|
|
@@ -178,8 +186,13 @@ const ENGINE_PORT = Number.parseInt(process.env.TANDEM_ENGINE_PORT || "39731", 1
|
|
|
178
186
|
const ENGINE_URL = (
|
|
179
187
|
process.env.TANDEM_ENGINE_URL || `http://${ENGINE_HOST}:${ENGINE_PORT}`
|
|
180
188
|
).replace(/\/+$/, "");
|
|
189
|
+
const ACA_BASE_URL = String(process.env.ACA_BASE_URL || "")
|
|
190
|
+
.trim()
|
|
191
|
+
.replace(/\/+$/, "");
|
|
192
|
+
const CONTROL_PANEL_CONFIG_FILE = String(process.env.TANDEM_CONTROL_PANEL_CONFIG_FILE || "").trim();
|
|
193
|
+
const CONTROL_PANEL_MODE = String(process.env.TANDEM_CONTROL_PANEL_MODE || "auto").trim();
|
|
181
194
|
const DEFAULT_TANDEM_SEARCH_URL = (
|
|
182
|
-
process.env.TANDEM_SEARCH_URL || "https://search.tandem.
|
|
195
|
+
process.env.TANDEM_SEARCH_URL || "https://search.tandem.ac"
|
|
183
196
|
).replace(/\/+$/, "");
|
|
184
197
|
const SWARM_RUNS_PATH = resolve(homedir(), ".tandem", "control-panel", "swarm-runs.json");
|
|
185
198
|
const SWARM_HIDDEN_RUNS_PATH = resolve(
|
|
@@ -189,11 +202,20 @@ const SWARM_HIDDEN_RUNS_PATH = resolve(
|
|
|
189
202
|
"swarm-hidden-runs.json"
|
|
190
203
|
);
|
|
191
204
|
const AUTO_START_ENGINE = (process.env.TANDEM_CONTROL_PANEL_AUTO_START_ENGINE || "1") !== "0";
|
|
192
|
-
const CONFIGURED_ENGINE_TOKEN = (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
205
|
+
const CONFIGURED_ENGINE_TOKEN = (() => {
|
|
206
|
+
const explicit = String(
|
|
207
|
+
process.env.TANDEM_CONTROL_PANEL_ENGINE_TOKEN || process.env.TANDEM_API_TOKEN || ""
|
|
208
|
+
).trim();
|
|
209
|
+
if (explicit) return explicit;
|
|
210
|
+
const tokenFile = String(process.env.TANDEM_API_TOKEN_FILE || "").trim();
|
|
211
|
+
if (tokenFile) {
|
|
212
|
+
try {
|
|
213
|
+
return readFileSync(resolve(tokenFile), "utf8").trim();
|
|
214
|
+
} catch {}
|
|
215
|
+
}
|
|
216
|
+
return "";
|
|
217
|
+
})();
|
|
218
|
+
const ACA_TOKEN_FILE = String(process.env.ACA_API_TOKEN_FILE || "").trim();
|
|
197
219
|
const SESSION_TTL_MS =
|
|
198
220
|
Number.parseInt(process.env.TANDEM_CONTROL_PANEL_SESSION_TTL_MINUTES || "1440", 10) * 60 * 1000;
|
|
199
221
|
const FILES_ROOT = resolve(
|
|
@@ -800,7 +822,7 @@ async function installServices() {
|
|
|
800
822
|
TANDEM_PROMPT_CONTEXT_HOOK_TIMEOUT_MS:
|
|
801
823
|
existingEngineEnv.TANDEM_PROMPT_CONTEXT_HOOK_TIMEOUT_MS || "5000",
|
|
802
824
|
TANDEM_PROVIDER_STREAM_CONNECT_TIMEOUT_MS:
|
|
803
|
-
existingEngineEnv.TANDEM_PROVIDER_STREAM_CONNECT_TIMEOUT_MS || "
|
|
825
|
+
existingEngineEnv.TANDEM_PROVIDER_STREAM_CONNECT_TIMEOUT_MS || "90000",
|
|
804
826
|
TANDEM_PROVIDER_STREAM_IDLE_TIMEOUT_MS:
|
|
805
827
|
existingEngineEnv.TANDEM_PROVIDER_STREAM_IDLE_TIMEOUT_MS || "90000",
|
|
806
828
|
TANDEM_PERMISSION_WAIT_TIMEOUT_MS:
|
|
@@ -1062,7 +1084,9 @@ function readManagedSearchSettings() {
|
|
|
1062
1084
|
has_brave_key: !!String(
|
|
1063
1085
|
env.TANDEM_BRAVE_SEARCH_API_KEY || env.BRAVE_SEARCH_API_KEY || ""
|
|
1064
1086
|
).trim(),
|
|
1065
|
-
has_exa_key: !!String(
|
|
1087
|
+
has_exa_key: !!String(
|
|
1088
|
+
env.TANDEM_EXA_API_KEY || env.TANDEM_EXA_SEARCH_API_KEY || env.EXA_API_KEY || ""
|
|
1089
|
+
).trim(),
|
|
1066
1090
|
},
|
|
1067
1091
|
reason: localEngine
|
|
1068
1092
|
? ""
|
|
@@ -1103,9 +1127,13 @@ async function writeManagedSearchSettings(payload = {}) {
|
|
|
1103
1127
|
}
|
|
1104
1128
|
|
|
1105
1129
|
const exaKey = String(payload.exa_api_key || payload.exaApiKey || "").trim();
|
|
1106
|
-
if (exaKey)
|
|
1130
|
+
if (exaKey) {
|
|
1131
|
+
nextEnv.TANDEM_EXA_API_KEY = exaKey;
|
|
1132
|
+
delete nextEnv.TANDEM_EXA_SEARCH_API_KEY;
|
|
1133
|
+
}
|
|
1107
1134
|
else if (payload.clear_exa_key || payload.clearExaKey) {
|
|
1108
1135
|
delete nextEnv.TANDEM_EXA_API_KEY;
|
|
1136
|
+
delete nextEnv.TANDEM_EXA_SEARCH_API_KEY;
|
|
1109
1137
|
delete nextEnv.EXA_API_KEY;
|
|
1110
1138
|
}
|
|
1111
1139
|
|
|
@@ -1133,6 +1161,70 @@ async function writeManagedSearchSettings(payload = {}) {
|
|
|
1133
1161
|
};
|
|
1134
1162
|
}
|
|
1135
1163
|
|
|
1164
|
+
function getManagedSchedulerSettings() {
|
|
1165
|
+
const envPath = getManagedEngineEnvPath();
|
|
1166
|
+
const localEngine = isLocalEngineUrl(ENGINE_URL);
|
|
1167
|
+
const env = existsSync(envPath) ? parseDotEnv(readFileSync(envPath, "utf8")) : {};
|
|
1168
|
+
const modeRaw = String(env.TANDEM_SCHEDULER_MODE || "multi").trim().toLowerCase();
|
|
1169
|
+
const mode = modeRaw === "single" ? "single" : "multi";
|
|
1170
|
+
const maxRaw = Number.parseInt(String(env.TANDEM_SCHEDULER_MAX_CONCURRENT_RUNS || ""), 10);
|
|
1171
|
+
const maxConcurrentRuns = Number.isFinite(maxRaw) && maxRaw > 0 ? maxRaw : null;
|
|
1172
|
+
return {
|
|
1173
|
+
available: localEngine,
|
|
1174
|
+
local_engine: localEngine,
|
|
1175
|
+
writable: localEngine,
|
|
1176
|
+
managed_env_path: envPath,
|
|
1177
|
+
restart_required: false,
|
|
1178
|
+
restart_hint: "Restart tandem-engine after changing scheduler mode.",
|
|
1179
|
+
settings: {
|
|
1180
|
+
mode,
|
|
1181
|
+
max_concurrent_runs: maxConcurrentRuns,
|
|
1182
|
+
},
|
|
1183
|
+
reason: localEngine
|
|
1184
|
+
? ""
|
|
1185
|
+
: "Scheduler settings can only be edited here when the control panel points at a local engine host.",
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
async function writeManagedSchedulerSettings(payload = {}) {
|
|
1190
|
+
const current = getManagedSchedulerSettings();
|
|
1191
|
+
if (!current.local_engine) {
|
|
1192
|
+
const error = new Error(
|
|
1193
|
+
current.reason || "Scheduler settings are not editable for this engine."
|
|
1194
|
+
);
|
|
1195
|
+
error.statusCode = 400;
|
|
1196
|
+
throw error;
|
|
1197
|
+
}
|
|
1198
|
+
const envPath = current.managed_env_path;
|
|
1199
|
+
const existingEnv = existsSync(envPath) ? parseDotEnv(readFileSync(envPath, "utf8")) : {};
|
|
1200
|
+
const nextEnv = { ...existingEnv };
|
|
1201
|
+
const modeRaw = String(payload.mode || "multi").trim().toLowerCase();
|
|
1202
|
+
nextEnv.TANDEM_SCHEDULER_MODE = modeRaw === "single" ? "single" : "multi";
|
|
1203
|
+
if (payload.max_concurrent_runs != null && payload.max_concurrent_runs > 0) {
|
|
1204
|
+
nextEnv.TANDEM_SCHEDULER_MAX_CONCURRENT_RUNS = String(payload.max_concurrent_runs);
|
|
1205
|
+
} else {
|
|
1206
|
+
delete nextEnv.TANDEM_SCHEDULER_MAX_CONCURRENT_RUNS;
|
|
1207
|
+
}
|
|
1208
|
+
const preferredKeys = [
|
|
1209
|
+
"TANDEM_API_TOKEN",
|
|
1210
|
+
"TANDEM_STATE_DIR",
|
|
1211
|
+
"TANDEM_SCHEDULER_MODE",
|
|
1212
|
+
"TANDEM_SCHEDULER_MAX_CONCURRENT_RUNS",
|
|
1213
|
+
];
|
|
1214
|
+
const ordered = [];
|
|
1215
|
+
for (const key of preferredKeys) {
|
|
1216
|
+
if (nextEnv[key] !== undefined) ordered.push([key, nextEnv[key]]);
|
|
1217
|
+
}
|
|
1218
|
+
for (const [key, value] of Object.entries(nextEnv)) {
|
|
1219
|
+
if (!preferredKeys.includes(key)) ordered.push([key, value]);
|
|
1220
|
+
}
|
|
1221
|
+
await writeFile(envPath, serializeEnv(ordered), "utf8");
|
|
1222
|
+
return {
|
|
1223
|
+
...getManagedSchedulerSettings(),
|
|
1224
|
+
restart_required: true,
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1136
1228
|
function sendJson(res, code, payload) {
|
|
1137
1229
|
if (res.headersSent || res.writableEnded || res.destroyed) return;
|
|
1138
1230
|
const body = JSON.stringify(payload);
|
|
@@ -1143,6 +1235,54 @@ function sendJson(res, code, payload) {
|
|
|
1143
1235
|
res.end(body);
|
|
1144
1236
|
}
|
|
1145
1237
|
|
|
1238
|
+
function readOptionalTokenFile(pathname) {
|
|
1239
|
+
const target = String(pathname || "").trim();
|
|
1240
|
+
if (!target) return "";
|
|
1241
|
+
try {
|
|
1242
|
+
return readFileSync(resolve(target), "utf8").trim();
|
|
1243
|
+
} catch {
|
|
1244
|
+
return "";
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
function getAcaToken() {
|
|
1249
|
+
return (
|
|
1250
|
+
String(process.env.ACA_API_TOKEN || "").trim() ||
|
|
1251
|
+
readOptionalTokenFile(ACA_TOKEN_FILE) ||
|
|
1252
|
+
""
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
function getControlPanelConfigPath() {
|
|
1257
|
+
return resolveControlPanelConfigPath({
|
|
1258
|
+
explicitPath: CONTROL_PANEL_CONFIG_FILE,
|
|
1259
|
+
stateDir: process.env.TANDEM_CONTROL_PANEL_STATE_DIR,
|
|
1260
|
+
env: process.env,
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
async function getInstallProfile({ acaAvailable = false, acaReason = "" } = {}) {
|
|
1265
|
+
const configPath = getControlPanelConfigPath();
|
|
1266
|
+
const config = readControlPanelConfig(configPath);
|
|
1267
|
+
const mode = resolveControlPanelMode({
|
|
1268
|
+
config,
|
|
1269
|
+
envMode: CONTROL_PANEL_MODE,
|
|
1270
|
+
acaAvailable,
|
|
1271
|
+
});
|
|
1272
|
+
const summary = summarizeControlPanelConfig(config);
|
|
1273
|
+
return {
|
|
1274
|
+
control_panel_mode: mode.mode,
|
|
1275
|
+
control_panel_mode_source: mode.source,
|
|
1276
|
+
control_panel_mode_reason: mode.reason || "",
|
|
1277
|
+
control_panel_config_path: configPath,
|
|
1278
|
+
control_panel_config_ready: summary.ready,
|
|
1279
|
+
control_panel_config_missing: summary.missing,
|
|
1280
|
+
control_panel_compact_nav: !!summary.control_panel?.aca_compact_nav,
|
|
1281
|
+
aca_integration: !!acaAvailable,
|
|
1282
|
+
aca_reason: acaReason || "",
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1146
1286
|
function pushSwarmEvent(kind, payload = {}) {
|
|
1147
1287
|
const event = {
|
|
1148
1288
|
kind,
|
|
@@ -1188,6 +1328,73 @@ async function engineHealth(token = "") {
|
|
|
1188
1328
|
}
|
|
1189
1329
|
}
|
|
1190
1330
|
|
|
1331
|
+
async function executeEngineTool(token, tool, args = {}) {
|
|
1332
|
+
const response = await fetch(`${ENGINE_URL}/tool/execute`, {
|
|
1333
|
+
method: "POST",
|
|
1334
|
+
headers: {
|
|
1335
|
+
"content-type": "application/json",
|
|
1336
|
+
authorization: `Bearer ${token}`,
|
|
1337
|
+
"x-tandem-token": token,
|
|
1338
|
+
},
|
|
1339
|
+
body: JSON.stringify({ tool, args }),
|
|
1340
|
+
signal: AbortSignal.timeout(15000),
|
|
1341
|
+
});
|
|
1342
|
+
const text = await response.text().catch(() => "");
|
|
1343
|
+
let parsed = null;
|
|
1344
|
+
try {
|
|
1345
|
+
parsed = text ? JSON.parse(text) : {};
|
|
1346
|
+
} catch {
|
|
1347
|
+
parsed = null;
|
|
1348
|
+
}
|
|
1349
|
+
if (!response.ok) {
|
|
1350
|
+
const message =
|
|
1351
|
+
parsed?.error || parsed?.detail || text || `${tool} failed (${response.status})`;
|
|
1352
|
+
const error = new Error(message);
|
|
1353
|
+
error.statusCode = response.status;
|
|
1354
|
+
error.payload = parsed;
|
|
1355
|
+
throw error;
|
|
1356
|
+
}
|
|
1357
|
+
return parsed || {};
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function buildSearchTestMarkdown(payload) {
|
|
1361
|
+
const query = String(payload?.query || "").trim();
|
|
1362
|
+
const backend = String(payload?.backend || "unknown").trim();
|
|
1363
|
+
const configuredBackend = String(payload?.configured_backend || backend || "unknown").trim();
|
|
1364
|
+
const attemptedBackends = Array.isArray(payload?.attempted_backends)
|
|
1365
|
+
? payload.attempted_backends.filter(Boolean)
|
|
1366
|
+
: [];
|
|
1367
|
+
const resultCount = Number(payload?.result_count || 0) || 0;
|
|
1368
|
+
const partial = payload?.partial === true;
|
|
1369
|
+
const results = Array.isArray(payload?.results) ? payload.results : [];
|
|
1370
|
+
|
|
1371
|
+
const lines = [
|
|
1372
|
+
"# Websearch test",
|
|
1373
|
+
"",
|
|
1374
|
+
`- Query: \`${query || "n/a"}\``,
|
|
1375
|
+
`- Backend used: \`${backend || "unknown"}\``,
|
|
1376
|
+
`- Configured backend: \`${configuredBackend || "unknown"}\``,
|
|
1377
|
+
`- Attempted backends: ${attemptedBackends.length ? attemptedBackends.map((name) => `\`${name}\``).join(", ") : "none"}`,
|
|
1378
|
+
`- Results: ${resultCount}${partial ? " (partial)" : ""}`,
|
|
1379
|
+
"",
|
|
1380
|
+
];
|
|
1381
|
+
|
|
1382
|
+
if (!results.length) {
|
|
1383
|
+
lines.push("No search results were returned.");
|
|
1384
|
+
return lines.join("\n");
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
lines.push("## Top results", "");
|
|
1388
|
+
for (const [index, row] of results.entries()) {
|
|
1389
|
+
const title = String(row?.title || row?.url || `Result ${index + 1}`).trim();
|
|
1390
|
+
const url = String(row?.url || "").trim();
|
|
1391
|
+
const snippet = String(row?.snippet || "").trim();
|
|
1392
|
+
lines.push(`${index + 1}. [${title}](${url || "#"})`);
|
|
1393
|
+
if (snippet) lines.push(` ${snippet}`);
|
|
1394
|
+
}
|
|
1395
|
+
return lines.join("\n");
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1191
1398
|
async function validateEngineToken(token) {
|
|
1192
1399
|
try {
|
|
1193
1400
|
const response = await fetch(`${ENGINE_URL}/config/providers`, {
|
|
@@ -1225,7 +1432,7 @@ async function ensureEngineRunning() {
|
|
|
1225
1432
|
engineEntrypoint = require.resolve("@frumu/tandem/bin/tandem-engine.js");
|
|
1226
1433
|
} catch (e) {
|
|
1227
1434
|
err("Could not resolve @frumu/tandem binary entrypoint.");
|
|
1228
|
-
err("Reinstall with: npm i -g @frumu/tandem
|
|
1435
|
+
err("Reinstall with: npm i -g @frumu/tandem");
|
|
1229
1436
|
throw e;
|
|
1230
1437
|
}
|
|
1231
1438
|
|
|
@@ -1252,7 +1459,7 @@ async function ensureEngineRunning() {
|
|
|
1252
1459
|
TANDEM_PROMPT_CONTEXT_HOOK_TIMEOUT_MS:
|
|
1253
1460
|
process.env.TANDEM_PROMPT_CONTEXT_HOOK_TIMEOUT_MS || "5000",
|
|
1254
1461
|
TANDEM_PROVIDER_STREAM_CONNECT_TIMEOUT_MS:
|
|
1255
|
-
process.env.TANDEM_PROVIDER_STREAM_CONNECT_TIMEOUT_MS || "
|
|
1462
|
+
process.env.TANDEM_PROVIDER_STREAM_CONNECT_TIMEOUT_MS || "90000",
|
|
1256
1463
|
TANDEM_PROVIDER_STREAM_IDLE_TIMEOUT_MS:
|
|
1257
1464
|
process.env.TANDEM_PROVIDER_STREAM_IDLE_TIMEOUT_MS || "90000",
|
|
1258
1465
|
TANDEM_PERMISSION_WAIT_TIMEOUT_MS: process.env.TANDEM_PERMISSION_WAIT_TIMEOUT_MS || "15000",
|
|
@@ -4522,8 +4729,10 @@ const handleSwarmApi = createSwarmApiHandler({
|
|
|
4522
4729
|
|
|
4523
4730
|
const handleCapabilities = createCapabilitiesHandler({
|
|
4524
4731
|
PROBE_TIMEOUT_MS: Number.parseInt(process.env.ACA_PROBE_TIMEOUT_MS || "5000", 10),
|
|
4525
|
-
ACA_BASE_URL
|
|
4526
|
-
ACA_HEALTH_PATH: "/health",
|
|
4732
|
+
ACA_BASE_URL,
|
|
4733
|
+
ACA_HEALTH_PATH: process.env.ACA_HEALTH_PATH || "/health",
|
|
4734
|
+
getAcaToken,
|
|
4735
|
+
getInstallProfile,
|
|
4527
4736
|
engineHealth: async (token) => {
|
|
4528
4737
|
const health = await engineHealth(token).catch(() => null);
|
|
4529
4738
|
return health;
|
|
@@ -4532,6 +4741,24 @@ const handleCapabilities = createCapabilitiesHandler({
|
|
|
4532
4741
|
cacheTtlMs: Number.parseInt(process.env.ACA_CAPABILITY_CACHE_TTL_MS || "45000", 10),
|
|
4533
4742
|
});
|
|
4534
4743
|
|
|
4744
|
+
const handleAcaApi = createAcaApiHandler({
|
|
4745
|
+
PORTAL_PORT,
|
|
4746
|
+
ACA_BASE_URL,
|
|
4747
|
+
getAcaToken,
|
|
4748
|
+
sendJson,
|
|
4749
|
+
});
|
|
4750
|
+
|
|
4751
|
+
const handleControlPanelConfig = createControlPanelConfigHandler({
|
|
4752
|
+
CONTROL_PANEL_CONFIG_FILE,
|
|
4753
|
+
TANDEM_CONTROL_PANEL_STATE_DIR: process.env.TANDEM_CONTROL_PANEL_STATE_DIR || "",
|
|
4754
|
+
CONTROL_PANEL_MODE,
|
|
4755
|
+
ACA_BASE_URL,
|
|
4756
|
+
PROBE_TIMEOUT_MS: Number.parseInt(process.env.ACA_PROBE_TIMEOUT_MS || "5000", 10),
|
|
4757
|
+
getAcaToken,
|
|
4758
|
+
sendJson,
|
|
4759
|
+
readJsonBody,
|
|
4760
|
+
});
|
|
4761
|
+
|
|
4535
4762
|
async function handleApi(req, res) {
|
|
4536
4763
|
const pathname = new URL(req.url, `http://127.0.0.1:${PORTAL_PORT}`).pathname;
|
|
4537
4764
|
|
|
@@ -4557,6 +4784,11 @@ async function handleApi(req, res) {
|
|
|
4557
4784
|
return true;
|
|
4558
4785
|
}
|
|
4559
4786
|
|
|
4787
|
+
if (pathname === "/api/install/profile" && req.method === "GET") {
|
|
4788
|
+
await handleCapabilities(req, res);
|
|
4789
|
+
return true;
|
|
4790
|
+
}
|
|
4791
|
+
|
|
4560
4792
|
if (pathname === "/api/system/orchestrator-metrics" && req.method === "GET") {
|
|
4561
4793
|
sendJson(res, 200, getOrchestratorMetrics());
|
|
4562
4794
|
return true;
|
|
@@ -4585,6 +4817,72 @@ async function handleApi(req, res) {
|
|
|
4585
4817
|
return true;
|
|
4586
4818
|
}
|
|
4587
4819
|
|
|
4820
|
+
if (pathname === "/api/system/search-settings/test" && req.method === "POST") {
|
|
4821
|
+
const session = requireSession(req, res);
|
|
4822
|
+
if (!session) return true;
|
|
4823
|
+
try {
|
|
4824
|
+
const payload = await readJsonBody(req);
|
|
4825
|
+
const query = String(payload?.query || "").trim();
|
|
4826
|
+
const limitRaw = Number.parseInt(String(payload?.limit || "5"), 10);
|
|
4827
|
+
const limit = Number.isFinite(limitRaw) ? Math.min(Math.max(limitRaw, 1), 10) : 5;
|
|
4828
|
+
if (!query) {
|
|
4829
|
+
sendJson(res, 400, { ok: false, error: "Search query is required." });
|
|
4830
|
+
return true;
|
|
4831
|
+
}
|
|
4832
|
+
const result = await executeEngineTool(session.token, "websearch", {
|
|
4833
|
+
query,
|
|
4834
|
+
limit,
|
|
4835
|
+
});
|
|
4836
|
+
const output = String(result?.output || "");
|
|
4837
|
+
let parsedOutput = null;
|
|
4838
|
+
try {
|
|
4839
|
+
parsedOutput = output ? JSON.parse(output) : null;
|
|
4840
|
+
} catch {
|
|
4841
|
+
parsedOutput = null;
|
|
4842
|
+
}
|
|
4843
|
+
const markdown = parsedOutput
|
|
4844
|
+
? buildSearchTestMarkdown(parsedOutput)
|
|
4845
|
+
: `# Websearch test\n\n## Output\n\n\`\`\`\n${output || "No output returned."}\n\`\`\``;
|
|
4846
|
+
sendJson(res, 200, {
|
|
4847
|
+
ok: true,
|
|
4848
|
+
query,
|
|
4849
|
+
markdown,
|
|
4850
|
+
output,
|
|
4851
|
+
parsed_output: parsedOutput,
|
|
4852
|
+
metadata: result?.metadata || {},
|
|
4853
|
+
});
|
|
4854
|
+
} catch (error) {
|
|
4855
|
+
sendJson(res, Number(error?.statusCode || 500), {
|
|
4856
|
+
ok: false,
|
|
4857
|
+
error: error instanceof Error ? error.message : String(error),
|
|
4858
|
+
});
|
|
4859
|
+
}
|
|
4860
|
+
return true;
|
|
4861
|
+
}
|
|
4862
|
+
|
|
4863
|
+
if (pathname === "/api/system/scheduler-settings" && req.method === "GET") {
|
|
4864
|
+
const session = requireSession(req, res);
|
|
4865
|
+
if (!session) return true;
|
|
4866
|
+
sendJson(res, 200, getManagedSchedulerSettings());
|
|
4867
|
+
return true;
|
|
4868
|
+
}
|
|
4869
|
+
|
|
4870
|
+
if (pathname === "/api/system/scheduler-settings" && req.method === "PATCH") {
|
|
4871
|
+
const session = requireSession(req, res);
|
|
4872
|
+
if (!session) return true;
|
|
4873
|
+
try {
|
|
4874
|
+
const payload = await readJsonBody(req);
|
|
4875
|
+
const saved = await writeManagedSchedulerSettings(payload);
|
|
4876
|
+
sendJson(res, 200, saved);
|
|
4877
|
+
} catch (error) {
|
|
4878
|
+
sendJson(res, Number(error?.statusCode || 500), {
|
|
4879
|
+
ok: false,
|
|
4880
|
+
error: error instanceof Error ? error.message : String(error),
|
|
4881
|
+
});
|
|
4882
|
+
}
|
|
4883
|
+
return true;
|
|
4884
|
+
}
|
|
4885
|
+
|
|
4588
4886
|
if (pathname === "/api/auth/login" && req.method === "POST") {
|
|
4589
4887
|
await handleAuthLogin(req, res);
|
|
4590
4888
|
return true;
|
|
@@ -4620,12 +4918,24 @@ async function handleApi(req, res) {
|
|
|
4620
4918
|
return true;
|
|
4621
4919
|
}
|
|
4622
4920
|
|
|
4921
|
+
if (pathname === "/api/control-panel/config" && (req.method === "GET" || req.method === "PATCH")) {
|
|
4922
|
+
const session = requireSession(req, res);
|
|
4923
|
+
if (!session) return true;
|
|
4924
|
+
return handleControlPanelConfig(req, res);
|
|
4925
|
+
}
|
|
4926
|
+
|
|
4623
4927
|
if (pathname.startsWith("/api/swarm") || pathname.startsWith("/api/orchestrator")) {
|
|
4624
4928
|
const session = requireSession(req, res);
|
|
4625
4929
|
if (!session) return true;
|
|
4626
4930
|
return handleSwarmApi(req, res, session);
|
|
4627
4931
|
}
|
|
4628
4932
|
|
|
4933
|
+
if (pathname.startsWith("/api/aca")) {
|
|
4934
|
+
const session = requireSession(req, res);
|
|
4935
|
+
if (!session) return true;
|
|
4936
|
+
return handleAcaApi(req, res);
|
|
4937
|
+
}
|
|
4938
|
+
|
|
4629
4939
|
if (pathname.startsWith("/api/files")) {
|
|
4630
4940
|
const session = requireSession(req, res);
|
|
4631
4941
|
if (!session) return true;
|