modular-studio 1.0.6 → 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 (198) hide show
  1. package/dist/assets/Badge-Bsy2H_p2.js +1 -0
  2. package/dist/assets/GraphPanel-D4X3faxA.js +47 -0
  3. package/dist/assets/{Input-ndEGQSgx.js → Input-Dyb88Erk.js} +1 -1
  4. package/dist/assets/KnowledgeTab-BccWz7Np.js +5 -0
  5. package/dist/assets/MemoryTab-Y_66cE01.js +16 -0
  6. package/dist/assets/QualificationTab-Dm9dEIpM.js +1 -0
  7. package/dist/assets/ReviewTab-BrfXSyyf.js +104 -0
  8. package/dist/assets/{Section-CgmwAj_2.js → Section-68XDCFTl.js} +1 -1
  9. package/dist/assets/TestTab-CLKRT63X.js +42 -0
  10. package/dist/assets/{ToolsTab-C10Ulm8b.js → ToolsTab-xumi9Uds.js} +1 -1
  11. package/dist/assets/icons-CS8RUPBi.js +1 -0
  12. package/dist/assets/index-B2bm0161.css +1 -0
  13. package/dist/assets/index-C626nWuA.js +422 -0
  14. package/dist/assets/services-BDk6yY4o.js +369 -0
  15. package/dist/index.html +4 -4
  16. package/dist-server/bin/modular-mcp.js +1 -0
  17. package/dist-server/server/index.d.ts.map +1 -1
  18. package/dist-server/server/index.js +30 -0
  19. package/dist-server/server/mcp/manager.d.ts +3 -0
  20. package/dist-server/server/mcp/manager.d.ts.map +1 -1
  21. package/dist-server/server/mcp/manager.js +64 -3
  22. package/dist-server/server/migrations/index.d.ts +11 -0
  23. package/dist-server/server/migrations/index.d.ts.map +1 -0
  24. package/dist-server/server/migrations/index.js +57 -0
  25. package/dist-server/server/routes/analytics.d.ts +3 -0
  26. package/dist-server/server/routes/analytics.d.ts.map +1 -0
  27. package/dist-server/server/routes/analytics.js +24 -0
  28. package/dist-server/server/routes/connectors/airtable.d.ts +7 -0
  29. package/dist-server/server/routes/connectors/airtable.d.ts.map +1 -0
  30. package/dist-server/server/routes/connectors/airtable.js +119 -0
  31. package/dist-server/server/routes/connectors/confluence.d.ts +7 -0
  32. package/dist-server/server/routes/connectors/confluence.d.ts.map +1 -0
  33. package/dist-server/server/routes/connectors/confluence.js +176 -0
  34. package/dist-server/server/routes/connectors/github.d.ts +7 -0
  35. package/dist-server/server/routes/connectors/github.d.ts.map +1 -0
  36. package/dist-server/server/routes/connectors/github.js +195 -0
  37. package/dist-server/server/routes/connectors/gmail.d.ts +7 -0
  38. package/dist-server/server/routes/connectors/gmail.d.ts.map +1 -0
  39. package/dist-server/server/routes/connectors/gmail.js +115 -0
  40. package/dist-server/server/routes/connectors/google-docs.d.ts +10 -0
  41. package/dist-server/server/routes/connectors/google-docs.d.ts.map +1 -0
  42. package/dist-server/server/routes/connectors/google-docs.js +165 -0
  43. package/dist-server/server/routes/connectors/google-drive.d.ts +7 -0
  44. package/dist-server/server/routes/connectors/google-drive.d.ts.map +1 -0
  45. package/dist-server/server/routes/connectors/google-drive.js +163 -0
  46. package/dist-server/server/routes/connectors/google-sheets.d.ts +7 -0
  47. package/dist-server/server/routes/connectors/google-sheets.d.ts.map +1 -0
  48. package/dist-server/server/routes/connectors/google-sheets.js +90 -0
  49. package/dist-server/server/routes/connectors/hubspot.d.ts +7 -0
  50. package/dist-server/server/routes/connectors/hubspot.d.ts.map +1 -0
  51. package/dist-server/server/routes/connectors/hubspot.js +134 -0
  52. package/dist-server/server/routes/connectors/index.d.ts +6 -0
  53. package/dist-server/server/routes/connectors/index.d.ts.map +1 -0
  54. package/dist-server/server/routes/connectors/index.js +38 -0
  55. package/dist-server/server/routes/connectors/jira.d.ts +7 -0
  56. package/dist-server/server/routes/connectors/jira.d.ts.map +1 -0
  57. package/dist-server/server/routes/connectors/jira.js +151 -0
  58. package/dist-server/server/routes/connectors/linear.d.ts +7 -0
  59. package/dist-server/server/routes/connectors/linear.d.ts.map +1 -0
  60. package/dist-server/server/routes/connectors/linear.js +154 -0
  61. package/dist-server/server/routes/connectors/notion.d.ts +10 -0
  62. package/dist-server/server/routes/connectors/notion.d.ts.map +1 -0
  63. package/dist-server/server/routes/connectors/notion.js +201 -0
  64. package/dist-server/server/routes/connectors/plane.d.ts +10 -0
  65. package/dist-server/server/routes/connectors/plane.d.ts.map +1 -0
  66. package/dist-server/server/routes/connectors/plane.js +189 -0
  67. package/dist-server/server/routes/connectors/shared.d.ts +25 -0
  68. package/dist-server/server/routes/connectors/shared.d.ts.map +1 -0
  69. package/dist-server/server/routes/connectors/shared.js +202 -0
  70. package/dist-server/server/routes/connectors/slack.d.ts +7 -0
  71. package/dist-server/server/routes/connectors/slack.d.ts.map +1 -0
  72. package/dist-server/server/routes/connectors/slack.js +153 -0
  73. package/dist-server/server/routes/cost.d.ts +3 -0
  74. package/dist-server/server/routes/cost.d.ts.map +1 -0
  75. package/dist-server/server/routes/cost.js +113 -0
  76. package/dist-server/server/routes/graph.d.ts +11 -0
  77. package/dist-server/server/routes/graph.d.ts.map +1 -0
  78. package/dist-server/server/routes/graph.js +213 -0
  79. package/dist-server/server/routes/lessons.d.ts.map +1 -1
  80. package/dist-server/server/routes/lessons.js +119 -5
  81. package/dist-server/server/routes/llm.d.ts.map +1 -1
  82. package/dist-server/server/routes/llm.js +85 -18
  83. package/dist-server/server/routes/metaprompt-v2.d.ts +3 -0
  84. package/dist-server/server/routes/metaprompt-v2.d.ts.map +1 -0
  85. package/dist-server/server/routes/metaprompt-v2.js +104 -0
  86. package/dist-server/server/routes/qualification.d.ts.map +1 -1
  87. package/dist-server/server/routes/qualification.js +61 -11
  88. package/dist-server/server/routes/skills-search.d.ts.map +1 -1
  89. package/dist-server/server/routes/skills-search.js +10 -0
  90. package/dist-server/server/routes/tool-analytics.d.ts +3 -0
  91. package/dist-server/server/routes/tool-analytics.d.ts.map +1 -0
  92. package/dist-server/server/routes/tool-analytics.js +47 -0
  93. package/dist-server/server/services/adapters/sqliteAdapter.d.ts +1 -0
  94. package/dist-server/server/services/adapters/sqliteAdapter.d.ts.map +1 -1
  95. package/dist-server/server/services/adapters/sqliteAdapter.js +78 -48
  96. package/dist-server/server/services/credentialStore.d.ts +10 -0
  97. package/dist-server/server/services/credentialStore.d.ts.map +1 -0
  98. package/dist-server/server/services/credentialStore.js +123 -0
  99. package/dist-server/server/services/hindsightClient.d.ts.map +1 -1
  100. package/dist-server/server/services/hindsightClient.js +1 -0
  101. package/dist-server/server/services/lessonExtractor.d.ts +2 -0
  102. package/dist-server/server/services/lessonExtractor.d.ts.map +1 -1
  103. package/dist-server/server/services/lessonExtractor.js +7 -2
  104. package/dist-server/server/services/repoIndexer.d.ts +7 -1
  105. package/dist-server/server/services/repoIndexer.d.ts.map +1 -1
  106. package/dist-server/server/services/repoIndexer.js +295 -94
  107. package/dist-server/server/services/sqliteStore.d.ts +64 -0
  108. package/dist-server/server/services/sqliteStore.d.ts.map +1 -1
  109. package/dist-server/server/services/sqliteStore.js +238 -0
  110. package/dist-server/src/config.d.ts +2 -0
  111. package/dist-server/src/config.d.ts.map +1 -0
  112. package/dist-server/src/config.js +3 -0
  113. package/dist-server/src/graph/db.d.ts +46 -0
  114. package/dist-server/src/graph/db.d.ts.map +1 -0
  115. package/dist-server/src/graph/db.js +241 -0
  116. package/dist-server/src/graph/extractors/code.d.ts +12 -0
  117. package/dist-server/src/graph/extractors/code.d.ts.map +1 -0
  118. package/dist-server/src/graph/extractors/code.js +239 -0
  119. package/dist-server/src/graph/extractors/cross-type.d.ts +16 -0
  120. package/dist-server/src/graph/extractors/cross-type.d.ts.map +1 -0
  121. package/dist-server/src/graph/extractors/cross-type.js +67 -0
  122. package/dist-server/src/graph/extractors/markdown.d.ts +29 -0
  123. package/dist-server/src/graph/extractors/markdown.d.ts.map +1 -0
  124. package/dist-server/src/graph/extractors/markdown.js +224 -0
  125. package/dist-server/src/graph/extractors/yaml.d.ts +15 -0
  126. package/dist-server/src/graph/extractors/yaml.d.ts.map +1 -0
  127. package/dist-server/src/graph/extractors/yaml.js +104 -0
  128. package/dist-server/src/graph/index.d.ts +62 -0
  129. package/dist-server/src/graph/index.d.ts.map +1 -0
  130. package/dist-server/src/graph/index.js +67 -0
  131. package/dist-server/src/graph/packer.d.ts +19 -0
  132. package/dist-server/src/graph/packer.d.ts.map +1 -0
  133. package/dist-server/src/graph/packer.js +134 -0
  134. package/dist-server/src/graph/resolver.d.ts +12 -0
  135. package/dist-server/src/graph/resolver.d.ts.map +1 -0
  136. package/dist-server/src/graph/resolver.js +81 -0
  137. package/dist-server/src/graph/scanner.d.ts +34 -0
  138. package/dist-server/src/graph/scanner.d.ts.map +1 -0
  139. package/dist-server/src/graph/scanner.js +252 -0
  140. package/dist-server/src/graph/traverser.d.ts +17 -0
  141. package/dist-server/src/graph/traverser.d.ts.map +1 -0
  142. package/dist-server/src/graph/traverser.js +185 -0
  143. package/dist-server/src/graph/types.d.ts +117 -0
  144. package/dist-server/src/graph/types.d.ts.map +1 -0
  145. package/dist-server/src/graph/types.js +63 -0
  146. package/dist-server/src/metaprompt/v2/assembler.d.ts +3 -0
  147. package/dist-server/src/metaprompt/v2/assembler.d.ts.map +1 -0
  148. package/dist-server/src/metaprompt/v2/assembler.js +261 -0
  149. package/dist-server/src/metaprompt/v2/context-strategist.d.ts +3 -0
  150. package/dist-server/src/metaprompt/v2/context-strategist.d.ts.map +1 -0
  151. package/dist-server/src/metaprompt/v2/context-strategist.js +173 -0
  152. package/dist-server/src/metaprompt/v2/evaluator.d.ts +3 -0
  153. package/dist-server/src/metaprompt/v2/evaluator.d.ts.map +1 -0
  154. package/dist-server/src/metaprompt/v2/evaluator.js +281 -0
  155. package/dist-server/src/metaprompt/v2/index.d.ts +41 -0
  156. package/dist-server/src/metaprompt/v2/index.d.ts.map +1 -0
  157. package/dist-server/src/metaprompt/v2/index.js +90 -0
  158. package/dist-server/src/metaprompt/v2/parser.d.ts +3 -0
  159. package/dist-server/src/metaprompt/v2/parser.d.ts.map +1 -0
  160. package/dist-server/src/metaprompt/v2/parser.js +138 -0
  161. package/dist-server/src/metaprompt/v2/pattern-selector.d.ts +3 -0
  162. package/dist-server/src/metaprompt/v2/pattern-selector.d.ts.map +1 -0
  163. package/dist-server/src/metaprompt/v2/pattern-selector.js +154 -0
  164. package/dist-server/src/metaprompt/v2/researcher.d.ts +3 -0
  165. package/dist-server/src/metaprompt/v2/researcher.d.ts.map +1 -0
  166. package/dist-server/src/metaprompt/v2/researcher.js +194 -0
  167. package/dist-server/src/metaprompt/v2/tool-discovery.d.ts +74 -0
  168. package/dist-server/src/metaprompt/v2/tool-discovery.d.ts.map +1 -0
  169. package/dist-server/src/metaprompt/v2/tool-discovery.js +290 -0
  170. package/dist-server/src/metaprompt/v2/types.d.ts +154 -0
  171. package/dist-server/src/metaprompt/v2/types.d.ts.map +1 -0
  172. package/dist-server/src/metaprompt/v2/types.js +2 -0
  173. package/dist-server/src/services/contradictionDetector.js +1 -1
  174. package/dist-server/src/services/llmService.d.ts +61 -0
  175. package/dist-server/src/services/llmService.d.ts.map +1 -0
  176. package/dist-server/src/services/llmService.js +222 -0
  177. package/dist-server/src/store/knowledgeBase.d.ts +5 -1
  178. package/dist-server/src/store/knowledgeBase.d.ts.map +1 -1
  179. package/dist-server/src/store/knowledgeBase.js +0 -1
  180. package/dist-server/src/store/mcp-registry.d.ts +29 -0
  181. package/dist-server/src/store/mcp-registry.d.ts.map +1 -0
  182. package/dist-server/src/store/mcp-registry.js +1303 -0
  183. package/dist-server/src/types/registry.types.d.ts +13 -0
  184. package/dist-server/src/types/registry.types.d.ts.map +1 -0
  185. package/dist-server/src/types/registry.types.js +2 -0
  186. package/dist-server/tsconfig.server.tsbuildinfo +1 -1
  187. package/package.json +118 -105
  188. package/scripts/cleanup-worktrees.ps1 +29 -0
  189. package/dist/assets/Badge-DrUmDAXz.js +0 -1
  190. package/dist/assets/KnowledgeTab-CxlC76Rf.js +0 -4
  191. package/dist/assets/MemoryTab-CUScYWs9.js +0 -16
  192. package/dist/assets/QualificationTab-BqnWSQHm.js +0 -1
  193. package/dist/assets/ReviewTab-DKYl6cR9.js +0 -103
  194. package/dist/assets/TestTab-iJ2vCf9l.js +0 -33
  195. package/dist/assets/icons-MKpPNvV8.js +0 -1
  196. package/dist/assets/index-B_ip7Amg.css +0 -1
  197. package/dist/assets/index-gBy3427k.js +0 -143
  198. package/dist/assets/services-CTWXQK6j.js +0 -356
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Slack Connector — Channels + Messages
3
+ * Issue #92
4
+ */
5
+ import { Router } from 'express';
6
+ import { rateLimitedFetch, connectorError, getApiKey, formatTimestamp } from './shared.js';
7
+ const router = Router();
8
+ const sessionKeys = new Map();
9
+ const SLACK_API = 'https://slack.com/api';
10
+ function slackHeaders(token) {
11
+ return { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' };
12
+ }
13
+ // ── Test ──
14
+ router.post('/test', async (req, res) => {
15
+ const { apiKey } = req.body;
16
+ if (!apiKey) {
17
+ res.status(400).json({ status: 'error', error: 'Missing bot token' });
18
+ return;
19
+ }
20
+ try {
21
+ const resp = await rateLimitedFetch(`${SLACK_API}/auth.test`, {
22
+ method: 'POST', headers: slackHeaders(apiKey),
23
+ });
24
+ const data = await resp.json();
25
+ if (!data.ok) {
26
+ res.status(401).json({ status: 'error', error: data.error ?? 'Auth failed' });
27
+ return;
28
+ }
29
+ sessionKeys.set('slack', apiKey);
30
+ res.json({ status: 'ok', data: { user: data.user, team: data.team } });
31
+ }
32
+ catch (err) {
33
+ connectorError(res, 'Slack', err);
34
+ }
35
+ });
36
+ // ── List Channels ──
37
+ router.post('/channels', async (req, res) => {
38
+ const body = req.body;
39
+ const token = getApiKey('slack', body, sessionKeys);
40
+ if (!token) {
41
+ res.status(401).json({ status: 'error', error: 'No Slack token.' });
42
+ return;
43
+ }
44
+ try {
45
+ const resp = await rateLimitedFetch(`${SLACK_API}/conversations.list?types=public_channel,private_channel&limit=200`, { headers: slackHeaders(token) });
46
+ const data = await resp.json();
47
+ if (!data.ok)
48
+ throw new Error(data.error ?? 'Failed to list channels');
49
+ res.json({
50
+ status: 'ok',
51
+ data: (data.channels ?? []).map((ch) => ({
52
+ id: ch.id,
53
+ name: ch.name,
54
+ topic: ch.topic?.value ?? '',
55
+ memberCount: ch.num_members ?? 0,
56
+ isPrivate: ch.is_private ?? false,
57
+ })),
58
+ });
59
+ }
60
+ catch (err) {
61
+ connectorError(res, 'Slack', err);
62
+ }
63
+ });
64
+ // ── Fetch Messages ──
65
+ router.post('/fetch', async (req, res) => {
66
+ const body = req.body;
67
+ const token = getApiKey('slack', body, sessionKeys);
68
+ if (!token) {
69
+ res.status(401).json({ status: 'error', error: 'No Slack token.' });
70
+ return;
71
+ }
72
+ const channelId = body.channelId;
73
+ const limit = Math.min(body.limit ?? 100, 200);
74
+ if (!channelId) {
75
+ res.status(400).json({ status: 'error', error: 'channelId required' });
76
+ return;
77
+ }
78
+ try {
79
+ const params = new URLSearchParams({ channel: channelId, limit: String(limit) });
80
+ if (body.oldest)
81
+ params.set('oldest', String(body.oldest));
82
+ const resp = await rateLimitedFetch(`${SLACK_API}/conversations.history?${params}`, { headers: slackHeaders(token) });
83
+ const data = await resp.json();
84
+ if (!data.ok)
85
+ throw new Error(data.error ?? 'Failed to fetch messages');
86
+ const messages = (data.messages ?? []).reverse(); // chronological
87
+ // Resolve user names (batch)
88
+ const userIds = [...new Set(messages.map((m) => m.user).filter(Boolean))];
89
+ const userNames = new Map();
90
+ for (const uid of userIds.slice(0, 50)) {
91
+ try {
92
+ const uResp = await rateLimitedFetch(`${SLACK_API}/users.info?user=${uid}`, { headers: slackHeaders(token) });
93
+ const uData = await uResp.json();
94
+ if (uData.ok && uData.user) {
95
+ userNames.set(uid, uData.user.real_name ?? uData.user.name ?? uid);
96
+ }
97
+ }
98
+ catch { /* skip */ }
99
+ }
100
+ const items = messages.map((m) => ({
101
+ user: userNames.get(m.user) ?? m.user ?? 'bot',
102
+ text: m.text ?? '',
103
+ timestamp: m.ts,
104
+ threadTs: m.thread_ts,
105
+ replyCount: m.reply_count ?? 0,
106
+ }));
107
+ const markdown = items.map((m) => {
108
+ const ts = formatTimestamp(parseFloat(m.timestamp) * 1000);
109
+ const thread = m.replyCount > 0 ? ` (${m.replyCount} replies)` : '';
110
+ return `**${m.user}** (${ts})${thread}:\n${m.text}`;
111
+ }).join('\n\n');
112
+ res.json({
113
+ status: 'ok',
114
+ data: { items, markdown, count: items.length, tokens: Math.ceil(markdown.length / 4) },
115
+ });
116
+ }
117
+ catch (err) {
118
+ connectorError(res, 'Slack', err);
119
+ }
120
+ });
121
+ // ── Search ──
122
+ router.post('/search', async (req, res) => {
123
+ const body = req.body;
124
+ const token = getApiKey('slack', body, sessionKeys);
125
+ if (!token) {
126
+ res.status(401).json({ status: 'error', error: 'No Slack token.' });
127
+ return;
128
+ }
129
+ const query = body.query;
130
+ if (!query) {
131
+ res.status(400).json({ status: 'error', error: 'query required' });
132
+ return;
133
+ }
134
+ try {
135
+ const resp = await rateLimitedFetch(`${SLACK_API}/search.messages?query=${encodeURIComponent(query)}&count=20`, { headers: slackHeaders(token) });
136
+ const data = await resp.json();
137
+ if (!data.ok)
138
+ throw new Error(data.error ?? 'Search failed');
139
+ res.json({
140
+ status: 'ok',
141
+ data: (data.messages?.matches ?? []).map((m) => ({
142
+ text: m.text?.slice(0, 200),
143
+ user: m.user,
144
+ channel: m.channel?.name,
145
+ timestamp: m.ts,
146
+ })),
147
+ });
148
+ }
149
+ catch (err) {
150
+ connectorError(res, 'Slack', err);
151
+ }
152
+ });
153
+ export default router;
@@ -0,0 +1,3 @@
1
+ declare const router: import("express-serve-static-core").Router;
2
+ export default router;
3
+ //# sourceMappingURL=cost.d.ts.map
@@ -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;
@@ -1 +1 @@
1
- {"version":3,"file":"lessons.d.ts","sourceRoot":"","sources":["../../../server/routes/lessons.ts"],"names":[],"mappings":"AAOA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA8DxB,eAAe,MAAM,CAAC"}
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"}
@@ -1,17 +1,106 @@
1
1
  import { Router } from 'express';
2
+ import { z } from 'zod';
2
3
  import { readConfig } from '../config.js';
3
4
  import { detectCorrection } from '../services/correctionDetector.js';
4
5
  import { extractLesson } from '../services/lessonExtractor.js';
6
+ import { saveInstinct, getInstincts, updateConfidence, deleteInstinct } from '../services/sqliteStore.js';
5
7
  const router = Router();
6
8
  function genId() {
7
9
  return `lesson-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
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 */
9
97
  router.post('/extract', async (req, res) => {
10
- const { userMessage, previousAssistant, providerId, model, agentId } = req.body;
11
- if (!userMessage || !providerId || !model) {
12
- res.status(400).json({ error: 'Missing required fields' });
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(', ') });
13
101
  return;
14
102
  }
103
+ const { userMessage, previousAssistant, providerId, model, agentId } = parsed.data;
15
104
  const correction = detectCorrection(userMessage, previousAssistant ?? '');
16
105
  if (!correction) {
17
106
  res.json({ lesson: null });
@@ -29,14 +118,39 @@ router.post('/extract', async (req, res) => {
29
118
  res.json({ lesson: null });
30
119
  return;
31
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
+ });
32
138
  const lesson = {
139
+ id,
33
140
  rule: extracted.rule,
34
141
  category: extracted.category,
35
- agentId: agentId ?? '',
142
+ domain: extracted.domain ?? 'general',
143
+ confidence: extracted.confidence ?? 0.30,
144
+ agentId: effectiveAgentId,
36
145
  sourceUserMessage: userMessage,
37
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,
38
152
  };
39
- res.json({ lesson: { ...lesson, id: genId(), createdAt: Date.now(), appliedCount: 0, status: 'pending' } });
153
+ res.json({ lesson });
40
154
  }
41
155
  catch (err) {
42
156
  const message = err instanceof Error ? err.message : 'Extraction failed';
@@ -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"}