mobygate 0.8.0 → 0.8.1
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/CHANGELOG.md +105 -0
- package/bin/mobygate.js +74 -0
- package/index.html +1 -0
- package/inspector.html +422 -0
- package/lib/anthropic.js +23 -0
- package/lib/connectors/hermes.js +3 -1
- package/lib/connectors/openclaw.js +39 -2
- package/lib/connectors/safety.js +18 -1
- package/lib/request-capture.js +394 -0
- package/package.json +2 -1
- package/server.js +248 -6
package/server.js
CHANGED
|
@@ -76,8 +76,10 @@ import {
|
|
|
76
76
|
makeStreamTranslator,
|
|
77
77
|
hasAnthropicTools,
|
|
78
78
|
mapStopReason,
|
|
79
|
+
extractSdkUsage,
|
|
79
80
|
} from './lib/anthropic.js';
|
|
80
81
|
import { resolveSessionKey } from './lib/session-derive.js';
|
|
82
|
+
import { captureRequest, captureResponse, isCaptureEnabled, CAPTURE_DIR_PATH } from './lib/request-capture.js';
|
|
81
83
|
|
|
82
84
|
const __filename = fileURLToPath(import.meta.url);
|
|
83
85
|
const __dirname = dirname(__filename);
|
|
@@ -444,6 +446,10 @@ async function handleStreaming(req, res, body, requestId, sessionKey) {
|
|
|
444
446
|
let resolvedModel = model;
|
|
445
447
|
let capturedSessionId = existing?.sdkSessionId || null;
|
|
446
448
|
let clientDisconnected = false;
|
|
449
|
+
let inputTokens = 0;
|
|
450
|
+
let outputTokens = 0;
|
|
451
|
+
let cacheReadTokens = 0;
|
|
452
|
+
let cacheCreateTokens = 0;
|
|
447
453
|
|
|
448
454
|
res.on('close', () => {
|
|
449
455
|
clientDisconnected = true;
|
|
@@ -568,6 +574,11 @@ async function handleStreaming(req, res, body, requestId, sessionKey) {
|
|
|
568
574
|
isFirst = false;
|
|
569
575
|
}
|
|
570
576
|
if (toolsEnabled && !bufferedText && message.result) bufferedText = message.result;
|
|
577
|
+
const usage = extractSdkUsage(message);
|
|
578
|
+
inputTokens = usage.input_tokens;
|
|
579
|
+
outputTokens = usage.output_tokens;
|
|
580
|
+
cacheReadTokens = usage.cache_read_input_tokens;
|
|
581
|
+
cacheCreateTokens = usage.cache_creation_input_tokens;
|
|
571
582
|
break;
|
|
572
583
|
}
|
|
573
584
|
}
|
|
@@ -627,6 +638,13 @@ async function handleStreaming(req, res, body, requestId, sessionKey) {
|
|
|
627
638
|
}
|
|
628
639
|
res.write('data: [DONE]\n\n');
|
|
629
640
|
res.end();
|
|
641
|
+
captureResponse({
|
|
642
|
+
requestId,
|
|
643
|
+
usage: { input_tokens: inputTokens, output_tokens: outputTokens, cache_read_input_tokens: cacheReadTokens, cache_creation_input_tokens: cacheCreateTokens },
|
|
644
|
+
status: 'ok',
|
|
645
|
+
stopReason: collectedToolCalls.length > 0 ? 'tool_use' : 'end_turn',
|
|
646
|
+
model: resolvedModel,
|
|
647
|
+
});
|
|
630
648
|
return;
|
|
631
649
|
}
|
|
632
650
|
|
|
@@ -635,6 +653,14 @@ async function handleStreaming(req, res, body, requestId, sessionKey) {
|
|
|
635
653
|
res.write('data: [DONE]\n\n');
|
|
636
654
|
res.end();
|
|
637
655
|
}
|
|
656
|
+
|
|
657
|
+
captureResponse({
|
|
658
|
+
requestId,
|
|
659
|
+
usage: { input_tokens: inputTokens, output_tokens: outputTokens, cache_read_input_tokens: cacheReadTokens, cache_creation_input_tokens: cacheCreateTokens },
|
|
660
|
+
status: clientDisconnected ? 'client_disconnect' : 'ok',
|
|
661
|
+
stopReason: 'end_turn',
|
|
662
|
+
model: resolvedModel,
|
|
663
|
+
});
|
|
638
664
|
}
|
|
639
665
|
|
|
640
666
|
// ---------------------------------------------------------------------------
|
|
@@ -670,6 +696,9 @@ async function handleNonStreaming(res, body, requestId, sessionKey) {
|
|
|
670
696
|
let resolvedModel = model;
|
|
671
697
|
let inputTokens = 0;
|
|
672
698
|
let outputTokens = 0;
|
|
699
|
+
let cacheReadTokens = 0;
|
|
700
|
+
let cacheCreateTokens = 0;
|
|
701
|
+
let stopReason = 'end_turn';
|
|
673
702
|
let capturedSessionId = existing?.sdkSessionId || null;
|
|
674
703
|
const abortController = new AbortController();
|
|
675
704
|
|
|
@@ -750,8 +779,12 @@ async function handleNonStreaming(res, body, requestId, sessionKey) {
|
|
|
750
779
|
if (isAuthFailureText(resultText)) {
|
|
751
780
|
throw new AuthFailureInResultText(resultText);
|
|
752
781
|
}
|
|
753
|
-
|
|
754
|
-
|
|
782
|
+
const usage = extractSdkUsage(message);
|
|
783
|
+
inputTokens = usage.input_tokens;
|
|
784
|
+
outputTokens = usage.output_tokens;
|
|
785
|
+
cacheReadTokens = usage.cache_read_input_tokens;
|
|
786
|
+
cacheCreateTokens = usage.cache_creation_input_tokens;
|
|
787
|
+
if (message.subtype) stopReason = message.subtype;
|
|
755
788
|
break;
|
|
756
789
|
}
|
|
757
790
|
}
|
|
@@ -818,6 +851,14 @@ async function handleNonStreaming(res, body, requestId, sessionKey) {
|
|
|
818
851
|
}],
|
|
819
852
|
usage: { prompt_tokens: inputTokens, completion_tokens: outputTokens, total_tokens: inputTokens + outputTokens },
|
|
820
853
|
});
|
|
854
|
+
|
|
855
|
+
captureResponse({
|
|
856
|
+
requestId,
|
|
857
|
+
usage: { input_tokens: inputTokens, output_tokens: outputTokens, cache_read_input_tokens: cacheReadTokens, cache_creation_input_tokens: cacheCreateTokens },
|
|
858
|
+
status: 'ok',
|
|
859
|
+
stopReason,
|
|
860
|
+
model: resolvedModel,
|
|
861
|
+
});
|
|
821
862
|
}
|
|
822
863
|
|
|
823
864
|
// ---------------------------------------------------------------------------
|
|
@@ -870,6 +911,8 @@ async function handleAnthropicNonStreaming(res, body, requestId, sessionKey) {
|
|
|
870
911
|
let resolvedModel = model;
|
|
871
912
|
let inputTokens = 0;
|
|
872
913
|
let outputTokens = 0;
|
|
914
|
+
let cacheReadTokens = 0;
|
|
915
|
+
let cacheCreateTokens = 0;
|
|
873
916
|
let capturedSessionId = existing?.sdkSessionId || null;
|
|
874
917
|
let stopReason = 'end_turn';
|
|
875
918
|
const abortController = new AbortController();
|
|
@@ -950,8 +993,11 @@ async function handleAnthropicNonStreaming(res, body, requestId, sessionKey) {
|
|
|
950
993
|
if (isAuthFailureText(resultText)) {
|
|
951
994
|
throw new AuthFailureInResultText(resultText);
|
|
952
995
|
}
|
|
953
|
-
|
|
954
|
-
|
|
996
|
+
const usage = extractSdkUsage(message);
|
|
997
|
+
inputTokens = usage.input_tokens;
|
|
998
|
+
outputTokens = usage.output_tokens;
|
|
999
|
+
cacheReadTokens = usage.cache_read_input_tokens;
|
|
1000
|
+
cacheCreateTokens = usage.cache_creation_input_tokens;
|
|
955
1001
|
stopReason = mapStopReason(message);
|
|
956
1002
|
break;
|
|
957
1003
|
}
|
|
@@ -990,6 +1036,14 @@ async function handleAnthropicNonStreaming(res, body, requestId, sessionKey) {
|
|
|
990
1036
|
requestId,
|
|
991
1037
|
stopReason,
|
|
992
1038
|
}));
|
|
1039
|
+
|
|
1040
|
+
captureResponse({
|
|
1041
|
+
requestId,
|
|
1042
|
+
usage: { input_tokens: inputTokens, output_tokens: outputTokens, cache_read_input_tokens: cacheReadTokens, cache_creation_input_tokens: cacheCreateTokens },
|
|
1043
|
+
status: 'ok',
|
|
1044
|
+
stopReason,
|
|
1045
|
+
model: resolvedModel,
|
|
1046
|
+
});
|
|
993
1047
|
}
|
|
994
1048
|
|
|
995
1049
|
async function handleAnthropicStreaming(req, res, body, requestId, sessionKey) {
|
|
@@ -1033,6 +1087,8 @@ async function handleAnthropicStreaming(req, res, body, requestId, sessionKey) {
|
|
|
1033
1087
|
let capturedSessionId = existing?.sdkSessionId || null;
|
|
1034
1088
|
let inputTokens = 0;
|
|
1035
1089
|
let outputTokens = 0;
|
|
1090
|
+
let cacheReadTokens = 0;
|
|
1091
|
+
let cacheCreateTokens = 0;
|
|
1036
1092
|
let stopReason = 'end_turn';
|
|
1037
1093
|
let clientDisconnected = false;
|
|
1038
1094
|
let textEmittedSoFar = ''; // dedup against same-message reflow from SDK
|
|
@@ -1182,8 +1238,11 @@ async function handleAnthropicStreaming(req, res, body, requestId, sessionKey) {
|
|
|
1182
1238
|
if (isAuthFailureText(message.result || '') && !tx.hasStarted) {
|
|
1183
1239
|
throw new AuthFailureInResultText(message.result);
|
|
1184
1240
|
}
|
|
1185
|
-
|
|
1186
|
-
|
|
1241
|
+
const usage = extractSdkUsage(message);
|
|
1242
|
+
inputTokens = usage.input_tokens;
|
|
1243
|
+
outputTokens = usage.output_tokens;
|
|
1244
|
+
cacheReadTokens = usage.cache_read_input_tokens;
|
|
1245
|
+
cacheCreateTokens = usage.cache_creation_input_tokens;
|
|
1187
1246
|
if (!toolUseEmitted) stopReason = mapStopReason(message);
|
|
1188
1247
|
break;
|
|
1189
1248
|
}
|
|
@@ -1214,6 +1273,14 @@ async function handleAnthropicStreaming(req, res, body, requestId, sessionKey) {
|
|
|
1214
1273
|
}
|
|
1215
1274
|
|
|
1216
1275
|
tx.finish({ stopReason, usage: { output_tokens: outputTokens } });
|
|
1276
|
+
|
|
1277
|
+
captureResponse({
|
|
1278
|
+
requestId,
|
|
1279
|
+
usage: { input_tokens: inputTokens, output_tokens: outputTokens, cache_read_input_tokens: cacheReadTokens, cache_creation_input_tokens: cacheCreateTokens },
|
|
1280
|
+
status: 'ok',
|
|
1281
|
+
stopReason,
|
|
1282
|
+
model: resolvedModel,
|
|
1283
|
+
});
|
|
1217
1284
|
}
|
|
1218
1285
|
|
|
1219
1286
|
// ---------------------------------------------------------------------------
|
|
@@ -1311,6 +1378,19 @@ app.get('/', async (_req, res) => {
|
|
|
1311
1378
|
}
|
|
1312
1379
|
});
|
|
1313
1380
|
|
|
1381
|
+
// /inspector — session inspector UI for browsing captures.
|
|
1382
|
+
// Backed by /dashboard/captures and /dashboard/captures/:filename.
|
|
1383
|
+
app.get('/inspector', async (_req, res) => {
|
|
1384
|
+
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
|
1385
|
+
try {
|
|
1386
|
+
const { readFile } = await import('fs/promises');
|
|
1387
|
+
const html = await readFile(join(__dirname, 'inspector.html'), 'utf8');
|
|
1388
|
+
res.type('html').send(html);
|
|
1389
|
+
} catch (e) {
|
|
1390
|
+
res.status(404).type('text').send(`inspector.html not found at ${join(__dirname, 'inspector.html')}`);
|
|
1391
|
+
}
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1314
1394
|
// POST /v1/chat/completions
|
|
1315
1395
|
app.post('/v1/chat/completions', async (req, res) => {
|
|
1316
1396
|
const requestId = uuidv4().replace(/-/g, '').slice(0, 24);
|
|
@@ -1339,6 +1419,9 @@ app.post('/v1/chat/completions', async (req, res) => {
|
|
|
1339
1419
|
|
|
1340
1420
|
console.log(`[${new Date().toISOString()}] ${body.stream ? 'stream' : 'sync'} | model=${body.model} → ${resolveModel(body.model)} | msgs=${body.messages.length}${sessionTag}`);
|
|
1341
1421
|
|
|
1422
|
+
// Diagnostic capture — off by default, enable with MOBY_CAPTURE=1.
|
|
1423
|
+
captureRequest({ path: '/v1/chat/completions', body, requestId, sessionKey, sessionKeySource });
|
|
1424
|
+
|
|
1342
1425
|
// Dashboard: request.start
|
|
1343
1426
|
const startedAt = Date.now();
|
|
1344
1427
|
const imageBlocks = collectImages(body.messages).length;
|
|
@@ -1411,6 +1494,10 @@ app.post('/v1/messages', async (req, res) => {
|
|
|
1411
1494
|
|
|
1412
1495
|
console.log(`[${new Date().toISOString()}] anthropic ${body.stream ? 'stream' : 'sync'} | model=${body.model} → ${resolveModel(body.model)} | msgs=${body.messages.length}${sessionTag}`);
|
|
1413
1496
|
|
|
1497
|
+
// Diagnostic capture — off by default, enable with MOBY_CAPTURE=1.
|
|
1498
|
+
// Writes raw body + summary to ~/.mobygate/captures/.
|
|
1499
|
+
captureRequest({ path: '/v1/messages', body, requestId, sessionKey, sessionKeySource });
|
|
1500
|
+
|
|
1414
1501
|
// Dashboard event — same shape as the OpenAI route, just labeled by path.
|
|
1415
1502
|
const startedAt = Date.now();
|
|
1416
1503
|
const imageBlocks = collectAnthropicImages(body.messages || []).length;
|
|
@@ -1659,6 +1746,158 @@ app.get('/dashboard/logs', requireLocalOrigin, async (req, res) => {
|
|
|
1659
1746
|
}
|
|
1660
1747
|
});
|
|
1661
1748
|
|
|
1749
|
+
// ---------------------------------------------------------------------------
|
|
1750
|
+
// Captures — diagnostic request/response inspector for the dashboard
|
|
1751
|
+
// ---------------------------------------------------------------------------
|
|
1752
|
+
//
|
|
1753
|
+
// These endpoints back the session-inspector UI. They expose the contents
|
|
1754
|
+
// of `~/.mobygate/captures/` (created by lib/request-capture.js) so the
|
|
1755
|
+
// dashboard can list past requests, drill into individual ones, and
|
|
1756
|
+
// toggle capture on/off live without restarting mobygate.
|
|
1757
|
+
//
|
|
1758
|
+
// All capture endpoints require local-origin (DNS-rebinding protection)
|
|
1759
|
+
// because they expose full request bodies including conversation content.
|
|
1760
|
+
|
|
1761
|
+
// Helper: parse a capture filename and return its components.
|
|
1762
|
+
// "2026-04-28_03-49-05_v1-chat-completions_abc123.json" → { ts, slug, requestId }
|
|
1763
|
+
function parseCaptureFilename(name) {
|
|
1764
|
+
const match = name.match(/^(\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2})_([\w-]+)_([0-9a-f]+)\.(json|summary\.txt)$/);
|
|
1765
|
+
if (!match) return null;
|
|
1766
|
+
return {
|
|
1767
|
+
timestamp: match[1].replace('_', 'T').replace(/-/g, (m, i) => i < 10 ? '-' : ':') + 'Z',
|
|
1768
|
+
slug: '/' + match[2].replace(/-/g, '/'),
|
|
1769
|
+
requestId: match[3],
|
|
1770
|
+
type: match[4],
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// Quick-read a summary.txt for the listing card (avoid loading the full JSON).
|
|
1775
|
+
async function readSummaryQuick(summaryPath) {
|
|
1776
|
+
const { readFile } = await import('fs/promises');
|
|
1777
|
+
const text = await readFile(summaryPath, 'utf8');
|
|
1778
|
+
const grab = (re, dflt = null) => { const m = text.match(re); return m ? m[1].trim() : dflt; };
|
|
1779
|
+
return {
|
|
1780
|
+
sessionKey: grab(/^session_key:\s+(\S+)/m),
|
|
1781
|
+
sessionSrc: grab(/^session_source:\s+(\S+)/m),
|
|
1782
|
+
model: grab(/^model:\s+(.+?)$/m),
|
|
1783
|
+
stream: grab(/^stream:\s+(\S+)/m) === 'true',
|
|
1784
|
+
msgCount: parseInt(grab(/^messages:\s+(\d+)/m, '0'), 10),
|
|
1785
|
+
sysBytes: parseInt(grab(/^\s*system:\s+(\d+)/m, '0'), 10),
|
|
1786
|
+
grandBytes: parseInt(grab(/^grand total:\s+(\d+)/m, '0'), 10),
|
|
1787
|
+
grandTokens: parseInt(grab(/≈\s+(\d+)\s+input/, '0'), 10),
|
|
1788
|
+
cacheControlSystem: grab(/cache_control:\s+(\d+\/\d+)\s+system/),
|
|
1789
|
+
// Response side (only present after captureResponse fires)
|
|
1790
|
+
inputTokens: parseInt(grab(/input_tokens \(uncached\):\s+(\d+)/, '0'), 10),
|
|
1791
|
+
cacheRead: parseInt(grab(/cache_read_input_tokens:\s+(\d+)/, '0'), 10),
|
|
1792
|
+
cacheCreate: parseInt(grab(/cache_creation_input_tokens:\s+(\d+)/, '0'), 10),
|
|
1793
|
+
outputTokens: parseInt(grab(/output_tokens:\s+(\d+)/, '0'), 10),
|
|
1794
|
+
cacheHitPct: grab(/cache hit rate:\s+([\d.]+)%/),
|
|
1795
|
+
durationMs: parseInt(grab(/^duration:\s+(\d+) ms/m, '0'), 10),
|
|
1796
|
+
status: grab(/^status:\s+(\S+)/m),
|
|
1797
|
+
stopReason: grab(/^stop_reason:\s+(\S+)/m),
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
// GET /dashboard/captures — list captures (newest first), with summary stats.
|
|
1802
|
+
app.get('/dashboard/captures', requireLocalOrigin, async (req, res) => {
|
|
1803
|
+
try {
|
|
1804
|
+
const { readdir, stat } = await import('fs/promises');
|
|
1805
|
+
const { CAPTURE_DIR_PATH } = await import('./lib/request-capture.js');
|
|
1806
|
+
const limit = Math.min(500, parseInt(req.query.limit || '100', 10));
|
|
1807
|
+
|
|
1808
|
+
let entries;
|
|
1809
|
+
try {
|
|
1810
|
+
entries = await readdir(CAPTURE_DIR_PATH);
|
|
1811
|
+
} catch {
|
|
1812
|
+
return res.json({ captures: [], dir: CAPTURE_DIR_PATH, note: 'capture dir does not exist (capture has not run yet)' });
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
// Pair .json with .summary.txt by matching the base name.
|
|
1816
|
+
const jsonFiles = entries.filter((n) => n.endsWith('.json'));
|
|
1817
|
+
const items = [];
|
|
1818
|
+
for (const jsonName of jsonFiles) {
|
|
1819
|
+
const summaryName = jsonName.replace(/\.json$/, '.summary.txt');
|
|
1820
|
+
if (!entries.includes(summaryName)) continue;
|
|
1821
|
+
const meta = parseCaptureFilename(jsonName);
|
|
1822
|
+
if (!meta) continue;
|
|
1823
|
+
const fullJson = join(CAPTURE_DIR_PATH, jsonName);
|
|
1824
|
+
const fullSummary = join(CAPTURE_DIR_PATH, summaryName);
|
|
1825
|
+
try {
|
|
1826
|
+
const [stJson, stSummary, summary] = await Promise.all([
|
|
1827
|
+
stat(fullJson),
|
|
1828
|
+
stat(fullSummary),
|
|
1829
|
+
readSummaryQuick(fullSummary),
|
|
1830
|
+
]);
|
|
1831
|
+
items.push({
|
|
1832
|
+
filename: jsonName,
|
|
1833
|
+
summaryFilename: summaryName,
|
|
1834
|
+
ts: stJson.mtimeMs,
|
|
1835
|
+
path: meta.slug,
|
|
1836
|
+
requestId: meta.requestId,
|
|
1837
|
+
jsonBytes: stJson.size,
|
|
1838
|
+
...summary,
|
|
1839
|
+
});
|
|
1840
|
+
} catch {}
|
|
1841
|
+
}
|
|
1842
|
+
items.sort((a, b) => b.ts - a.ts);
|
|
1843
|
+
res.json({ captures: items.slice(0, limit), total: items.length, dir: CAPTURE_DIR_PATH });
|
|
1844
|
+
} catch (e) {
|
|
1845
|
+
res.status(500).json({ error: e.message });
|
|
1846
|
+
}
|
|
1847
|
+
});
|
|
1848
|
+
|
|
1849
|
+
// GET /dashboard/captures/:filename — full body + summary for one capture.
|
|
1850
|
+
// Filename must end in .json (we serve the body and infer the summary path).
|
|
1851
|
+
app.get('/dashboard/captures/:filename', requireLocalOrigin, async (req, res) => {
|
|
1852
|
+
try {
|
|
1853
|
+
const { readFile } = await import('fs/promises');
|
|
1854
|
+
const { CAPTURE_DIR_PATH } = await import('./lib/request-capture.js');
|
|
1855
|
+
const filename = req.params.filename;
|
|
1856
|
+
// Defense in depth: reject anything with path separators or .. — must
|
|
1857
|
+
// be a bare filename within the capture dir.
|
|
1858
|
+
if (!/^[\w.-]+\.json$/.test(filename)) {
|
|
1859
|
+
return res.status(400).json({ error: 'invalid filename' });
|
|
1860
|
+
}
|
|
1861
|
+
const jsonPath = join(CAPTURE_DIR_PATH, filename);
|
|
1862
|
+
const summaryPath = join(CAPTURE_DIR_PATH, filename.replace(/\.json$/, '.summary.txt'));
|
|
1863
|
+
const [bodyRaw, summaryRaw] = await Promise.all([
|
|
1864
|
+
readFile(jsonPath, 'utf8'),
|
|
1865
|
+
readFile(summaryPath, 'utf8').catch(() => '(summary not found)'),
|
|
1866
|
+
]);
|
|
1867
|
+
res.json({
|
|
1868
|
+
filename,
|
|
1869
|
+
body: JSON.parse(bodyRaw),
|
|
1870
|
+
summary: summaryRaw,
|
|
1871
|
+
});
|
|
1872
|
+
} catch (e) {
|
|
1873
|
+
res.status(404).json({ error: e.message });
|
|
1874
|
+
}
|
|
1875
|
+
});
|
|
1876
|
+
|
|
1877
|
+
// GET /dashboard/captures-state — is capture currently enabled?
|
|
1878
|
+
app.get('/dashboard/captures-state', requireLocalOrigin, async (_req, res) => {
|
|
1879
|
+
const { isCaptureEnabled, CAPTURE_DIR_PATH, CAPTURE_TOGGLE_FILE } = await import('./lib/request-capture.js');
|
|
1880
|
+
res.json({
|
|
1881
|
+
enabled: isCaptureEnabled(),
|
|
1882
|
+
captureDir: CAPTURE_DIR_PATH,
|
|
1883
|
+
toggleFile: CAPTURE_TOGGLE_FILE,
|
|
1884
|
+
envVar: !!(process.env.MOBY_CAPTURE === '1' || process.env.MOBY_CAPTURE === 'true'),
|
|
1885
|
+
});
|
|
1886
|
+
});
|
|
1887
|
+
|
|
1888
|
+
// POST /dashboard/captures-toggle — flip the touch file on/off.
|
|
1889
|
+
// Body: { enabled: true | false }
|
|
1890
|
+
app.post('/dashboard/captures-toggle', requireLocalOrigin, async (req, res) => {
|
|
1891
|
+
try {
|
|
1892
|
+
const { setCaptureEnabled } = await import('./lib/request-capture.js');
|
|
1893
|
+
const target = !!req.body?.enabled;
|
|
1894
|
+
const newState = await setCaptureEnabled(target);
|
|
1895
|
+
res.json({ enabled: newState });
|
|
1896
|
+
} catch (e) {
|
|
1897
|
+
res.status(500).json({ error: e.message });
|
|
1898
|
+
}
|
|
1899
|
+
});
|
|
1900
|
+
|
|
1662
1901
|
// ---------------------------------------------------------------------------
|
|
1663
1902
|
// Updater — dashboard-driven "update available → update now" flow
|
|
1664
1903
|
// ---------------------------------------------------------------------------
|
|
@@ -1727,6 +1966,9 @@ app.listen(PORT, BIND, async () => {
|
|
|
1727
1966
|
console.log(` model ${DEFAULT_MODEL}`);
|
|
1728
1967
|
console.log(` session TTL ${ttlMin} min`);
|
|
1729
1968
|
console.log(` dashboard http://localhost:${PORT}`);
|
|
1969
|
+
if (isCaptureEnabled()) {
|
|
1970
|
+
console.log(` capture ON → ${CAPTURE_DIR_PATH.replace(process.env.HOME || '', '~')}`);
|
|
1971
|
+
}
|
|
1730
1972
|
console.log('');
|
|
1731
1973
|
dashboardBus.emitEvent({ type: 'server.boot', port: PORT, bind: BIND, defaultModel: DEFAULT_MODEL });
|
|
1732
1974
|
});
|