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.
Files changed (232) hide show
  1. package/README.md +122 -122
  2. package/dist/assets/Badge-Bsy2H_p2.js +1 -0
  3. package/dist/assets/GraphPanel-D4X3faxA.js +47 -0
  4. package/dist/assets/{Input-Bgp734xs.js → Input-Dyb88Erk.js} +1 -1
  5. package/dist/assets/KnowledgeTab-BccWz7Np.js +5 -0
  6. package/dist/assets/MemoryTab-Y_66cE01.js +16 -0
  7. package/dist/assets/QualificationTab-Dm9dEIpM.js +1 -0
  8. package/dist/assets/ReviewTab-BrfXSyyf.js +104 -0
  9. package/dist/assets/{Section-DoJrmytO.js → Section-68XDCFTl.js} +1 -1
  10. package/dist/assets/TestTab-CLKRT63X.js +42 -0
  11. package/dist/assets/ToolsTab-xumi9Uds.js +1 -0
  12. package/dist/assets/icons-CS8RUPBi.js +1 -0
  13. package/dist/assets/index-B2bm0161.css +1 -0
  14. package/dist/assets/index-C626nWuA.js +422 -0
  15. package/dist/assets/services-BDk6yY4o.js +369 -0
  16. package/dist/index.html +18 -18
  17. package/dist-server/bin/modular-mcp.js +1 -0
  18. package/dist-server/server/index.d.ts.map +1 -1
  19. package/dist-server/server/index.js +34 -0
  20. package/dist-server/server/mcp/manager.d.ts +3 -0
  21. package/dist-server/server/mcp/manager.d.ts.map +1 -1
  22. package/dist-server/server/mcp/manager.js +80 -5
  23. package/dist-server/server/migrations/index.d.ts +11 -0
  24. package/dist-server/server/migrations/index.d.ts.map +1 -0
  25. package/dist-server/server/migrations/index.js +57 -0
  26. package/dist-server/server/routes/agents.d.ts.map +1 -1
  27. package/dist-server/server/routes/agents.js +27 -0
  28. package/dist-server/server/routes/analytics.d.ts +3 -0
  29. package/dist-server/server/routes/analytics.d.ts.map +1 -0
  30. package/dist-server/server/routes/analytics.js +24 -0
  31. package/dist-server/server/routes/cache.d.ts +3 -0
  32. package/dist-server/server/routes/cache.d.ts.map +1 -0
  33. package/dist-server/server/routes/cache.js +55 -0
  34. package/dist-server/server/routes/connectors/airtable.d.ts +7 -0
  35. package/dist-server/server/routes/connectors/airtable.d.ts.map +1 -0
  36. package/dist-server/server/routes/connectors/airtable.js +119 -0
  37. package/dist-server/server/routes/connectors/confluence.d.ts +7 -0
  38. package/dist-server/server/routes/connectors/confluence.d.ts.map +1 -0
  39. package/dist-server/server/routes/connectors/confluence.js +176 -0
  40. package/dist-server/server/routes/connectors/github.d.ts +7 -0
  41. package/dist-server/server/routes/connectors/github.d.ts.map +1 -0
  42. package/dist-server/server/routes/connectors/github.js +195 -0
  43. package/dist-server/server/routes/connectors/gmail.d.ts +7 -0
  44. package/dist-server/server/routes/connectors/gmail.d.ts.map +1 -0
  45. package/dist-server/server/routes/connectors/gmail.js +115 -0
  46. package/dist-server/server/routes/connectors/google-docs.d.ts +10 -0
  47. package/dist-server/server/routes/connectors/google-docs.d.ts.map +1 -0
  48. package/dist-server/server/routes/connectors/google-docs.js +165 -0
  49. package/dist-server/server/routes/connectors/google-drive.d.ts +7 -0
  50. package/dist-server/server/routes/connectors/google-drive.d.ts.map +1 -0
  51. package/dist-server/server/routes/connectors/google-drive.js +163 -0
  52. package/dist-server/server/routes/connectors/google-sheets.d.ts +7 -0
  53. package/dist-server/server/routes/connectors/google-sheets.d.ts.map +1 -0
  54. package/dist-server/server/routes/connectors/google-sheets.js +90 -0
  55. package/dist-server/server/routes/connectors/hubspot.d.ts +7 -0
  56. package/dist-server/server/routes/connectors/hubspot.d.ts.map +1 -0
  57. package/dist-server/server/routes/connectors/hubspot.js +134 -0
  58. package/dist-server/server/routes/connectors/index.d.ts +6 -0
  59. package/dist-server/server/routes/connectors/index.d.ts.map +1 -0
  60. package/dist-server/server/routes/connectors/index.js +38 -0
  61. package/dist-server/server/routes/connectors/jira.d.ts +7 -0
  62. package/dist-server/server/routes/connectors/jira.d.ts.map +1 -0
  63. package/dist-server/server/routes/connectors/jira.js +151 -0
  64. package/dist-server/server/routes/connectors/linear.d.ts +7 -0
  65. package/dist-server/server/routes/connectors/linear.d.ts.map +1 -0
  66. package/dist-server/server/routes/connectors/linear.js +154 -0
  67. package/dist-server/server/routes/connectors/notion.d.ts +10 -0
  68. package/dist-server/server/routes/connectors/notion.d.ts.map +1 -0
  69. package/dist-server/server/routes/connectors/notion.js +201 -0
  70. package/dist-server/server/routes/connectors/plane.d.ts +10 -0
  71. package/dist-server/server/routes/connectors/plane.d.ts.map +1 -0
  72. package/dist-server/server/routes/connectors/plane.js +189 -0
  73. package/dist-server/server/routes/connectors/shared.d.ts +25 -0
  74. package/dist-server/server/routes/connectors/shared.d.ts.map +1 -0
  75. package/dist-server/server/routes/connectors/shared.js +202 -0
  76. package/dist-server/server/routes/connectors/slack.d.ts +7 -0
  77. package/dist-server/server/routes/connectors/slack.d.ts.map +1 -0
  78. package/dist-server/server/routes/connectors/slack.js +153 -0
  79. package/dist-server/server/routes/connectors.d.ts.map +1 -1
  80. package/dist-server/server/routes/connectors.js +47 -17
  81. package/dist-server/server/routes/cost.d.ts +3 -0
  82. package/dist-server/server/routes/cost.d.ts.map +1 -0
  83. package/dist-server/server/routes/cost.js +113 -0
  84. package/dist-server/server/routes/graph.d.ts +11 -0
  85. package/dist-server/server/routes/graph.d.ts.map +1 -0
  86. package/dist-server/server/routes/graph.js +213 -0
  87. package/dist-server/server/routes/lessons.d.ts +3 -0
  88. package/dist-server/server/routes/lessons.d.ts.map +1 -0
  89. package/dist-server/server/routes/lessons.js +160 -0
  90. package/dist-server/server/routes/llm.d.ts.map +1 -1
  91. package/dist-server/server/routes/llm.js +85 -18
  92. package/dist-server/server/routes/memory.d.ts.map +1 -1
  93. package/dist-server/server/routes/memory.js +31 -0
  94. package/dist-server/server/routes/metaprompt-v2.d.ts +3 -0
  95. package/dist-server/server/routes/metaprompt-v2.d.ts.map +1 -0
  96. package/dist-server/server/routes/metaprompt-v2.js +104 -0
  97. package/dist-server/server/routes/qualification.d.ts.map +1 -1
  98. package/dist-server/server/routes/qualification.js +342 -334
  99. package/dist-server/server/routes/repo-index.d.ts.map +1 -1
  100. package/dist-server/server/routes/repo-index.js +7 -0
  101. package/dist-server/server/routes/skills-search.d.ts.map +1 -1
  102. package/dist-server/server/routes/skills-search.js +192 -26
  103. package/dist-server/server/routes/tool-analytics.d.ts +3 -0
  104. package/dist-server/server/routes/tool-analytics.d.ts.map +1 -0
  105. package/dist-server/server/routes/tool-analytics.js +47 -0
  106. package/dist-server/server/services/adapters/hindsightAdapter.d.ts +28 -0
  107. package/dist-server/server/services/adapters/hindsightAdapter.d.ts.map +1 -0
  108. package/dist-server/server/services/adapters/hindsightAdapter.js +63 -0
  109. package/dist-server/server/services/adapters/postgresAdapter.js +30 -30
  110. package/dist-server/server/services/adapters/sqliteAdapter.d.ts +1 -0
  111. package/dist-server/server/services/adapters/sqliteAdapter.d.ts.map +1 -1
  112. package/dist-server/server/services/adapters/sqliteAdapter.js +66 -36
  113. package/dist-server/server/services/agentStore.d.ts +2 -1
  114. package/dist-server/server/services/agentStore.d.ts.map +1 -1
  115. package/dist-server/server/services/agentStore.js +2 -1
  116. package/dist-server/server/services/correctionDetector.d.ts +22 -0
  117. package/dist-server/server/services/correctionDetector.d.ts.map +1 -0
  118. package/dist-server/server/services/correctionDetector.js +91 -0
  119. package/dist-server/server/services/credentialStore.d.ts +10 -0
  120. package/dist-server/server/services/credentialStore.d.ts.map +1 -0
  121. package/dist-server/server/services/credentialStore.js +123 -0
  122. package/dist-server/server/services/hindsightClient.d.ts +15 -0
  123. package/dist-server/server/services/hindsightClient.d.ts.map +1 -0
  124. package/dist-server/server/services/hindsightClient.js +48 -0
  125. package/dist-server/server/services/lessonExtractor.d.ts +21 -0
  126. package/dist-server/server/services/lessonExtractor.d.ts.map +1 -0
  127. package/dist-server/server/services/lessonExtractor.js +92 -0
  128. package/dist-server/server/services/repoIndexer.d.ts +7 -1
  129. package/dist-server/server/services/repoIndexer.d.ts.map +1 -1
  130. package/dist-server/server/services/repoIndexer.js +295 -94
  131. package/dist-server/server/services/responseCache.d.ts +24 -0
  132. package/dist-server/server/services/responseCache.d.ts.map +1 -0
  133. package/dist-server/server/services/responseCache.js +163 -0
  134. package/dist-server/server/services/sqliteStore.d.ts +72 -0
  135. package/dist-server/server/services/sqliteStore.d.ts.map +1 -1
  136. package/dist-server/server/services/sqliteStore.js +291 -13
  137. package/dist-server/src/config.d.ts +2 -0
  138. package/dist-server/src/config.d.ts.map +1 -0
  139. package/dist-server/src/config.js +3 -0
  140. package/dist-server/src/graph/db.d.ts +46 -0
  141. package/dist-server/src/graph/db.d.ts.map +1 -0
  142. package/dist-server/src/graph/db.js +241 -0
  143. package/dist-server/src/graph/extractors/code.d.ts +12 -0
  144. package/dist-server/src/graph/extractors/code.d.ts.map +1 -0
  145. package/dist-server/src/graph/extractors/code.js +239 -0
  146. package/dist-server/src/graph/extractors/cross-type.d.ts +16 -0
  147. package/dist-server/src/graph/extractors/cross-type.d.ts.map +1 -0
  148. package/dist-server/src/graph/extractors/cross-type.js +67 -0
  149. package/dist-server/src/graph/extractors/markdown.d.ts +29 -0
  150. package/dist-server/src/graph/extractors/markdown.d.ts.map +1 -0
  151. package/dist-server/src/graph/extractors/markdown.js +224 -0
  152. package/dist-server/src/graph/extractors/yaml.d.ts +15 -0
  153. package/dist-server/src/graph/extractors/yaml.d.ts.map +1 -0
  154. package/dist-server/src/graph/extractors/yaml.js +104 -0
  155. package/dist-server/src/graph/index.d.ts +62 -0
  156. package/dist-server/src/graph/index.d.ts.map +1 -0
  157. package/dist-server/src/graph/index.js +67 -0
  158. package/dist-server/src/graph/packer.d.ts +19 -0
  159. package/dist-server/src/graph/packer.d.ts.map +1 -0
  160. package/dist-server/src/graph/packer.js +134 -0
  161. package/dist-server/src/graph/resolver.d.ts +12 -0
  162. package/dist-server/src/graph/resolver.d.ts.map +1 -0
  163. package/dist-server/src/graph/resolver.js +81 -0
  164. package/dist-server/src/graph/scanner.d.ts +34 -0
  165. package/dist-server/src/graph/scanner.d.ts.map +1 -0
  166. package/dist-server/src/graph/scanner.js +252 -0
  167. package/dist-server/src/graph/traverser.d.ts +17 -0
  168. package/dist-server/src/graph/traverser.d.ts.map +1 -0
  169. package/dist-server/src/graph/traverser.js +185 -0
  170. package/dist-server/src/graph/types.d.ts +117 -0
  171. package/dist-server/src/graph/types.d.ts.map +1 -0
  172. package/dist-server/src/graph/types.js +63 -0
  173. package/dist-server/src/metaprompt/v2/assembler.d.ts +3 -0
  174. package/dist-server/src/metaprompt/v2/assembler.d.ts.map +1 -0
  175. package/dist-server/src/metaprompt/v2/assembler.js +261 -0
  176. package/dist-server/src/metaprompt/v2/context-strategist.d.ts +3 -0
  177. package/dist-server/src/metaprompt/v2/context-strategist.d.ts.map +1 -0
  178. package/dist-server/src/metaprompt/v2/context-strategist.js +173 -0
  179. package/dist-server/src/metaprompt/v2/evaluator.d.ts +3 -0
  180. package/dist-server/src/metaprompt/v2/evaluator.d.ts.map +1 -0
  181. package/dist-server/src/metaprompt/v2/evaluator.js +281 -0
  182. package/dist-server/src/metaprompt/v2/index.d.ts +41 -0
  183. package/dist-server/src/metaprompt/v2/index.d.ts.map +1 -0
  184. package/dist-server/src/metaprompt/v2/index.js +90 -0
  185. package/dist-server/src/metaprompt/v2/parser.d.ts +3 -0
  186. package/dist-server/src/metaprompt/v2/parser.d.ts.map +1 -0
  187. package/dist-server/src/metaprompt/v2/parser.js +138 -0
  188. package/dist-server/src/metaprompt/v2/pattern-selector.d.ts +3 -0
  189. package/dist-server/src/metaprompt/v2/pattern-selector.d.ts.map +1 -0
  190. package/dist-server/src/metaprompt/v2/pattern-selector.js +154 -0
  191. package/dist-server/src/metaprompt/v2/researcher.d.ts +3 -0
  192. package/dist-server/src/metaprompt/v2/researcher.d.ts.map +1 -0
  193. package/dist-server/src/metaprompt/v2/researcher.js +194 -0
  194. package/dist-server/src/metaprompt/v2/tool-discovery.d.ts +74 -0
  195. package/dist-server/src/metaprompt/v2/tool-discovery.d.ts.map +1 -0
  196. package/dist-server/src/metaprompt/v2/tool-discovery.js +290 -0
  197. package/dist-server/src/metaprompt/v2/types.d.ts +154 -0
  198. package/dist-server/src/metaprompt/v2/types.d.ts.map +1 -0
  199. package/dist-server/src/metaprompt/v2/types.js +2 -0
  200. package/dist-server/src/services/contradictionDetector.js +1 -1
  201. package/dist-server/src/services/llmService.d.ts +61 -0
  202. package/dist-server/src/services/llmService.d.ts.map +1 -0
  203. package/dist-server/src/services/llmService.js +222 -0
  204. package/dist-server/src/store/knowledgeBase.d.ts +6 -1
  205. package/dist-server/src/store/knowledgeBase.d.ts.map +1 -1
  206. package/dist-server/src/store/knowledgeBase.js +0 -1
  207. package/dist-server/src/store/lessonStore.d.ts +26 -0
  208. package/dist-server/src/store/lessonStore.d.ts.map +1 -0
  209. package/dist-server/src/store/lessonStore.js +64 -0
  210. package/dist-server/src/store/mcp-registry.d.ts +29 -0
  211. package/dist-server/src/store/mcp-registry.d.ts.map +1 -0
  212. package/dist-server/src/store/mcp-registry.js +1303 -0
  213. package/dist-server/src/store/memoryStore.d.ts +12 -1
  214. package/dist-server/src/store/memoryStore.d.ts.map +1 -1
  215. package/dist-server/src/store/memoryStore.js +9 -0
  216. package/dist-server/src/types/registry.types.d.ts +13 -0
  217. package/dist-server/src/types/registry.types.d.ts.map +1 -0
  218. package/dist-server/src/types/registry.types.js +2 -0
  219. package/dist-server/tsconfig.server.tsbuildinfo +1 -1
  220. package/package.json +15 -1
  221. package/scripts/cleanup-worktrees.ps1 +29 -0
  222. package/dist/assets/Badge-22Ai0eyi.js +0 -1
  223. package/dist/assets/KnowledgeTab-DABxirZh.js +0 -4
  224. package/dist/assets/MemoryTab-DZeYElIT.js +0 -16
  225. package/dist/assets/QualificationTab-Dfpy3J30.js +0 -1
  226. package/dist/assets/ReviewTab-SD8lQuCc.js +0 -103
  227. package/dist/assets/TestTab-PDyMF8Fw.js +0 -33
  228. package/dist/assets/ToolsTab-B83qGCmG.js +0 -1
  229. package/dist/assets/icons-C2EV-le6.js +0 -1
  230. package/dist/assets/index-DkpMAxX7.css +0 -1
  231. package/dist/assets/index-q24ug5Qs.js +0 -143
  232. 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,3 @@
1
+ declare const router: import("express-serve-static-core").Router;
2
+ export default router;
3
+ //# sourceMappingURL=lessons.d.ts.map
@@ -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":"AAKA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAiSxB,eAAe,MAAM,CAAC"}
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 { provider: providerId, model, messages, temperature, maxTokens: rawMaxTokens, } = req.body;
97
- const maxTokens = rawMaxTokens
98
- ? Math.min(rawMaxTokens, MAX_TOKENS_LIMIT)
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 { provider: providerId, model, messages, tools, temperature, maxTokens: rawMaxTokens, } = req.body;
162
- const maxTokens = rawMaxTokens
163
- ? Math.min(rawMaxTokens, MAX_TOKENS_LIMIT)
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":"AASA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAmSxB,eAAe,MAAM,CAAC"}
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"}