total-recall-brain 3.26.0 → 3.28.0
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/bin/total-recall.mjs +31 -1
- package/docs/ARCHITECTURE.md +76 -9
- package/frontend/dist/assets/index-U7o-qU99.js +5742 -0
- package/frontend/dist/index.html +1 -1
- package/frontend/src/App.tsx +39 -1
- package/frontend/src/api/index.ts +1 -0
- package/frontend/src/api/plugins.ts +167 -0
- package/frontend/src/pages/ChatPage.tsx +44 -17
- package/frontend/src/pages/IntegrationsPage.tsx +7 -0
- package/frontend/src/pages/PluginsPage.spec.tsx +17 -0
- package/frontend/src/pages/PluginsPage.tsx +1331 -0
- package/package.json +1 -1
- package/scaffold/.agent/plugins/.gitkeep +0 -0
- package/scaffold/.agent/plugins/code-quality/README.md +16 -0
- package/scaffold/.agent/plugins/code-quality/cli.mjs +15 -0
- package/scaffold/.agent/plugins/code-quality/plugin.json +34 -0
- package/scaffold/.agent/plugins/git-sentinel/README.md +16 -0
- package/scaffold/.agent/plugins/git-sentinel/cli.mjs +86 -0
- package/scaffold/.agent/plugins/git-sentinel/plugin.json +34 -0
- package/scaffold/.agent/plugins/meta-harness/README.md +16 -0
- package/scaffold/.agent/plugins/meta-harness/cli.mjs +34 -0
- package/scaffold/.agent/plugins/meta-harness/plugin.json +34 -0
- package/scaffold/.agent/plugins/system-monitor/README.md +16 -0
- package/scaffold/.agent/plugins/system-monitor/cli.mjs +79 -0
- package/scaffold/.agent/plugins/system-monitor/generator.mjs +20 -0
- package/scaffold/.agent/plugins/system-monitor/plugin.json +38 -0
- package/scaffold/.agent/skills/meta-harness/SKILL.md +113 -0
- package/scaffold/.agent/skills/total-recall/references/cli-reference.md +23 -0
- package/src/cli/agent.mjs +173 -0
- package/src/cli/connect.mjs +89 -4
- package/src/cli/deploy-ui.mjs +6 -0
- package/src/cli/harness.mjs +135 -0
- package/src/cli/ingest-openwiki.mjs +71 -2
- package/src/cli/init.mjs +1 -0
- package/src/cli/lint.mjs +47 -3
- package/src/cli/mesh.mjs +286 -0
- package/src/cli/plugin/create.mjs +186 -0
- package/src/cli/plugin/index.mjs +139 -0
- package/src/cli/plugin/install.mjs +141 -0
- package/src/cli/plugin/list.mjs +83 -0
- package/src/cli/plugin/remove.mjs +51 -0
- package/src/cli/plugin/search.mjs +37 -0
- package/src/cli/skill-projection.mjs +14 -0
- package/src/cli/wizard.html +9 -0
- package/src/core/agent-manager.mjs +290 -0
- package/src/core/cache-prune.mjs +341 -0
- package/src/core/context-cache.mjs +130 -0
- package/src/core/daemon-loop.mjs +47 -1
- package/src/core/evolving-context.mjs +171 -0
- package/src/core/mesh-activity.mjs +140 -0
- package/src/core/mesh.mjs +174 -1
- package/src/core/meta-harness.mjs +251 -0
- package/src/core/plugin-context.mjs +206 -0
- package/src/core/plugin-loader.mjs +210 -0
- package/src/core/runtime.mjs +65 -13
- package/src/core/surface.mjs +32 -10
- package/src/server/api.mjs +18 -2
- package/src/server/index.mjs +8 -2
- package/src/server/rest.mjs +4 -0
- package/src/server/routes/integrations.mjs +5 -1
- package/src/server/routes/mesh-presence.mjs +143 -0
- package/src/server/routes/models.mjs +42 -30
- package/src/server/routes/plugins.mjs +517 -0
- package/src/server/tools.mjs +266 -0
- package/frontend/dist/assets/index-Bl3I11B0.js +0 -5742
package/src/core/runtime.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import yaml from 'yaml';
|
|
4
4
|
import os from 'os';
|
|
5
|
-
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
6
6
|
import { logger } from './logger.mjs';
|
|
7
7
|
import { agentDir, brainDir, googleApiKey, tavilyApiKey, braveApiKey, exaApiKey, serperApiKey, githubToken, embedModel } from './config.mjs';
|
|
8
8
|
import { checkBudgetSafety } from './usage-tracker.mjs';
|
|
@@ -57,10 +57,10 @@ export function findBinaryInPath(binaryName) {
|
|
|
57
57
|
// fails with HTTP 400 at exit code 0.
|
|
58
58
|
const DEFAULT_AGENTS = [
|
|
59
59
|
{ name: 'agy', binary: 'agy', flags: '--output-format json -p', priority: 1, enabled: true, exec: 'flag' },
|
|
60
|
+
{ name: 'antigravity', binary: 'agy', flags: '--output-format json -p', priority: 1, enabled: true, exec: 'flag' },
|
|
60
61
|
{ name: 'claude', binary: 'claude', flags: '--output-format json --permission-mode bypassPermissions --setting-sources local --tools "" -p', priority: 2, enabled: true, exec: 'flag' },
|
|
61
62
|
{ name: 'codex', binary: 'codex', flags: '-m gpt-5.5 --sandbox workspace-write --json --skip-git-repo-check', priority: 3, enabled: true, exec: 'subcommand' },
|
|
62
|
-
{ name: '
|
|
63
|
-
{ name: 'gemini', binary: 'gemini', flags: '--sandbox=false --yolo -o json', priority: 9, enabled: false, exec: 'flag' },
|
|
63
|
+
{ name: 'gemini', binary: 'agy', flags: '--output-format json -p', priority: 9, enabled: false, exec: 'flag' },
|
|
64
64
|
{ name: 'grok', binary: 'grok', flags: '--output-format plain --always-approve --permission-mode bypassPermissions', priority: 10, enabled: false, exec: 'flag' },
|
|
65
65
|
];
|
|
66
66
|
|
|
@@ -251,7 +251,10 @@ function resolveAgent(config) {
|
|
|
251
251
|
|
|
252
252
|
for (const agent of agents.sort((a, b) => a.priority - b.priority)) {
|
|
253
253
|
if (!agent.enabled) continue;
|
|
254
|
-
|
|
254
|
+
let binaryPath = findBinaryInPath(agent.binary);
|
|
255
|
+
if (!binaryPath && (agent.name === 'antigravity' || agent.name === 'gemini' || agent.binary === 'antigravity')) {
|
|
256
|
+
binaryPath = findBinaryInPath('agy');
|
|
257
|
+
}
|
|
255
258
|
if (binaryPath) {
|
|
256
259
|
return {
|
|
257
260
|
...agent,
|
|
@@ -364,11 +367,24 @@ export async function callLocalRuntime(prompt, system, config) {
|
|
|
364
367
|
.map(tok => tok.replace(/^(['"])(.*)\1$/, '$2')),
|
|
365
368
|
);
|
|
366
369
|
}
|
|
370
|
+
// If -p or --print is in args, remove it so options like --model can precede it,
|
|
371
|
+
// and we will push -p immediately before fullPrompt.
|
|
372
|
+
const hadPrintFlag = args.includes('-p') || args.includes('--print');
|
|
373
|
+
if (hadPrintFlag) {
|
|
374
|
+
const pIdx = args.indexOf('-p');
|
|
375
|
+
if (pIdx !== -1) args.splice(pIdx, 1);
|
|
376
|
+
const printIdx = args.indexOf('--print');
|
|
377
|
+
if (printIdx !== -1) args.splice(printIdx, 1);
|
|
378
|
+
}
|
|
367
379
|
if (agent.model) {
|
|
368
|
-
|
|
380
|
+
if (agent.binary === 'agy' || agent.name === 'agy' || (agent.binaryPath && agent.binaryPath.endsWith('/agy'))) {
|
|
381
|
+
args.push('--model', agent.model);
|
|
382
|
+
} else {
|
|
383
|
+
args.push('-m', agent.model);
|
|
384
|
+
}
|
|
369
385
|
}
|
|
370
|
-
// `exec: flag` agents need an explicit `-p`
|
|
371
|
-
if (agent.exec !== 'subcommand'
|
|
386
|
+
// `exec: flag` agents need an explicit `-p` right before fullPrompt
|
|
387
|
+
if (agent.exec !== 'subcommand' || hadPrintFlag) {
|
|
372
388
|
args.push('-p');
|
|
373
389
|
}
|
|
374
390
|
args.push(fullPrompt);
|
|
@@ -415,14 +431,50 @@ export async function callLocalRuntime(prompt, system, config) {
|
|
|
415
431
|
};
|
|
416
432
|
|
|
417
433
|
logger.info('api', 'SPAWN_ENV GOOGLE_API_KEY length: ' + (spawnEnv.GOOGLE_API_KEY ? spawnEnv.GOOGLE_API_KEY.length : 0));
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
434
|
+
const runAsync = () => new Promise((resolve) => {
|
|
435
|
+
let child;
|
|
436
|
+
try {
|
|
437
|
+
child = spawn(agent.binaryPath, args, {
|
|
438
|
+
env: spawnEnv,
|
|
439
|
+
cwd: process.cwd(),
|
|
440
|
+
});
|
|
441
|
+
} catch (err) {
|
|
442
|
+
return resolve({ status: 1, stdout: '', stderr: err.message });
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
let stdout = '';
|
|
446
|
+
let stderr = '';
|
|
447
|
+
let timer = null;
|
|
448
|
+
if (timeout) {
|
|
449
|
+
timer = setTimeout(() => {
|
|
450
|
+
try { child.kill('SIGTERM'); } catch {}
|
|
451
|
+
}, timeout);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
child.on('error', (err) => {
|
|
455
|
+
if (timer) clearTimeout(timer);
|
|
456
|
+
resolve({ status: 1, stdout, stderr: err.message });
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
if (child.stdout) {
|
|
460
|
+
child.stdout.on('data', (d) => {
|
|
461
|
+
if (stdout.length < 10 * 1024 * 1024) stdout += d.toString();
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
if (child.stderr) {
|
|
465
|
+
child.stderr.on('data', (d) => {
|
|
466
|
+
if (stderr.length < 10 * 1024 * 1024) stderr += d.toString();
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
child.on('close', (code) => {
|
|
471
|
+
if (timer) clearTimeout(timer);
|
|
472
|
+
resolve({ status: code ?? 0, stdout, stderr });
|
|
473
|
+
});
|
|
424
474
|
});
|
|
425
475
|
|
|
476
|
+
const result = await runAsync();
|
|
477
|
+
|
|
426
478
|
const softFailure = detectAgentFailure(result.stdout || '');
|
|
427
479
|
|
|
428
480
|
if (result.status === 0 && !softFailure) {
|
package/src/core/surface.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
buildMemoryLayerIndex,
|
|
11
11
|
inferMemoryLayer,
|
|
12
12
|
} from './memory-layers.mjs';
|
|
13
|
+
import { assemblePluginContexts } from './plugin-context.mjs';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Extract [[slug]] wikilink references and relative Markdown link targets from body text.
|
|
@@ -306,7 +307,7 @@ async function compactNode(node, derivedDir, force = false) {
|
|
|
306
307
|
return compacted;
|
|
307
308
|
}
|
|
308
309
|
|
|
309
|
-
export async function buildRulesBlock(skillsDir, nodes = [], { consumer = 'ide', derivedDir, force = false } = {}) {
|
|
310
|
+
export async function buildRulesBlock(skillsDir, nodes = [], { consumer = 'ide', derivedDir, force = false, vaultDir, projectRoot } = {}) {
|
|
310
311
|
// 1. Filter expired rules. Compilation must remain a pure projection step;
|
|
311
312
|
// archival is handled by explicit memory operations.
|
|
312
313
|
const now = new Date();
|
|
@@ -481,6 +482,21 @@ export async function buildRulesBlock(skillsDir, nodes = [], { consumer = 'ide',
|
|
|
481
482
|
}
|
|
482
483
|
}
|
|
483
484
|
|
|
485
|
+
// 5. Inject Evolving Plugin Context
|
|
486
|
+
try {
|
|
487
|
+
const pluginContext = await assemblePluginContexts({
|
|
488
|
+
projectRoot: projectRoot || (skillsDir ? path.dirname(path.dirname(skillsDir)) : process.cwd()),
|
|
489
|
+
vaultDir,
|
|
490
|
+
nodes,
|
|
491
|
+
derivedDir
|
|
492
|
+
});
|
|
493
|
+
if (pluginContext) {
|
|
494
|
+
combined += pluginContext;
|
|
495
|
+
}
|
|
496
|
+
} catch (err) {
|
|
497
|
+
console.error('Error injecting plugin context:', err);
|
|
498
|
+
}
|
|
499
|
+
|
|
484
500
|
return combined.trim();
|
|
485
501
|
}
|
|
486
502
|
|
|
@@ -546,15 +562,21 @@ async function writeShim(shimPath, skillsDir, nodes = [], { vaultDir, derivedDir
|
|
|
546
562
|
* Map of client names → shim file paths they require.
|
|
547
563
|
*/
|
|
548
564
|
const CLIENT_SHIMS = {
|
|
549
|
-
cursor:
|
|
550
|
-
claude:
|
|
551
|
-
'claude-code':
|
|
552
|
-
cline:
|
|
553
|
-
antigravity:
|
|
554
|
-
gemini:
|
|
555
|
-
codex:
|
|
556
|
-
vscode:
|
|
557
|
-
githubCopilot:
|
|
565
|
+
cursor: ['.cursorrules'],
|
|
566
|
+
claude: ['CLAUDE.md'],
|
|
567
|
+
'claude-code': ['CLAUDE.md'],
|
|
568
|
+
cline: ['.clinerules/total-recall.md'],
|
|
569
|
+
antigravity: ['AGENTS.md', '.agents/rules/AGENTS.md'],
|
|
570
|
+
gemini: ['GEMINI.md', '.agents/rules/GEMINI.md'],
|
|
571
|
+
codex: ['AGENTS.md'],
|
|
572
|
+
vscode: ['.github/copilot-instructions.md', '.vscode/copilot-instructions.md'],
|
|
573
|
+
githubCopilot: ['.github/copilot-instructions.md'],
|
|
574
|
+
pi: ['AGENTS.md'],
|
|
575
|
+
hermes: ['.hermes/memories/MEMORY.md'],
|
|
576
|
+
'hermes-agent': ['.hermes/memories/MEMORY.md'],
|
|
577
|
+
dsh: ['AGENTS.md'],
|
|
578
|
+
'deepseek-harness': ['AGENTS.md'],
|
|
579
|
+
openclaw: ['MEMORY.md', 'AGENTS.md']
|
|
558
580
|
};
|
|
559
581
|
|
|
560
582
|
/**
|
package/src/server/api.mjs
CHANGED
|
@@ -384,11 +384,17 @@ apiRouter.post('/v1/chat/completions', requireScope('chat:write'), async (req, r
|
|
|
384
384
|
if (targetAgentName.includes(':')) {
|
|
385
385
|
const parts = targetAgentName.split(':');
|
|
386
386
|
targetAgentName = parts[0];
|
|
387
|
-
subModel = parts
|
|
387
|
+
subModel = parts.slice(1).join(':');
|
|
388
388
|
} else if (targetAgentName.includes('/')) {
|
|
389
389
|
const parts = targetAgentName.split('/');
|
|
390
390
|
targetAgentName = parts[0];
|
|
391
|
-
subModel = parts
|
|
391
|
+
subModel = parts.slice(1).join('/');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// If user selected 'antigravity' or 'gemini', check if 'agy' is available
|
|
395
|
+
if (targetAgentName === 'antigravity' || targetAgentName === 'gemini') {
|
|
396
|
+
const hasAgy = activeConfig.agents.some(a => a.name === 'agy');
|
|
397
|
+
if (hasAgy) targetAgentName = 'agy';
|
|
392
398
|
}
|
|
393
399
|
|
|
394
400
|
const idx = activeConfig.agents.findIndex(a => a.name === targetAgentName);
|
|
@@ -396,6 +402,7 @@ apiRouter.post('/v1/chat/completions', requireScope('chat:write'), async (req, r
|
|
|
396
402
|
// Deep copy the agents list so we don't mutate the cached config
|
|
397
403
|
activeConfig.agents = activeConfig.agents.map(a => ({ ...a }));
|
|
398
404
|
activeConfig.agents[idx].priority = 0;
|
|
405
|
+
activeConfig.agents[idx].enabled = true;
|
|
399
406
|
if (subModel) {
|
|
400
407
|
activeConfig.agents[idx].model = subModel;
|
|
401
408
|
}
|
|
@@ -432,6 +439,14 @@ CODE / MEMORY:
|
|
|
432
439
|
- 'execute_code': Run Node.js to call APIs, process data, or perform calculations.
|
|
433
440
|
- 'update_design': Write markdown to DESIGN.md when asked to create a UI or document.
|
|
434
441
|
|
|
442
|
+
MESH NETWORK TOOLS (WireGuard / Headscale mesh management):
|
|
443
|
+
- 'mesh_list_nodes': List all nodes on the mesh network (hostname, IP, online status, role, OS, SSH login account).
|
|
444
|
+
- 'mesh_status': Check mesh control plane (Headscale) availability, network status, and self node info.
|
|
445
|
+
- 'mesh_exec': Execute a shell command remotely on any peer node via SSH (e.g. 'uptime', 'docker ps', 'git status').
|
|
446
|
+
- 'mesh_set_access': Record or update SSH login account (user, port, key, host override) on a node entity.
|
|
447
|
+
- 'mesh_ping': Test reachability and measure round-trip latency to a mesh node.
|
|
448
|
+
- 'mesh_mint_preauthkey': Mint a pre-auth enrollment key to connect a new machine to the mesh.
|
|
449
|
+
|
|
435
450
|
MANDATORY TOOL USE AND CITATION RULES:
|
|
436
451
|
1. FORCE SEARCH: If the user asks you to "search", "lookup", "find", or asks about current/recent events or anything you do not explicitly have in your memory vault, you MUST output a tool call to 'search_web' immediately. Do not write a conversational introduction first.
|
|
437
452
|
2. CITATION MANDATE: When answering using results from 'search_web' or 'browser_navigate', you MUST explicitly cite your sources by embedding clickable markdown links (e.g., [Source Title](URL)) directly in your response. Never summarize web facts without providing their corresponding URLs.
|
|
@@ -447,6 +462,7 @@ Example:
|
|
|
447
462
|
}
|
|
448
463
|
</tool_call>
|
|
449
464
|
Do not add any other conversational text when outputting the fallback tool call. Output it immediately as your entire response.
|
|
465
|
+
5. MESH OPERATIONS: When asked about nodes on the mesh, running commands across the network, checking mesh connectivity, or configuring node login access, you MUST use the appropriate 'mesh_*' tool.
|
|
450
466
|
|
|
451
467
|
You have a REAL browser AND full desktop control. Use them. Navigate, click, type, scrape — do not just describe what you would do. For web tasks prefer browser tools. For native apps or desktop workflows use computer_screenshot first to orient yourself.`;
|
|
452
468
|
|
package/src/server/index.mjs
CHANGED
|
@@ -236,8 +236,14 @@ app.get('/health', requireAuthOrLocal, async (req, res) => {
|
|
|
236
236
|
// CLI agent availability
|
|
237
237
|
const { findBinaryInPath } = await import('../core/runtime.mjs');
|
|
238
238
|
const cliAgents = [];
|
|
239
|
-
for (const bin of ['antigravity', '
|
|
240
|
-
if (findBinaryInPath(bin))
|
|
239
|
+
for (const bin of ['agy', 'antigravity', 'claude', 'codex', 'grok', 'gemini']) {
|
|
240
|
+
if (findBinaryInPath(bin)) {
|
|
241
|
+
if (bin === 'agy') {
|
|
242
|
+
if (!cliAgents.includes('antigravity')) cliAgents.push('antigravity');
|
|
243
|
+
} else if (!cliAgents.includes(bin)) {
|
|
244
|
+
cliAgents.push(bin);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
241
247
|
}
|
|
242
248
|
|
|
243
249
|
const agentDir = configAgentDir;
|
package/src/server/rest.mjs
CHANGED
|
@@ -123,6 +123,8 @@ import ttsRouter from './routes/tts.mjs';
|
|
|
123
123
|
import updateRouter from './routes/update.mjs';
|
|
124
124
|
import { rulesRouter } from './routes/rules.mjs';
|
|
125
125
|
import notificationsRouter from './routes/notifications.mjs';
|
|
126
|
+
import meshPresenceRouter from './routes/mesh-presence.mjs';
|
|
127
|
+
import pluginsRouter from './routes/plugins.mjs';
|
|
126
128
|
// ollamaUrl removed — CLI agents replace Ollama
|
|
127
129
|
import {
|
|
128
130
|
AGENT_DIR,
|
|
@@ -239,6 +241,8 @@ router.use(updateRouter);
|
|
|
239
241
|
router.use(ssssRouter);
|
|
240
242
|
router.use(rulesRouter);
|
|
241
243
|
router.use(notificationsRouter);
|
|
244
|
+
router.use(meshPresenceRouter);
|
|
245
|
+
router.use(pluginsRouter);
|
|
242
246
|
|
|
243
247
|
export { router as restRouter };
|
|
244
248
|
export default router;
|
|
@@ -88,6 +88,9 @@ router.get('/api/integrations/active', requireAuth, (req, res) => {
|
|
|
88
88
|
'gemini': [path.join(HOME, '.gemini')],
|
|
89
89
|
'pi': [path.join(HOME, '.pi', 'agent')],
|
|
90
90
|
'hermes': [path.join(HOME, '.hermes')],
|
|
91
|
+
'hermes-agent':[path.join(HOME, '.hermes')],
|
|
92
|
+
'dsh': [path.join(HOME, '.dsh')],
|
|
93
|
+
'deepseek-harness': [path.join(HOME, '.dsh')],
|
|
91
94
|
'openclaw': [path.join(HOME, '.openclaw')],
|
|
92
95
|
};
|
|
93
96
|
|
|
@@ -116,7 +119,8 @@ router.post('/api/integrations/connect', requireAuth, (req, res) => {
|
|
|
116
119
|
if (!client) return badRequest(res, 'client is required');
|
|
117
120
|
|
|
118
121
|
const validClients = [
|
|
119
|
-
'vscode', 'pi', 'hermes', '
|
|
122
|
+
'vscode', 'pi', 'hermes', 'hermes-agent', 'dsh', 'deepseek-harness',
|
|
123
|
+
'openclaw', 'cursor', 'cline', 'claude-code',
|
|
120
124
|
'codex', 'gemini', 'aider', 'http-api', 'obsidian',
|
|
121
125
|
'generic', 'antigravity'
|
|
122
126
|
];
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { requireAuth, requireScope, requireAuthOrLocal } from '../auth.mjs';
|
|
3
|
+
import { getLocalPresence, resolveActiveDevice } from '../../core/mesh-activity.mjs';
|
|
4
|
+
import { getMeshPeers, execMeshCommand } from '../../core/mesh.mjs';
|
|
5
|
+
import { throttledFetch } from '../../core/throttled-fetch.mjs';
|
|
6
|
+
import { serverError, badRequest } from './_shared.mjs';
|
|
7
|
+
|
|
8
|
+
const router = Router();
|
|
9
|
+
|
|
10
|
+
function meshServerPort() {
|
|
11
|
+
const value = Number(process.env.TR_SERVER_PORT || process.env.PORT || 3000);
|
|
12
|
+
return Number.isInteger(value) && value > 0 && value <= 65535 ? value : 3000;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* GET /api/mesh/presence
|
|
17
|
+
* Reports the presence state across the mesh, highlighting the user's active device.
|
|
18
|
+
*/
|
|
19
|
+
router.get('/api/mesh/presence', requireAuth, requireScope('config:read'), async (req, res) => {
|
|
20
|
+
try {
|
|
21
|
+
const local = getLocalPresence();
|
|
22
|
+
const includePeers = req.query?.peers !== 'false';
|
|
23
|
+
const peers = [];
|
|
24
|
+
|
|
25
|
+
if (includePeers) {
|
|
26
|
+
const meshPeers = getMeshPeers({ includeSelf: false }) || [];
|
|
27
|
+
const port = meshServerPort();
|
|
28
|
+
|
|
29
|
+
const peerPromises = meshPeers
|
|
30
|
+
.filter((p) => p && p.online && p.ip)
|
|
31
|
+
.map(async (p) => {
|
|
32
|
+
try {
|
|
33
|
+
const peerRes = await throttledFetch(
|
|
34
|
+
`http://${p.ip}:${port}/api/mesh/presence/local`,
|
|
35
|
+
{},
|
|
36
|
+
1500
|
|
37
|
+
);
|
|
38
|
+
if (peerRes && (peerRes.ok || peerRes.status === 200)) {
|
|
39
|
+
return await peerRes.json();
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// Peer unreachable or offline
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
node_id: p.hostname,
|
|
46
|
+
mesh_ip: p.ip,
|
|
47
|
+
user_active: false,
|
|
48
|
+
idle_seconds: 999999,
|
|
49
|
+
active_surface: 'offline',
|
|
50
|
+
timestamp: new Date().toISOString()
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const resolved = await Promise.all(peerPromises);
|
|
55
|
+
peers.push(...resolved.filter(Boolean));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const allRecords = [local, ...peers];
|
|
59
|
+
const activeDevice = resolveActiveDevice(allRecords);
|
|
60
|
+
|
|
61
|
+
res.json({
|
|
62
|
+
success: true,
|
|
63
|
+
local,
|
|
64
|
+
peers,
|
|
65
|
+
active_device: activeDevice,
|
|
66
|
+
active_surface: local.active_surface,
|
|
67
|
+
timestamp: new Date().toISOString()
|
|
68
|
+
});
|
|
69
|
+
} catch (err) {
|
|
70
|
+
serverError(res, err);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* GET /api/mesh/presence/local
|
|
76
|
+
* Lightweight unauthenticated/mesh-only endpoint for peer heartbeat polling.
|
|
77
|
+
*/
|
|
78
|
+
router.get('/api/mesh/presence/local', requireAuthOrLocal, (_req, res) => {
|
|
79
|
+
try {
|
|
80
|
+
const local = getLocalPresence();
|
|
81
|
+
res.json(local);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
serverError(res, err);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* POST /api/mesh/presence/resolve
|
|
89
|
+
* Evaluates arbitrary presence records to determine the user's currently active device.
|
|
90
|
+
* Body: { presence_records: [...] }
|
|
91
|
+
*/
|
|
92
|
+
router.post('/api/mesh/presence/resolve', requireAuth, requireScope('config:read'), (req, res) => {
|
|
93
|
+
try {
|
|
94
|
+
const records = Array.isArray(req.body?.presence_records) ? req.body.presence_records : [];
|
|
95
|
+
const active = resolveActiveDevice(records);
|
|
96
|
+
res.json({
|
|
97
|
+
success: true,
|
|
98
|
+
active_device: active,
|
|
99
|
+
timestamp: new Date().toISOString()
|
|
100
|
+
});
|
|
101
|
+
} catch (err) {
|
|
102
|
+
serverError(res, err);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* POST /api/mesh/dispatch
|
|
108
|
+
* Follow the User dynamic dispatch: routes notifications or commands to the most active node.
|
|
109
|
+
* Body: { notification?: string, command?: string, target?: string, payload?: any }
|
|
110
|
+
*/
|
|
111
|
+
router.post('/api/mesh/dispatch', requireAuth, requireScope('config:write'), async (req, res) => {
|
|
112
|
+
try {
|
|
113
|
+
const local = getLocalPresence();
|
|
114
|
+
let targetNode = req.body?.target;
|
|
115
|
+
|
|
116
|
+
if (!targetNode) {
|
|
117
|
+
const active = resolveActiveDevice([local]);
|
|
118
|
+
targetNode = active.node_id;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const isLocal = targetNode === local.node_id || targetNode === 'local';
|
|
122
|
+
let executionResult = null;
|
|
123
|
+
|
|
124
|
+
if (!isLocal && req.body?.command) {
|
|
125
|
+
executionResult = await execMeshCommand(targetNode, req.body.command);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
res.json({
|
|
129
|
+
success: true,
|
|
130
|
+
dispatched: true,
|
|
131
|
+
target_node: targetNode,
|
|
132
|
+
is_local: isLocal,
|
|
133
|
+
command_result: executionResult,
|
|
134
|
+
notification: req.body?.notification || null,
|
|
135
|
+
payload: req.body?.payload || null,
|
|
136
|
+
timestamp: new Date().toISOString()
|
|
137
|
+
});
|
|
138
|
+
} catch (err) {
|
|
139
|
+
serverError(res, err);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
export default router;
|
|
@@ -151,43 +151,48 @@ router.get('/v1/models', requireAuthOrLocal, async (req, res) => {
|
|
|
151
151
|
}
|
|
152
152
|
});
|
|
153
153
|
|
|
154
|
+
let cachedAgyModels = null;
|
|
155
|
+
let lastAgyModelsFetch = 0;
|
|
156
|
+
|
|
154
157
|
/**
|
|
155
158
|
* GET /api/gemini-models
|
|
156
159
|
* Dynamically fetches available Gemini models.
|
|
157
160
|
* Strategy: CLI discovery → Google API → static fallback list.
|
|
158
161
|
*/
|
|
159
|
-
router.get('/api/gemini-models',
|
|
162
|
+
router.get('/api/gemini-models', requireAuthOrLocal, async (req, res) => {
|
|
160
163
|
try {
|
|
161
|
-
// 1. Try
|
|
164
|
+
// 1. Try Google Antigravity (agy) CLI first (cached 5 minutes)
|
|
165
|
+
if (cachedAgyModels && Date.now() - lastAgyModelsFetch < 1000 * 60 * 5) {
|
|
166
|
+
return res.json({ models: cachedAgyModels, source: 'cli' });
|
|
167
|
+
}
|
|
168
|
+
|
|
162
169
|
try {
|
|
163
170
|
const { findBinaryInPath } = await import('../../core/runtime.mjs');
|
|
164
|
-
const
|
|
165
|
-
if (
|
|
166
|
-
const result = spawnSync(
|
|
171
|
+
const agyPath = findBinaryInPath('agy');
|
|
172
|
+
if (agyPath) {
|
|
173
|
+
const result = spawnSync(agyPath, ['models'], { encoding: 'utf8', timeout: 8000, input: '' });
|
|
167
174
|
if (result.status === 0 && result.stdout) {
|
|
168
|
-
const
|
|
175
|
+
const lines = result.stdout.split('\n');
|
|
169
176
|
const discovered = [];
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
177
|
+
for (const line of lines) {
|
|
178
|
+
const trimmed = line.trim();
|
|
179
|
+
if (!trimmed || trimmed.startsWith('Fetching')) continue;
|
|
180
|
+
const parts = trimmed.split('\t');
|
|
181
|
+
if (parts.length >= 2) {
|
|
182
|
+
discovered.push({ id: parts[0].trim(), displayName: parts[1].trim() });
|
|
183
|
+
} else if (parts.length === 1 && parts[0].includes('gemini')) {
|
|
184
|
+
discovered.push({ id: parts[0].trim(), displayName: parts[0].trim() });
|
|
185
|
+
}
|
|
175
186
|
}
|
|
176
|
-
|
|
177
|
-
const versionRegex = /\b(\d+\.\d+-\w+)\b/g;
|
|
178
|
-
const versionMatches = output.match(versionRegex) || [];
|
|
179
|
-
for (const vm of versionMatches) {
|
|
180
|
-
discovered.push(`gemini-${vm}`);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const uniqueModels = [...new Set(discovered)];
|
|
184
|
-
if (uniqueModels.length > 0) {
|
|
187
|
+
if (discovered.length > 0) {
|
|
185
188
|
const pricingMap = await getPricingMap();
|
|
186
|
-
const cliModels =
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
});
|
|
189
|
+
const cliModels = discovered.map(m => ({
|
|
190
|
+
id: m.id,
|
|
191
|
+
displayName: m.displayName,
|
|
192
|
+
pricing: pricingMap[m.id] || pricingMap[`google/${m.id}`] || null,
|
|
193
|
+
}));
|
|
194
|
+
cachedAgyModels = cliModels;
|
|
195
|
+
lastAgyModelsFetch = Date.now();
|
|
191
196
|
return res.json({ models: cliModels, source: 'cli' });
|
|
192
197
|
}
|
|
193
198
|
}
|
|
@@ -196,7 +201,7 @@ router.get('/api/gemini-models', requireAuth, async (req, res) => {
|
|
|
196
201
|
// Fail silently and fall back
|
|
197
202
|
}
|
|
198
203
|
|
|
199
|
-
//
|
|
204
|
+
// 3. Try Google API key
|
|
200
205
|
let apiKey = process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY;
|
|
201
206
|
if (!apiKey) {
|
|
202
207
|
try {
|
|
@@ -207,10 +212,17 @@ router.get('/api/gemini-models', requireAuth, async (req, res) => {
|
|
|
207
212
|
}
|
|
208
213
|
|
|
209
214
|
const fallbackModels = [
|
|
215
|
+
{ id: 'gemini-3.8-flash-high', displayName: 'Gemini 3.8 Flash (High)', pricing: null },
|
|
216
|
+
{ id: 'gemini-3.8-flash-medium', displayName: 'Gemini 3.8 Flash (Medium)', pricing: null },
|
|
217
|
+
{ id: 'gemini-3.8-flash-low', displayName: 'Gemini 3.8 Flash (Low)', pricing: null },
|
|
218
|
+
{ id: 'gemini-3.7-flash-high', displayName: 'Gemini 3.7 Flash (High)', pricing: null },
|
|
219
|
+
{ id: 'gemini-3.7-flash-medium', displayName: 'Gemini 3.7 Flash (Medium)', pricing: null },
|
|
220
|
+
{ id: 'gemini-3.6-flash-high', displayName: 'Gemini 3.6 Flash (High)', pricing: null },
|
|
221
|
+
{ id: 'gemini-3.1-pro-high', displayName: 'Gemini 3.1 Pro (High)', pricing: null },
|
|
210
222
|
{ id: 'gemini-3.5-flash', displayName: 'Gemini 3.5 Flash', pricing: null },
|
|
211
223
|
{ id: 'gemini-3.5-pro', displayName: 'Gemini 3.5 Pro', pricing: null },
|
|
224
|
+
{ id: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', pricing: null },
|
|
212
225
|
{ id: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', pricing: null },
|
|
213
|
-
{ id: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', pricing: null }
|
|
214
226
|
];
|
|
215
227
|
|
|
216
228
|
if (!apiKey) return res.json({ models: fallbackModels, source: 'missing_key' });
|
|
@@ -251,7 +263,7 @@ router.get('/api/gemini-models', requireAuth, async (req, res) => {
|
|
|
251
263
|
* GET /api/claude-models
|
|
252
264
|
* Dynamically fetches available Anthropic models using ANTHROPIC_API_KEY.
|
|
253
265
|
*/
|
|
254
|
-
router.get('/api/claude-models',
|
|
266
|
+
router.get('/api/claude-models', requireAuthOrLocal, async (req, res) => {
|
|
255
267
|
try {
|
|
256
268
|
let apiKey = process.env.ANTHROPIC_API_KEY;
|
|
257
269
|
if (!apiKey) {
|
|
@@ -301,7 +313,7 @@ router.get('/api/claude-models', requireAuth, async (req, res) => {
|
|
|
301
313
|
* GET /api/openai-models
|
|
302
314
|
* Dynamically fetches available OpenAI models using OPENAI_API_KEY.
|
|
303
315
|
*/
|
|
304
|
-
router.get('/api/openai-models',
|
|
316
|
+
router.get('/api/openai-models', requireAuthOrLocal, async (req, res) => {
|
|
305
317
|
try {
|
|
306
318
|
let apiKey = process.env.OPENAI_API_KEY;
|
|
307
319
|
if (!apiKey) {
|
|
@@ -352,7 +364,7 @@ router.get('/api/openai-models', requireAuth, async (req, res) => {
|
|
|
352
364
|
* GET /api/openrouter-models
|
|
353
365
|
* Dynamically fetches the list of available OpenRouter models.
|
|
354
366
|
*/
|
|
355
|
-
router.get('/api/openrouter-models',
|
|
367
|
+
router.get('/api/openrouter-models', requireAuthOrLocal, async (req, res) => {
|
|
356
368
|
try {
|
|
357
369
|
const response = await throttledFetch('https://openrouter.ai/api/v1/models');
|
|
358
370
|
if (response.ok) {
|