modular-studio 1.0.5 → 1.1.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/README.md +122 -122
- package/dist/assets/Badge-Bsy2H_p2.js +1 -0
- package/dist/assets/GraphPanel-D4X3faxA.js +47 -0
- package/dist/assets/{Input-Bgp734xs.js → Input-Dyb88Erk.js} +1 -1
- package/dist/assets/KnowledgeTab-BccWz7Np.js +5 -0
- package/dist/assets/MemoryTab-Y_66cE01.js +16 -0
- package/dist/assets/QualificationTab-Dm9dEIpM.js +1 -0
- package/dist/assets/ReviewTab-BrfXSyyf.js +104 -0
- package/dist/assets/{Section-DoJrmytO.js → Section-68XDCFTl.js} +1 -1
- package/dist/assets/TestTab-CLKRT63X.js +42 -0
- package/dist/assets/ToolsTab-xumi9Uds.js +1 -0
- package/dist/assets/icons-CS8RUPBi.js +1 -0
- package/dist/assets/index-B2bm0161.css +1 -0
- package/dist/assets/index-C626nWuA.js +422 -0
- package/dist/assets/services-BDk6yY4o.js +369 -0
- package/dist/index.html +18 -18
- package/dist-server/bin/modular-mcp.js +1 -0
- package/dist-server/server/index.d.ts.map +1 -1
- package/dist-server/server/index.js +34 -0
- package/dist-server/server/mcp/manager.d.ts +3 -0
- package/dist-server/server/mcp/manager.d.ts.map +1 -1
- package/dist-server/server/mcp/manager.js +80 -5
- package/dist-server/server/migrations/index.d.ts +11 -0
- package/dist-server/server/migrations/index.d.ts.map +1 -0
- package/dist-server/server/migrations/index.js +57 -0
- package/dist-server/server/routes/agents.d.ts.map +1 -1
- package/dist-server/server/routes/agents.js +27 -0
- package/dist-server/server/routes/analytics.d.ts +3 -0
- package/dist-server/server/routes/analytics.d.ts.map +1 -0
- package/dist-server/server/routes/analytics.js +24 -0
- package/dist-server/server/routes/cache.d.ts +3 -0
- package/dist-server/server/routes/cache.d.ts.map +1 -0
- package/dist-server/server/routes/cache.js +55 -0
- package/dist-server/server/routes/connectors/airtable.d.ts +7 -0
- package/dist-server/server/routes/connectors/airtable.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/airtable.js +119 -0
- package/dist-server/server/routes/connectors/confluence.d.ts +7 -0
- package/dist-server/server/routes/connectors/confluence.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/confluence.js +176 -0
- package/dist-server/server/routes/connectors/github.d.ts +7 -0
- package/dist-server/server/routes/connectors/github.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/github.js +195 -0
- package/dist-server/server/routes/connectors/gmail.d.ts +7 -0
- package/dist-server/server/routes/connectors/gmail.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/gmail.js +115 -0
- package/dist-server/server/routes/connectors/google-docs.d.ts +10 -0
- package/dist-server/server/routes/connectors/google-docs.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/google-docs.js +165 -0
- package/dist-server/server/routes/connectors/google-drive.d.ts +7 -0
- package/dist-server/server/routes/connectors/google-drive.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/google-drive.js +163 -0
- package/dist-server/server/routes/connectors/google-sheets.d.ts +7 -0
- package/dist-server/server/routes/connectors/google-sheets.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/google-sheets.js +90 -0
- package/dist-server/server/routes/connectors/hubspot.d.ts +7 -0
- package/dist-server/server/routes/connectors/hubspot.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/hubspot.js +134 -0
- package/dist-server/server/routes/connectors/index.d.ts +6 -0
- package/dist-server/server/routes/connectors/index.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/index.js +38 -0
- package/dist-server/server/routes/connectors/jira.d.ts +7 -0
- package/dist-server/server/routes/connectors/jira.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/jira.js +151 -0
- package/dist-server/server/routes/connectors/linear.d.ts +7 -0
- package/dist-server/server/routes/connectors/linear.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/linear.js +154 -0
- package/dist-server/server/routes/connectors/notion.d.ts +10 -0
- package/dist-server/server/routes/connectors/notion.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/notion.js +201 -0
- package/dist-server/server/routes/connectors/plane.d.ts +10 -0
- package/dist-server/server/routes/connectors/plane.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/plane.js +189 -0
- package/dist-server/server/routes/connectors/shared.d.ts +25 -0
- package/dist-server/server/routes/connectors/shared.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/shared.js +202 -0
- package/dist-server/server/routes/connectors/slack.d.ts +7 -0
- package/dist-server/server/routes/connectors/slack.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/slack.js +153 -0
- package/dist-server/server/routes/connectors.d.ts.map +1 -1
- package/dist-server/server/routes/connectors.js +47 -17
- package/dist-server/server/routes/cost.d.ts +3 -0
- package/dist-server/server/routes/cost.d.ts.map +1 -0
- package/dist-server/server/routes/cost.js +113 -0
- package/dist-server/server/routes/graph.d.ts +11 -0
- package/dist-server/server/routes/graph.d.ts.map +1 -0
- package/dist-server/server/routes/graph.js +213 -0
- package/dist-server/server/routes/lessons.d.ts +3 -0
- package/dist-server/server/routes/lessons.d.ts.map +1 -0
- package/dist-server/server/routes/lessons.js +160 -0
- package/dist-server/server/routes/llm.d.ts.map +1 -1
- package/dist-server/server/routes/llm.js +85 -18
- package/dist-server/server/routes/memory.d.ts.map +1 -1
- package/dist-server/server/routes/memory.js +31 -0
- package/dist-server/server/routes/metaprompt-v2.d.ts +3 -0
- package/dist-server/server/routes/metaprompt-v2.d.ts.map +1 -0
- package/dist-server/server/routes/metaprompt-v2.js +104 -0
- package/dist-server/server/routes/qualification.d.ts.map +1 -1
- package/dist-server/server/routes/qualification.js +342 -334
- package/dist-server/server/routes/repo-index.d.ts.map +1 -1
- package/dist-server/server/routes/repo-index.js +7 -0
- package/dist-server/server/routes/skills-search.d.ts.map +1 -1
- package/dist-server/server/routes/skills-search.js +192 -26
- package/dist-server/server/routes/tool-analytics.d.ts +3 -0
- package/dist-server/server/routes/tool-analytics.d.ts.map +1 -0
- package/dist-server/server/routes/tool-analytics.js +47 -0
- package/dist-server/server/services/adapters/hindsightAdapter.d.ts +28 -0
- package/dist-server/server/services/adapters/hindsightAdapter.d.ts.map +1 -0
- package/dist-server/server/services/adapters/hindsightAdapter.js +63 -0
- package/dist-server/server/services/adapters/postgresAdapter.js +30 -30
- package/dist-server/server/services/adapters/sqliteAdapter.d.ts +1 -0
- package/dist-server/server/services/adapters/sqliteAdapter.d.ts.map +1 -1
- package/dist-server/server/services/adapters/sqliteAdapter.js +66 -36
- package/dist-server/server/services/agentStore.d.ts +2 -1
- package/dist-server/server/services/agentStore.d.ts.map +1 -1
- package/dist-server/server/services/agentStore.js +2 -1
- package/dist-server/server/services/correctionDetector.d.ts +22 -0
- package/dist-server/server/services/correctionDetector.d.ts.map +1 -0
- package/dist-server/server/services/correctionDetector.js +91 -0
- package/dist-server/server/services/credentialStore.d.ts +10 -0
- package/dist-server/server/services/credentialStore.d.ts.map +1 -0
- package/dist-server/server/services/credentialStore.js +123 -0
- package/dist-server/server/services/hindsightClient.d.ts +15 -0
- package/dist-server/server/services/hindsightClient.d.ts.map +1 -0
- package/dist-server/server/services/hindsightClient.js +48 -0
- package/dist-server/server/services/lessonExtractor.d.ts +21 -0
- package/dist-server/server/services/lessonExtractor.d.ts.map +1 -0
- package/dist-server/server/services/lessonExtractor.js +92 -0
- package/dist-server/server/services/repoIndexer.d.ts +7 -1
- package/dist-server/server/services/repoIndexer.d.ts.map +1 -1
- package/dist-server/server/services/repoIndexer.js +295 -94
- package/dist-server/server/services/responseCache.d.ts +24 -0
- package/dist-server/server/services/responseCache.d.ts.map +1 -0
- package/dist-server/server/services/responseCache.js +163 -0
- package/dist-server/server/services/sqliteStore.d.ts +72 -0
- package/dist-server/server/services/sqliteStore.d.ts.map +1 -1
- package/dist-server/server/services/sqliteStore.js +291 -13
- package/dist-server/src/config.d.ts +2 -0
- package/dist-server/src/config.d.ts.map +1 -0
- package/dist-server/src/config.js +3 -0
- package/dist-server/src/graph/db.d.ts +46 -0
- package/dist-server/src/graph/db.d.ts.map +1 -0
- package/dist-server/src/graph/db.js +241 -0
- package/dist-server/src/graph/extractors/code.d.ts +12 -0
- package/dist-server/src/graph/extractors/code.d.ts.map +1 -0
- package/dist-server/src/graph/extractors/code.js +239 -0
- package/dist-server/src/graph/extractors/cross-type.d.ts +16 -0
- package/dist-server/src/graph/extractors/cross-type.d.ts.map +1 -0
- package/dist-server/src/graph/extractors/cross-type.js +67 -0
- package/dist-server/src/graph/extractors/markdown.d.ts +29 -0
- package/dist-server/src/graph/extractors/markdown.d.ts.map +1 -0
- package/dist-server/src/graph/extractors/markdown.js +224 -0
- package/dist-server/src/graph/extractors/yaml.d.ts +15 -0
- package/dist-server/src/graph/extractors/yaml.d.ts.map +1 -0
- package/dist-server/src/graph/extractors/yaml.js +104 -0
- package/dist-server/src/graph/index.d.ts +62 -0
- package/dist-server/src/graph/index.d.ts.map +1 -0
- package/dist-server/src/graph/index.js +67 -0
- package/dist-server/src/graph/packer.d.ts +19 -0
- package/dist-server/src/graph/packer.d.ts.map +1 -0
- package/dist-server/src/graph/packer.js +134 -0
- package/dist-server/src/graph/resolver.d.ts +12 -0
- package/dist-server/src/graph/resolver.d.ts.map +1 -0
- package/dist-server/src/graph/resolver.js +81 -0
- package/dist-server/src/graph/scanner.d.ts +34 -0
- package/dist-server/src/graph/scanner.d.ts.map +1 -0
- package/dist-server/src/graph/scanner.js +252 -0
- package/dist-server/src/graph/traverser.d.ts +17 -0
- package/dist-server/src/graph/traverser.d.ts.map +1 -0
- package/dist-server/src/graph/traverser.js +185 -0
- package/dist-server/src/graph/types.d.ts +117 -0
- package/dist-server/src/graph/types.d.ts.map +1 -0
- package/dist-server/src/graph/types.js +63 -0
- package/dist-server/src/metaprompt/v2/assembler.d.ts +3 -0
- package/dist-server/src/metaprompt/v2/assembler.d.ts.map +1 -0
- package/dist-server/src/metaprompt/v2/assembler.js +261 -0
- package/dist-server/src/metaprompt/v2/context-strategist.d.ts +3 -0
- package/dist-server/src/metaprompt/v2/context-strategist.d.ts.map +1 -0
- package/dist-server/src/metaprompt/v2/context-strategist.js +173 -0
- package/dist-server/src/metaprompt/v2/evaluator.d.ts +3 -0
- package/dist-server/src/metaprompt/v2/evaluator.d.ts.map +1 -0
- package/dist-server/src/metaprompt/v2/evaluator.js +281 -0
- package/dist-server/src/metaprompt/v2/index.d.ts +41 -0
- package/dist-server/src/metaprompt/v2/index.d.ts.map +1 -0
- package/dist-server/src/metaprompt/v2/index.js +90 -0
- package/dist-server/src/metaprompt/v2/parser.d.ts +3 -0
- package/dist-server/src/metaprompt/v2/parser.d.ts.map +1 -0
- package/dist-server/src/metaprompt/v2/parser.js +138 -0
- package/dist-server/src/metaprompt/v2/pattern-selector.d.ts +3 -0
- package/dist-server/src/metaprompt/v2/pattern-selector.d.ts.map +1 -0
- package/dist-server/src/metaprompt/v2/pattern-selector.js +154 -0
- package/dist-server/src/metaprompt/v2/researcher.d.ts +3 -0
- package/dist-server/src/metaprompt/v2/researcher.d.ts.map +1 -0
- package/dist-server/src/metaprompt/v2/researcher.js +194 -0
- package/dist-server/src/metaprompt/v2/tool-discovery.d.ts +74 -0
- package/dist-server/src/metaprompt/v2/tool-discovery.d.ts.map +1 -0
- package/dist-server/src/metaprompt/v2/tool-discovery.js +290 -0
- package/dist-server/src/metaprompt/v2/types.d.ts +154 -0
- package/dist-server/src/metaprompt/v2/types.d.ts.map +1 -0
- package/dist-server/src/metaprompt/v2/types.js +2 -0
- package/dist-server/src/services/contradictionDetector.js +1 -1
- package/dist-server/src/services/llmService.d.ts +61 -0
- package/dist-server/src/services/llmService.d.ts.map +1 -0
- package/dist-server/src/services/llmService.js +222 -0
- package/dist-server/src/store/knowledgeBase.d.ts +6 -1
- package/dist-server/src/store/knowledgeBase.d.ts.map +1 -1
- package/dist-server/src/store/knowledgeBase.js +0 -1
- package/dist-server/src/store/lessonStore.d.ts +26 -0
- package/dist-server/src/store/lessonStore.d.ts.map +1 -0
- package/dist-server/src/store/lessonStore.js +64 -0
- package/dist-server/src/store/mcp-registry.d.ts +29 -0
- package/dist-server/src/store/mcp-registry.d.ts.map +1 -0
- package/dist-server/src/store/mcp-registry.js +1303 -0
- package/dist-server/src/store/memoryStore.d.ts +12 -1
- package/dist-server/src/store/memoryStore.d.ts.map +1 -1
- package/dist-server/src/store/memoryStore.js +9 -0
- package/dist-server/src/types/registry.types.d.ts +13 -0
- package/dist-server/src/types/registry.types.d.ts.map +1 -0
- package/dist-server/src/types/registry.types.js +2 -0
- package/dist-server/tsconfig.server.tsbuildinfo +1 -1
- package/package.json +15 -1
- package/scripts/cleanup-worktrees.ps1 +29 -0
- package/dist/assets/Badge-22Ai0eyi.js +0 -1
- package/dist/assets/KnowledgeTab-DABxirZh.js +0 -4
- package/dist/assets/MemoryTab-DZeYElIT.js +0 -16
- package/dist/assets/QualificationTab-Dfpy3J30.js +0 -1
- package/dist/assets/ReviewTab-SD8lQuCc.js +0 -103
- package/dist/assets/TestTab-PDyMF8Fw.js +0 -33
- package/dist/assets/ToolsTab-B83qGCmG.js +0 -1
- package/dist/assets/icons-C2EV-le6.js +0 -1
- package/dist/assets/index-DkpMAxX7.css +0 -1
- package/dist/assets/index-q24ug5Qs.js +0 -143
- package/dist/assets/services-BaKotDf0.js +0 -343
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cost.d.ts","sourceRoot":"","sources":["../../../server/routes/cost.ts"],"names":[],"mappings":"AAYA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA2GxB,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { saveCostRecord, getCostHistory, getTotalSpent, getBudgetConfig, setBudgetConfig } from '../services/sqliteStore.js';
|
|
4
|
+
function classifyModel(modelName) {
|
|
5
|
+
const n = modelName.toLowerCase();
|
|
6
|
+
if (/haiku|4o-mini|mini|flash|nano/.test(n))
|
|
7
|
+
return 'haiku';
|
|
8
|
+
if (/opus|gpt-4\.5|gemini-ultra|r1/.test(n))
|
|
9
|
+
return 'opus';
|
|
10
|
+
return 'sonnet';
|
|
11
|
+
}
|
|
12
|
+
const router = Router();
|
|
13
|
+
/* ── GET /:agentId/history ── */
|
|
14
|
+
router.get('/:agentId/history', async (req, res) => {
|
|
15
|
+
const agentId = String(req.params['agentId'] ?? '');
|
|
16
|
+
const limit = Math.min(parseInt(String(req.query['limit'] ?? '50'), 10) || 50, 200);
|
|
17
|
+
try {
|
|
18
|
+
const records = await getCostHistory(agentId, limit);
|
|
19
|
+
res.json({ status: 'ok', data: records });
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
res.status(500).json({ status: 'error', error: err instanceof Error ? err.message : String(err) });
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
/* ── GET /:agentId/summary ── */
|
|
26
|
+
router.get('/:agentId/summary', async (req, res) => {
|
|
27
|
+
const agentId = String(req.params['agentId'] ?? '');
|
|
28
|
+
try {
|
|
29
|
+
const records = await getCostHistory(agentId, 200);
|
|
30
|
+
const totalSpent = records.reduce((s, r) => s + r.costUsd, 0);
|
|
31
|
+
const runCount = records.length;
|
|
32
|
+
const avgCostPerRun = runCount > 0 ? totalSpent / runCount : 0;
|
|
33
|
+
// Model breakdown
|
|
34
|
+
const modelBreakdown = {};
|
|
35
|
+
for (const r of records) {
|
|
36
|
+
const tier = classifyModel(r.model);
|
|
37
|
+
modelBreakdown[tier] = modelBreakdown[tier] ?? { count: 0, cost: 0 };
|
|
38
|
+
modelBreakdown[tier].count += 1;
|
|
39
|
+
modelBreakdown[tier].cost += r.costUsd;
|
|
40
|
+
}
|
|
41
|
+
// Cache hit % (cached tokens / total input tokens)
|
|
42
|
+
const totalInput = records.reduce((s, r) => s + r.inputTokens, 0);
|
|
43
|
+
const totalCached = records.reduce((s, r) => s + r.cachedTokens, 0);
|
|
44
|
+
const cacheHitPct = totalInput > 0 ? totalCached / totalInput : 0;
|
|
45
|
+
res.json({ status: 'ok', data: { totalSpent, runCount, avgCostPerRun, modelBreakdown, cacheHitPct } });
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
res.status(500).json({ status: 'error', error: err instanceof Error ? err.message : String(err) });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
/* ── GET /:agentId/budget ── */
|
|
52
|
+
router.get('/:agentId/budget', async (req, res) => {
|
|
53
|
+
const agentId = String(req.params['agentId'] ?? '');
|
|
54
|
+
try {
|
|
55
|
+
const [config, totalSpent] = await Promise.all([getBudgetConfig(agentId), getTotalSpent(agentId)]);
|
|
56
|
+
res.json({ status: 'ok', data: { ...config, totalSpent } });
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
res.status(500).json({ status: 'error', error: err instanceof Error ? err.message : String(err) });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
/* ── PUT /:agentId/budget ── */
|
|
63
|
+
router.put('/:agentId/budget', async (req, res) => {
|
|
64
|
+
const agentId = String(req.params['agentId'] ?? '');
|
|
65
|
+
const { budgetLimit, preferredModel, maxModel } = req.body;
|
|
66
|
+
try {
|
|
67
|
+
await setBudgetConfig(agentId, { budgetLimit, preferredModel, maxModel });
|
|
68
|
+
const config = await getBudgetConfig(agentId);
|
|
69
|
+
res.json({ status: 'ok', data: config });
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
res.status(500).json({ status: 'error', error: err instanceof Error ? err.message : String(err) });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
const recordSchema = z.object({
|
|
76
|
+
model: z.string().min(1),
|
|
77
|
+
inputTokens: z.number().int().nonnegative(),
|
|
78
|
+
outputTokens: z.number().int().nonnegative(),
|
|
79
|
+
costUsd: z.number().nonnegative(),
|
|
80
|
+
cachedTokens: z.number().int().nonnegative().optional(),
|
|
81
|
+
});
|
|
82
|
+
const agentIdSchema = z.string().min(1);
|
|
83
|
+
/* ── POST /:agentId/record ── */
|
|
84
|
+
router.post('/:agentId/record', async (req, res) => {
|
|
85
|
+
const agentIdParsed = agentIdSchema.safeParse(req.params['agentId']);
|
|
86
|
+
if (!agentIdParsed.success) {
|
|
87
|
+
res.status(400).json({ status: 'error', error: 'agentId is required' });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const agentId = agentIdParsed.data;
|
|
91
|
+
const bodyParsed = recordSchema.safeParse(req.body);
|
|
92
|
+
if (!bodyParsed.success) {
|
|
93
|
+
res.status(400).json({ status: 'error', error: bodyParsed.error.issues.map(i => i.message).join(', ') });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const { model, inputTokens, outputTokens, costUsd, cachedTokens } = bodyParsed.data;
|
|
97
|
+
try {
|
|
98
|
+
await saveCostRecord({
|
|
99
|
+
agentId,
|
|
100
|
+
timestamp: new Date().toISOString(),
|
|
101
|
+
model,
|
|
102
|
+
inputTokens,
|
|
103
|
+
outputTokens,
|
|
104
|
+
costUsd,
|
|
105
|
+
cachedTokens: cachedTokens ?? 0,
|
|
106
|
+
});
|
|
107
|
+
res.json({ status: 'ok' });
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
res.status(500).json({ status: 'error', error: err instanceof Error ? err.message : String(err) });
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
export default router;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Graph API — Server Routes
|
|
3
|
+
*
|
|
4
|
+
* POST /api/graph/scan — Full or incremental scan
|
|
5
|
+
* POST /api/graph/query — Query → entry points → traverse → pack
|
|
6
|
+
* GET /api/graph/status — Graph stats
|
|
7
|
+
* GET /api/graph/file/:id — File detail with symbols + relations
|
|
8
|
+
*/
|
|
9
|
+
declare const router: import("express-serve-static-core").Router;
|
|
10
|
+
export default router;
|
|
11
|
+
//# sourceMappingURL=graph.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../../server/routes/graph.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,QAAA,MAAM,MAAM,4CAAW,CAAC;AAyNxB,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Graph API — Server Routes
|
|
3
|
+
*
|
|
4
|
+
* POST /api/graph/scan — Full or incremental scan
|
|
5
|
+
* POST /api/graph/query — Query → entry points → traverse → pack
|
|
6
|
+
* GET /api/graph/status — Graph stats
|
|
7
|
+
* GET /api/graph/file/:id — File detail with symbols + relations
|
|
8
|
+
*/
|
|
9
|
+
import { Router } from 'express';
|
|
10
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
11
|
+
import { join, relative } from 'node:path';
|
|
12
|
+
const router = Router();
|
|
13
|
+
// Lazy-init engine (avoid circular imports at module load)
|
|
14
|
+
let engine = null;
|
|
15
|
+
async function getEngine() {
|
|
16
|
+
if (!engine) {
|
|
17
|
+
const { ContextGraphEngine } = await import('../../src/graph/index.js');
|
|
18
|
+
engine = new ContextGraphEngine();
|
|
19
|
+
}
|
|
20
|
+
return engine;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Recursively list files in a directory.
|
|
24
|
+
*/
|
|
25
|
+
function listFiles(dir, rootDir) {
|
|
26
|
+
const results = [];
|
|
27
|
+
const IGNORE = /node_modules|\.git|dist|build|\.next|coverage|\.cache/;
|
|
28
|
+
const MAX_FILE_SIZE = 500_000; // 500KB max per file
|
|
29
|
+
try {
|
|
30
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
31
|
+
const fullPath = join(dir, entry.name);
|
|
32
|
+
if (IGNORE.test(entry.name))
|
|
33
|
+
continue;
|
|
34
|
+
if (entry.isDirectory()) {
|
|
35
|
+
results.push(...listFiles(fullPath, rootDir));
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
const ext = entry.name.split('.').pop()?.toLowerCase() ?? '';
|
|
39
|
+
if (!['ts', 'tsx', 'js', 'jsx', 'py', 'md', 'mdx', 'yml', 'yaml', 'json'].includes(ext))
|
|
40
|
+
continue;
|
|
41
|
+
try {
|
|
42
|
+
const stat = statSync(fullPath);
|
|
43
|
+
if (stat.size > MAX_FILE_SIZE)
|
|
44
|
+
continue;
|
|
45
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
46
|
+
results.push({
|
|
47
|
+
path: relative(rootDir, fullPath).replace(/\\/g, '/'),
|
|
48
|
+
content,
|
|
49
|
+
mtime: stat.mtimeMs,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
catch { /* skip unreadable files */ }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch { /* skip unreadable dirs */ }
|
|
57
|
+
return results;
|
|
58
|
+
}
|
|
59
|
+
// POST /api/graph/scan
|
|
60
|
+
router.post('/scan', async (req, res) => {
|
|
61
|
+
const { rootPath } = req.body;
|
|
62
|
+
if (!rootPath) {
|
|
63
|
+
res.status(400).json({ status: 'error', error: 'rootPath is required' });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Security: prevent scanning sensitive directories
|
|
67
|
+
const normalized = rootPath.replace(/\\/g, '/').toLowerCase();
|
|
68
|
+
if (normalized.includes('.ssh') || normalized.includes('.gnupg') || normalized.includes('.aws') ||
|
|
69
|
+
normalized.includes('/etc/') || normalized.includes('system32')) {
|
|
70
|
+
res.status(403).json({ status: 'error', error: 'Access denied: sensitive directory' });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const eng = await getEngine();
|
|
75
|
+
const files = listFiles(rootPath, rootPath);
|
|
76
|
+
const result = eng.scan(rootPath, files);
|
|
77
|
+
res.json({ status: 'ok', data: result });
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
81
|
+
res.status(500).json({ status: 'error', error: msg });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
// POST /api/graph/scan-sources — scan from pre-loaded content (any source)
|
|
85
|
+
router.post('/scan-sources', async (req, res) => {
|
|
86
|
+
const { sources } = req.body;
|
|
87
|
+
if (!sources || !Array.isArray(sources) || sources.length === 0) {
|
|
88
|
+
res.status(400).json({ status: 'error', error: 'sources array is required' });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const eng = await getEngine();
|
|
93
|
+
const result = eng.scan('(mixed sources)', sources);
|
|
94
|
+
res.json({ status: 'ok', data: result });
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
98
|
+
res.status(500).json({ status: 'error', error: msg });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
// POST /api/graph/query
|
|
102
|
+
router.post('/query', async (req, res) => {
|
|
103
|
+
const { query, tokenBudget, taskType } = req.body;
|
|
104
|
+
if (!query?.trim()) {
|
|
105
|
+
res.status(400).json({ status: 'error', error: 'query is required' });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const eng = await getEngine();
|
|
110
|
+
const packed = eng.query(query, tokenBudget ?? 100000, taskType);
|
|
111
|
+
const graph = eng.getGraph();
|
|
112
|
+
// Find entry points for the response
|
|
113
|
+
const { resolveEntryPoints } = await import('../../src/graph/resolver.js');
|
|
114
|
+
const entryPoints = resolveEntryPoints(query, graph);
|
|
115
|
+
res.json({
|
|
116
|
+
status: 'ok',
|
|
117
|
+
data: {
|
|
118
|
+
items: packed.items.map((it) => ({
|
|
119
|
+
path: it.file.path,
|
|
120
|
+
language: it.file.language,
|
|
121
|
+
depth: it.depth,
|
|
122
|
+
tokens: it.tokens,
|
|
123
|
+
relevance: it.relevance,
|
|
124
|
+
symbols: it.file.symbols.map((s) => ({ name: s.name, kind: s.kind, exported: s.isExported })),
|
|
125
|
+
})),
|
|
126
|
+
totalTokens: packed.totalTokens,
|
|
127
|
+
budgetUtilization: packed.budgetUtilization,
|
|
128
|
+
entryPoints: entryPoints.slice(0, 10).map((ep) => ({
|
|
129
|
+
fileId: ep.fileId,
|
|
130
|
+
symbol: ep.symbolName,
|
|
131
|
+
confidence: ep.confidence,
|
|
132
|
+
reason: ep.reason,
|
|
133
|
+
})),
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
139
|
+
res.status(500).json({ status: 'error', error: msg });
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
// GET /api/graph/data — full graph (nodes + relations) for visualization
|
|
143
|
+
router.get('/data', async (_req, res) => {
|
|
144
|
+
try {
|
|
145
|
+
const eng = await getEngine();
|
|
146
|
+
const graph = eng.getGraph();
|
|
147
|
+
const nodes = Array.from(graph.nodes.values());
|
|
148
|
+
const relations = graph.relations;
|
|
149
|
+
res.json({
|
|
150
|
+
status: 'ok',
|
|
151
|
+
data: { nodes, relations },
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
156
|
+
res.status(500).json({ status: 'error', error: msg });
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
// GET /api/graph/status
|
|
160
|
+
router.get('/status', async (_req, res) => {
|
|
161
|
+
try {
|
|
162
|
+
const eng = await getEngine();
|
|
163
|
+
const stats = eng.getStats();
|
|
164
|
+
res.json({ status: 'ok', data: stats });
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
168
|
+
res.status(500).json({ status: 'error', error: msg });
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
// GET /api/graph/file/:id
|
|
172
|
+
router.get('/file/:id', async (req, res) => {
|
|
173
|
+
try {
|
|
174
|
+
const eng = await getEngine();
|
|
175
|
+
const db = eng.getDB();
|
|
176
|
+
const node = db.getNode(req.params.id);
|
|
177
|
+
if (!node) {
|
|
178
|
+
res.status(404).json({ status: 'error', error: 'File not found' });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const outgoing = db.getOutgoing(node.id);
|
|
182
|
+
const incoming = db.getIncoming(node.id);
|
|
183
|
+
res.json({
|
|
184
|
+
status: 'ok',
|
|
185
|
+
data: {
|
|
186
|
+
id: node.id,
|
|
187
|
+
path: node.path,
|
|
188
|
+
language: node.language,
|
|
189
|
+
tokens: node.tokens,
|
|
190
|
+
symbols: node.symbols,
|
|
191
|
+
outgoing: outgoing.map((r) => ({
|
|
192
|
+
targetFile: r.targetFile,
|
|
193
|
+
targetPath: db.getNode(r.targetFile)?.path,
|
|
194
|
+
kind: r.kind,
|
|
195
|
+
weight: r.weight,
|
|
196
|
+
targetSymbol: r.targetSymbol,
|
|
197
|
+
})),
|
|
198
|
+
incoming: incoming.map((r) => ({
|
|
199
|
+
sourceFile: r.sourceFile,
|
|
200
|
+
sourcePath: db.getNode(r.sourceFile)?.path,
|
|
201
|
+
kind: r.kind,
|
|
202
|
+
weight: r.weight,
|
|
203
|
+
sourceSymbol: r.sourceSymbol,
|
|
204
|
+
})),
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
210
|
+
res.status(500).json({ status: 'error', error: msg });
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
export default router;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lessons.d.ts","sourceRoot":"","sources":["../../../server/routes/lessons.ts"],"names":[],"mappings":"AAQA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAiLxB,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { readConfig } from '../config.js';
|
|
4
|
+
import { detectCorrection } from '../services/correctionDetector.js';
|
|
5
|
+
import { extractLesson } from '../services/lessonExtractor.js';
|
|
6
|
+
import { saveInstinct, getInstincts, updateConfidence, deleteInstinct } from '../services/sqliteStore.js';
|
|
7
|
+
const router = Router();
|
|
8
|
+
function genId() {
|
|
9
|
+
return `lesson-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
10
|
+
}
|
|
11
|
+
/** GET /api/lessons/:agentId — all instincts for an agent */
|
|
12
|
+
router.get('/:agentId', async (req, res) => {
|
|
13
|
+
try {
|
|
14
|
+
const instincts = await getInstincts(String(req.params['agentId'] ?? ''));
|
|
15
|
+
res.json({ instincts });
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
res.status(500).json({ error: err instanceof Error ? err.message : 'Failed to fetch instincts' });
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
/** GET /api/lessons/:agentId/active — only instincts with confidence >= 0.5 */
|
|
22
|
+
router.get('/:agentId/active', async (req, res) => {
|
|
23
|
+
try {
|
|
24
|
+
const all = await getInstincts(String(req.params['agentId'] ?? ''));
|
|
25
|
+
const active = all.filter((i) => i.confidence >= 0.5 && i.status === 'approved');
|
|
26
|
+
res.json({ instincts: active });
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
res.status(500).json({ error: err instanceof Error ? err.message : 'Failed to fetch active instincts' });
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
/** PUT /api/lessons/:id/confidence — bump or set confidence */
|
|
33
|
+
router.put('/:id/confidence', async (req, res) => {
|
|
34
|
+
const { confidence } = req.body;
|
|
35
|
+
if (typeof confidence !== 'number' || confidence < 0 || confidence > 1) {
|
|
36
|
+
res.status(400).json({ error: 'confidence must be a number 0–1' });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
await updateConfidence(String(req.params['id'] ?? ''), confidence);
|
|
41
|
+
res.json({ ok: true });
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
res.status(500).json({ error: err instanceof Error ? err.message : 'Failed to update confidence' });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
/** DELETE /api/lessons/:id — delete an instinct */
|
|
48
|
+
router.delete('/:id', async (req, res) => {
|
|
49
|
+
try {
|
|
50
|
+
await deleteInstinct(String(req.params['id'] ?? ''));
|
|
51
|
+
res.json({ ok: true });
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
res.status(500).json({ error: err instanceof Error ? err.message : 'Failed to delete instinct' });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
/** POST /api/lessons/sync-batch — migrate lessons from localStorage to SQLite (dedup by id) */
|
|
58
|
+
router.post('/sync-batch', async (req, res) => {
|
|
59
|
+
const { lessons } = req.body;
|
|
60
|
+
if (!Array.isArray(lessons)) {
|
|
61
|
+
res.status(400).json({ error: 'lessons must be an array' });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
let saved = 0;
|
|
65
|
+
for (const l of lessons) {
|
|
66
|
+
if (!l.id || !l.agentId)
|
|
67
|
+
continue;
|
|
68
|
+
try {
|
|
69
|
+
const now = new Date().toISOString();
|
|
70
|
+
await saveInstinct({
|
|
71
|
+
id: l.id,
|
|
72
|
+
agentId: l.agentId,
|
|
73
|
+
trigger: (l.sourceUserMessage ?? '').slice(0, 500),
|
|
74
|
+
action: l.rule ?? '',
|
|
75
|
+
confidence: typeof l.confidence === 'number' ? l.confidence : 0.30,
|
|
76
|
+
domain: l.domain ?? 'general',
|
|
77
|
+
scope: 'agent',
|
|
78
|
+
evidence: Array.isArray(l.evidence) ? JSON.stringify(l.evidence) : '[]',
|
|
79
|
+
status: l.status ?? 'pending',
|
|
80
|
+
createdAt: l.createdAt ? new Date(l.createdAt).toISOString() : now,
|
|
81
|
+
lastSeenAt: l.lastSeenAt ?? now,
|
|
82
|
+
});
|
|
83
|
+
saved++;
|
|
84
|
+
}
|
|
85
|
+
catch { /* skip individual failures */ }
|
|
86
|
+
}
|
|
87
|
+
res.json({ ok: true, saved });
|
|
88
|
+
});
|
|
89
|
+
const extractSchema = z.object({
|
|
90
|
+
userMessage: z.string().min(1),
|
|
91
|
+
previousAssistant: z.string().optional(),
|
|
92
|
+
providerId: z.string().min(1),
|
|
93
|
+
model: z.string().min(1),
|
|
94
|
+
agentId: z.string().optional(),
|
|
95
|
+
});
|
|
96
|
+
/** POST /api/lessons/extract — extract lesson from correction, save to SQLite */
|
|
97
|
+
router.post('/extract', async (req, res) => {
|
|
98
|
+
const parsed = extractSchema.safeParse(req.body);
|
|
99
|
+
if (!parsed.success) {
|
|
100
|
+
res.status(400).json({ error: parsed.error.issues.map(i => i.message).join(', ') });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const { userMessage, previousAssistant, providerId, model, agentId } = parsed.data;
|
|
104
|
+
const correction = detectCorrection(userMessage, previousAssistant ?? '');
|
|
105
|
+
if (!correction) {
|
|
106
|
+
res.json({ lesson: null });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const config = readConfig();
|
|
110
|
+
const provider = config.providers.find((p) => p.id === providerId);
|
|
111
|
+
if (!provider) {
|
|
112
|
+
res.json({ lesson: null });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const extracted = await extractLesson(correction, { type: provider.type, baseUrl: provider.baseUrl, apiKey: provider.apiKey }, model);
|
|
117
|
+
if (!extracted) {
|
|
118
|
+
res.json({ lesson: null });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const now = new Date().toISOString();
|
|
122
|
+
const id = genId();
|
|
123
|
+
const effectiveAgentId = agentId ?? '';
|
|
124
|
+
// Save to SQLite
|
|
125
|
+
await saveInstinct({
|
|
126
|
+
id,
|
|
127
|
+
agentId: effectiveAgentId,
|
|
128
|
+
trigger: userMessage.slice(0, 500),
|
|
129
|
+
action: extracted.rule,
|
|
130
|
+
confidence: extracted.confidence ?? 0.30,
|
|
131
|
+
domain: extracted.domain ?? 'general',
|
|
132
|
+
scope: 'agent',
|
|
133
|
+
evidence: JSON.stringify([{ type: 'correction', timestamp: now, description: 'Extracted from user correction' }]),
|
|
134
|
+
status: 'pending',
|
|
135
|
+
createdAt: now,
|
|
136
|
+
lastSeenAt: now,
|
|
137
|
+
});
|
|
138
|
+
const lesson = {
|
|
139
|
+
id,
|
|
140
|
+
rule: extracted.rule,
|
|
141
|
+
category: extracted.category,
|
|
142
|
+
domain: extracted.domain ?? 'general',
|
|
143
|
+
confidence: extracted.confidence ?? 0.30,
|
|
144
|
+
agentId: effectiveAgentId,
|
|
145
|
+
sourceUserMessage: userMessage,
|
|
146
|
+
sourcePreviousAssistant: previousAssistant ?? '',
|
|
147
|
+
createdAt: Date.now(),
|
|
148
|
+
appliedCount: 0,
|
|
149
|
+
status: 'pending',
|
|
150
|
+
evidence: [{ type: 'correction', timestamp: now, description: 'Extracted from user correction' }],
|
|
151
|
+
lastSeenAt: now,
|
|
152
|
+
};
|
|
153
|
+
res.json({ lesson });
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
const message = err instanceof Error ? err.message : 'Extraction failed';
|
|
157
|
+
res.status(500).json({ error: message });
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
export default router;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"llm.d.ts","sourceRoot":"","sources":["../../../server/routes/llm.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"llm.d.ts","sourceRoot":"","sources":["../../../server/routes/llm.ts"],"names":[],"mappings":"AAMA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA2VxB,eAAe,MAAM,CAAC"}
|
|
@@ -1,7 +1,55 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
|
+
import { z } from 'zod';
|
|
2
3
|
import { readConfig } from '../config.js';
|
|
3
4
|
const router = Router();
|
|
4
5
|
const MAX_TOKENS_LIMIT = 32768; // Server-side cap to prevent cost attacks
|
|
6
|
+
// ── SSRF protection ──────────────────────────────────────────────────────────
|
|
7
|
+
function isPrivateHost(hostname) {
|
|
8
|
+
const h = hostname.toLowerCase();
|
|
9
|
+
// Loopback and well-known local hostnames
|
|
10
|
+
if (h === 'localhost' || h === '0.0.0.0')
|
|
11
|
+
return true;
|
|
12
|
+
// IPv6 loopback / unique-local (fc00::/7) / link-local (fe80::/10)
|
|
13
|
+
if (h === '::1' || h === '::' || h.startsWith('fc') || h.startsWith('fd') || h.startsWith('fe8') || h.startsWith('fe9') || h.startsWith('fea') || h.startsWith('feb'))
|
|
14
|
+
return true;
|
|
15
|
+
// IPv4 private/reserved ranges
|
|
16
|
+
const parts = h.split('.');
|
|
17
|
+
if (parts.length === 4) {
|
|
18
|
+
const [a, b] = parts.map(Number);
|
|
19
|
+
if (isNaN(a) || isNaN(b))
|
|
20
|
+
return false;
|
|
21
|
+
if (a === 127)
|
|
22
|
+
return true; // 127.0.0.0/8 loopback
|
|
23
|
+
if (a === 10)
|
|
24
|
+
return true; // 10.0.0.0/8 private
|
|
25
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
26
|
+
return true; // 172.16.0.0/12 private
|
|
27
|
+
if (a === 192 && b === 168)
|
|
28
|
+
return true; // 192.168.0.0/16 private
|
|
29
|
+
if (a === 169 && b === 254)
|
|
30
|
+
return true; // 169.254.0.0/16 link-local
|
|
31
|
+
if (a === 0)
|
|
32
|
+
return true; // 0.0.0.0/8 reserved
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
/** Returns an error string if the URL is invalid/blocked, null if allowed. */
|
|
37
|
+
function validateBaseUrl(url) {
|
|
38
|
+
let parsed;
|
|
39
|
+
try {
|
|
40
|
+
parsed = new URL(url);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return `Invalid URL format: "${url}"`;
|
|
44
|
+
}
|
|
45
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
46
|
+
return `Blocked URL scheme "${parsed.protocol}" — only http/https allowed`;
|
|
47
|
+
}
|
|
48
|
+
if (isPrivateHost(parsed.hostname)) {
|
|
49
|
+
return `Blocked: provider baseUrl "${parsed.hostname}" resolves to a private/reserved address (SSRF prevention)`;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
5
53
|
function normalizeBaseUrl(providerId, baseUrl) {
|
|
6
54
|
const trimmed = (baseUrl || '').trim().replace(/\/+$/, '');
|
|
7
55
|
if (!trimmed)
|
|
@@ -31,6 +79,12 @@ function resolveProvider(providerId, res) {
|
|
|
31
79
|
res.status(400).json(resp);
|
|
32
80
|
return null;
|
|
33
81
|
}
|
|
82
|
+
const ssrfError = validateBaseUrl(baseUrl);
|
|
83
|
+
if (ssrfError) {
|
|
84
|
+
const resp = { status: 'error', error: ssrfError };
|
|
85
|
+
res.status(400).json(resp);
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
34
88
|
// Infer provider type from id/baseUrl when type is missing or stale
|
|
35
89
|
const inferredType = provider.type === 'anthropic' ||
|
|
36
90
|
providerId.includes('anthropic') ||
|
|
@@ -92,19 +146,25 @@ function buildRequest(resolved, model, messages, opts) {
|
|
|
92
146
|
}),
|
|
93
147
|
};
|
|
94
148
|
}
|
|
149
|
+
// ── POST /chat — streaming SSE ──
|
|
150
|
+
const chatSchema = z.object({
|
|
151
|
+
provider: z.string().min(1),
|
|
152
|
+
model: z.string().min(1),
|
|
153
|
+
messages: z.array(z.object({ role: z.string(), content: z.unknown() })),
|
|
154
|
+
temperature: z.number().optional(),
|
|
155
|
+
maxTokens: z.number().int().positive().optional(),
|
|
156
|
+
});
|
|
95
157
|
router.post('/chat', async (req, res) => {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
: undefined;
|
|
100
|
-
if (!providerId || !model || !messages) {
|
|
101
|
-
const resp = {
|
|
102
|
-
status: 'error',
|
|
103
|
-
error: 'Missing required fields: provider, model, messages',
|
|
104
|
-
};
|
|
158
|
+
const parsed = chatSchema.safeParse(req.body);
|
|
159
|
+
if (!parsed.success) {
|
|
160
|
+
const resp = { status: 'error', error: parsed.error.issues.map(i => i.message).join(', ') };
|
|
105
161
|
res.status(400).json(resp);
|
|
106
162
|
return;
|
|
107
163
|
}
|
|
164
|
+
const { provider: providerId, model, messages, temperature, maxTokens: rawMaxTokens, } = parsed.data;
|
|
165
|
+
const maxTokens = rawMaxTokens
|
|
166
|
+
? Math.min(rawMaxTokens, MAX_TOKENS_LIMIT)
|
|
167
|
+
: undefined;
|
|
108
168
|
const resolved = resolveProvider(providerId, res);
|
|
109
169
|
if (!resolved)
|
|
110
170
|
return;
|
|
@@ -157,19 +217,26 @@ router.post('/chat', async (req, res) => {
|
|
|
157
217
|
}
|
|
158
218
|
}
|
|
159
219
|
});
|
|
220
|
+
// ── POST /chat-tools — non-streaming JSON (tool loop) ──
|
|
221
|
+
const chatToolsSchema = z.object({
|
|
222
|
+
provider: z.string().min(1),
|
|
223
|
+
model: z.string().min(1),
|
|
224
|
+
messages: z.array(z.unknown()),
|
|
225
|
+
tools: z.array(z.unknown()).optional(),
|
|
226
|
+
temperature: z.number().optional(),
|
|
227
|
+
maxTokens: z.number().int().positive().optional(),
|
|
228
|
+
});
|
|
160
229
|
router.post('/chat-tools', async (req, res) => {
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
: undefined;
|
|
165
|
-
if (!providerId || !model || !messages) {
|
|
166
|
-
const resp = {
|
|
167
|
-
status: 'error',
|
|
168
|
-
error: 'Missing required fields: provider, model, messages',
|
|
169
|
-
};
|
|
230
|
+
const parsed = chatToolsSchema.safeParse(req.body);
|
|
231
|
+
if (!parsed.success) {
|
|
232
|
+
const resp = { status: 'error', error: parsed.error.issues.map(i => i.message).join(', ') };
|
|
170
233
|
res.status(400).json(resp);
|
|
171
234
|
return;
|
|
172
235
|
}
|
|
236
|
+
const { provider: providerId, model, messages, tools, temperature, maxTokens: rawMaxTokens, } = parsed.data;
|
|
237
|
+
const maxTokens = rawMaxTokens
|
|
238
|
+
? Math.min(rawMaxTokens, MAX_TOKENS_LIMIT)
|
|
239
|
+
: undefined;
|
|
173
240
|
const resolved = resolveProvider(providerId, res);
|
|
174
241
|
if (!resolved)
|
|
175
242
|
return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../../server/routes/memory.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../../server/routes/memory.ts"],"names":[],"mappings":"AAUA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA8TxB,eAAe,MAAM,CAAC"}
|