@frumu/tandem-panel 0.4.16 → 0.4.18

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 CHANGED
@@ -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=30000
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.frumu.ai
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=
package/README.md CHANGED
@@ -6,6 +6,15 @@ This package remains the control-panel add-on during migration. The canonical
6
6
  master CLI now lives in `@frumu/tandem` as `tandem`, and this package keeps
7
7
  `tandem-setup` / `tandem-control-panel` as compatibility shims.
8
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
+
9
18
  ## Install
10
19
 
11
20
  ```bash
@@ -57,6 +66,7 @@ Useful follow-up commands:
57
66
  ```bash
58
67
  tandem doctor
59
68
  tandem status
69
+ tandem service install
60
70
  tandem service status
61
71
  tandem service restart
62
72
  tandem panel doctor
package/bin/setup.js CHANGED
@@ -192,7 +192,7 @@ const ACA_BASE_URL = String(process.env.ACA_BASE_URL || "")
192
192
  const CONTROL_PANEL_CONFIG_FILE = String(process.env.TANDEM_CONTROL_PANEL_CONFIG_FILE || "").trim();
193
193
  const CONTROL_PANEL_MODE = String(process.env.TANDEM_CONTROL_PANEL_MODE || "auto").trim();
194
194
  const DEFAULT_TANDEM_SEARCH_URL = (
195
- process.env.TANDEM_SEARCH_URL || "https://search.tandem.frumu.ai"
195
+ process.env.TANDEM_SEARCH_URL || "https://search.tandem.ac"
196
196
  ).replace(/\/+$/, "");
197
197
  const SWARM_RUNS_PATH = resolve(homedir(), ".tandem", "control-panel", "swarm-runs.json");
198
198
  const SWARM_HIDDEN_RUNS_PATH = resolve(
@@ -822,7 +822,7 @@ async function installServices() {
822
822
  TANDEM_PROMPT_CONTEXT_HOOK_TIMEOUT_MS:
823
823
  existingEngineEnv.TANDEM_PROMPT_CONTEXT_HOOK_TIMEOUT_MS || "5000",
824
824
  TANDEM_PROVIDER_STREAM_CONNECT_TIMEOUT_MS:
825
- existingEngineEnv.TANDEM_PROVIDER_STREAM_CONNECT_TIMEOUT_MS || "30000",
825
+ existingEngineEnv.TANDEM_PROVIDER_STREAM_CONNECT_TIMEOUT_MS || "90000",
826
826
  TANDEM_PROVIDER_STREAM_IDLE_TIMEOUT_MS:
827
827
  existingEngineEnv.TANDEM_PROVIDER_STREAM_IDLE_TIMEOUT_MS || "90000",
828
828
  TANDEM_PERMISSION_WAIT_TIMEOUT_MS:
@@ -1161,6 +1161,70 @@ async function writeManagedSearchSettings(payload = {}) {
1161
1161
  };
1162
1162
  }
1163
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
+
1164
1228
  function sendJson(res, code, payload) {
1165
1229
  if (res.headersSent || res.writableEnded || res.destroyed) return;
1166
1230
  const body = JSON.stringify(payload);
@@ -1395,7 +1459,7 @@ async function ensureEngineRunning() {
1395
1459
  TANDEM_PROMPT_CONTEXT_HOOK_TIMEOUT_MS:
1396
1460
  process.env.TANDEM_PROMPT_CONTEXT_HOOK_TIMEOUT_MS || "5000",
1397
1461
  TANDEM_PROVIDER_STREAM_CONNECT_TIMEOUT_MS:
1398
- process.env.TANDEM_PROVIDER_STREAM_CONNECT_TIMEOUT_MS || "30000",
1462
+ process.env.TANDEM_PROVIDER_STREAM_CONNECT_TIMEOUT_MS || "90000",
1399
1463
  TANDEM_PROVIDER_STREAM_IDLE_TIMEOUT_MS:
1400
1464
  process.env.TANDEM_PROVIDER_STREAM_IDLE_TIMEOUT_MS || "90000",
1401
1465
  TANDEM_PERMISSION_WAIT_TIMEOUT_MS: process.env.TANDEM_PERMISSION_WAIT_TIMEOUT_MS || "15000",
@@ -4796,6 +4860,29 @@ async function handleApi(req, res) {
4796
4860
  return true;
4797
4861
  }
4798
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
+
4799
4886
  if (pathname === "/api/auth/login" && req.method === "POST") {
4800
4887
  await handleAuthLogin(req, res);
4801
4888
  return true;