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 @@
1
+ {"version":3,"file":"github.d.ts","sourceRoot":"","sources":["../../../../server/routes/connectors/github.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,QAAA,MAAM,MAAM,4CAAW,CAAC;AAqMxB,eAAe,MAAM,CAAC"}
@@ -0,0 +1,195 @@
1
+ /**
2
+ * GitHub Connector — Issues + PRs
3
+ * Issues #90, #99
4
+ */
5
+ import { Router } from 'express';
6
+ import { rateLimitedFetch, fetchPaginated, connectorError, getApiKey, formatTimestamp } from './shared.js';
7
+ const router = Router();
8
+ const sessionKeys = new Map();
9
+ const GH_API = 'https://api.github.com';
10
+ function ghHeaders(token) {
11
+ return {
12
+ Authorization: `Bearer ${token}`,
13
+ Accept: 'application/vnd.github+json',
14
+ 'X-GitHub-Api-Version': '2022-11-28',
15
+ };
16
+ }
17
+ // ── Test ──
18
+ router.post('/test', async (req, res) => {
19
+ const { apiKey } = req.body;
20
+ if (!apiKey) {
21
+ res.status(400).json({ status: 'error', error: 'Missing apiKey' });
22
+ return;
23
+ }
24
+ try {
25
+ const resp = await rateLimitedFetch(`${GH_API}/user`, { headers: ghHeaders(apiKey) });
26
+ if (!resp.ok) {
27
+ res.status(401).json({ status: 'error', error: 'Invalid GitHub token' });
28
+ return;
29
+ }
30
+ const user = await resp.json();
31
+ sessionKeys.set('github', apiKey);
32
+ res.json({ status: 'ok', data: { user: user.name ?? user.login } });
33
+ }
34
+ catch (err) {
35
+ connectorError(res, 'GitHub', err);
36
+ }
37
+ });
38
+ // ── Fetch Issues ──
39
+ router.post('/issues', async (req, res) => {
40
+ const body = req.body;
41
+ const token = getApiKey('github', body, sessionKeys);
42
+ if (!token) {
43
+ res.status(401).json({ status: 'error', error: 'No GitHub token. Test connection first.' });
44
+ return;
45
+ }
46
+ const repo = body.repo;
47
+ const state = body.state ?? 'open';
48
+ const labels = body.labels;
49
+ if (!repo || !repo.includes('/')) {
50
+ res.status(400).json({ status: 'error', error: 'repo must be owner/name format' });
51
+ return;
52
+ }
53
+ try {
54
+ const items = await fetchPaginated({
55
+ maxPages: 5,
56
+ maxItems: 200,
57
+ fetchPage: async (cursor) => {
58
+ const params = new URLSearchParams({ state, per_page: '100', sort: 'updated', direction: 'desc' });
59
+ if (labels?.length)
60
+ params.set('labels', labels.join(','));
61
+ if (cursor)
62
+ params.set('page', cursor);
63
+ const resp = await rateLimitedFetch(`${GH_API}/repos/${repo}/issues?${params}`, { headers: ghHeaders(token) });
64
+ if (!resp.ok)
65
+ throw new Error(`GitHub API ${resp.status}`);
66
+ const data = await resp.json();
67
+ // Filter out pull requests (GitHub API returns PRs as issues)
68
+ const issues = data.filter((d) => !d.pull_request);
69
+ // Check for next page via Link header
70
+ const link = resp.headers.get('Link') ?? '';
71
+ const nextMatch = link.match(/page=(\d+)>; rel="next"/);
72
+ const nextCursor = nextMatch ? nextMatch[1] : undefined;
73
+ return {
74
+ items: issues.map((issue) => ({
75
+ number: issue.number,
76
+ title: issue.title,
77
+ body: issue.body ?? '',
78
+ state: issue.state,
79
+ labels: (issue.labels ?? []).map((l) => l.name),
80
+ assignee: issue.assignee?.login ?? 'unassigned',
81
+ author: issue.user?.login ?? '',
82
+ createdAt: issue.created_at,
83
+ updatedAt: issue.updated_at,
84
+ comments: issue.comments ?? 0,
85
+ url: issue.html_url,
86
+ })),
87
+ nextCursor,
88
+ };
89
+ },
90
+ });
91
+ // Convert to markdown
92
+ const markdown = items.map((issue) => `## #${issue.number}: ${issue.title}\n` +
93
+ `**Status:** ${issue.state} · **Assignee:** ${issue.assignee} · **Author:** ${issue.author}\n` +
94
+ `**Labels:** ${issue.labels.join(', ') || 'none'} · **Comments:** ${issue.comments}\n` +
95
+ `**Created:** ${formatTimestamp(issue.createdAt)} · **Updated:** ${formatTimestamp(issue.updatedAt)}\n\n` +
96
+ `${issue.body}\n`).join('\n---\n\n');
97
+ res.json({
98
+ status: 'ok',
99
+ data: {
100
+ items,
101
+ markdown,
102
+ count: items.length,
103
+ tokens: Math.ceil(markdown.length / 4),
104
+ },
105
+ });
106
+ }
107
+ catch (err) {
108
+ connectorError(res, 'GitHub', err);
109
+ }
110
+ });
111
+ // ── Fetch PRs (#99) ──
112
+ router.post('/pulls', async (req, res) => {
113
+ const body = req.body;
114
+ const token = getApiKey('github', body, sessionKeys);
115
+ if (!token) {
116
+ res.status(401).json({ status: 'error', error: 'No GitHub token.' });
117
+ return;
118
+ }
119
+ const repo = body.repo;
120
+ const state = body.state ?? 'open';
121
+ if (!repo?.includes('/')) {
122
+ res.status(400).json({ status: 'error', error: 'repo must be owner/name format' });
123
+ return;
124
+ }
125
+ try {
126
+ const params = new URLSearchParams({ state, per_page: '50', sort: 'updated', direction: 'desc' });
127
+ const resp = await rateLimitedFetch(`${GH_API}/repos/${repo}/pulls?${params}`, { headers: ghHeaders(token) });
128
+ if (!resp.ok)
129
+ throw new Error(`GitHub API ${resp.status}`);
130
+ const prs = await resp.json();
131
+ const items = prs.map((pr) => ({
132
+ number: pr.number,
133
+ title: pr.title,
134
+ body: pr.body ?? '',
135
+ state: pr.state,
136
+ author: pr.user?.login ?? '',
137
+ draft: pr.draft ?? false,
138
+ additions: pr.additions ?? 0,
139
+ deletions: pr.deletions ?? 0,
140
+ changedFiles: pr.changed_files ?? 0,
141
+ createdAt: pr.created_at,
142
+ updatedAt: pr.updated_at,
143
+ mergedAt: pr.merged_at,
144
+ url: pr.html_url,
145
+ }));
146
+ const markdown = items.map((pr) => `## PR #${pr.number}: ${pr.title}${pr.draft ? ' [DRAFT]' : ''}\n` +
147
+ `**Status:** ${pr.mergedAt ? 'merged' : pr.state} · **Author:** ${pr.author}\n` +
148
+ `**Changes:** +${pr.additions} -${pr.deletions} in ${pr.changedFiles} files\n` +
149
+ `**Created:** ${formatTimestamp(pr.createdAt)}${pr.mergedAt ? ` · **Merged:** ${formatTimestamp(pr.mergedAt)}` : ''}\n\n` +
150
+ `${pr.body}\n`).join('\n---\n\n');
151
+ res.json({
152
+ status: 'ok',
153
+ data: { items, markdown, count: items.length, tokens: Math.ceil(markdown.length / 4) },
154
+ });
155
+ }
156
+ catch (err) {
157
+ connectorError(res, 'GitHub', err);
158
+ }
159
+ });
160
+ // ── Search ──
161
+ router.post('/search', async (req, res) => {
162
+ const body = req.body;
163
+ const token = getApiKey('github', body, sessionKeys);
164
+ if (!token) {
165
+ res.status(401).json({ status: 'error', error: 'No GitHub token.' });
166
+ return;
167
+ }
168
+ const query = body.query;
169
+ const repo = body.repo;
170
+ if (!query) {
171
+ res.status(400).json({ status: 'error', error: 'query required' });
172
+ return;
173
+ }
174
+ try {
175
+ const q = repo ? `${query}+repo:${repo}` : query;
176
+ const resp = await rateLimitedFetch(`${GH_API}/search/issues?q=${encodeURIComponent(q)}&per_page=20`, { headers: ghHeaders(token) });
177
+ if (!resp.ok)
178
+ throw new Error(`GitHub search ${resp.status}`);
179
+ const data = await resp.json();
180
+ res.json({
181
+ status: 'ok',
182
+ data: data.items.map((i) => ({
183
+ number: i.number,
184
+ title: i.title,
185
+ type: i.pull_request ? 'pr' : 'issue',
186
+ state: i.state,
187
+ repo: i.repository_url?.split('/').slice(-2).join('/'),
188
+ })),
189
+ });
190
+ }
191
+ catch (err) {
192
+ connectorError(res, 'GitHub', err);
193
+ }
194
+ });
195
+ export default router;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Gmail Connector — Messages via search
3
+ * Issue #100
4
+ */
5
+ declare const router: import("express-serve-static-core").Router;
6
+ export default router;
7
+ //# sourceMappingURL=gmail.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gmail.d.ts","sourceRoot":"","sources":["../../../../server/routes/connectors/gmail.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,QAAA,MAAM,MAAM,4CAAW,CAAC;AAmIxB,eAAe,MAAM,CAAC"}
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Gmail Connector — Messages via search
3
+ * Issue #100
4
+ */
5
+ import { Router } from 'express';
6
+ import { rateLimitedFetch, connectorError, getApiKey } from './shared.js';
7
+ const router = Router();
8
+ const sessionKeys = new Map();
9
+ const GMAIL_API = 'https://gmail.googleapis.com/gmail/v1/users/me';
10
+ function gHeaders(token) {
11
+ return { Authorization: `Bearer ${token}` };
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 OAuth token' });
18
+ return;
19
+ }
20
+ try {
21
+ const resp = await rateLimitedFetch(`${GMAIL_API}/profile`, { headers: gHeaders(apiKey) });
22
+ if (!resp.ok) {
23
+ res.status(401).json({ status: 'error', error: 'Invalid Gmail token' });
24
+ return;
25
+ }
26
+ const data = await resp.json();
27
+ sessionKeys.set('gmail', apiKey);
28
+ res.json({ status: 'ok', data: { email: data.emailAddress } });
29
+ }
30
+ catch (err) {
31
+ connectorError(res, 'Gmail', err);
32
+ }
33
+ });
34
+ // ── Fetch Messages ──
35
+ router.post('/fetch', async (req, res) => {
36
+ const body = req.body;
37
+ const token = getApiKey('gmail', body, sessionKeys);
38
+ if (!token) {
39
+ res.status(401).json({ status: 'error', error: 'No Gmail token.' });
40
+ return;
41
+ }
42
+ const query = body.query ?? 'in:inbox';
43
+ const maxResults = Math.min(body.maxResults ?? 20, 50);
44
+ try {
45
+ // List message IDs
46
+ const listResp = await rateLimitedFetch(`${GMAIL_API}/messages?q=${encodeURIComponent(query)}&maxResults=${maxResults}`, { headers: gHeaders(token) });
47
+ if (!listResp.ok)
48
+ throw new Error(`Gmail list ${listResp.status}`);
49
+ const listData = await listResp.json();
50
+ const messageIds = (listData.messages ?? []).map(m => m.id);
51
+ const items = [];
52
+ // Fetch each message (batch would be better but simpler this way)
53
+ for (const msgId of messageIds.slice(0, 20)) {
54
+ try {
55
+ const msgResp = await rateLimitedFetch(`${GMAIL_API}/messages/${msgId}?format=full`, { headers: gHeaders(token) });
56
+ if (!msgResp.ok)
57
+ continue;
58
+ const msg = await msgResp.json();
59
+ const headers = msg.payload?.headers ?? [];
60
+ const getHeader = (name) => headers.find(h => h.name.toLowerCase() === name.toLowerCase())?.value ?? '';
61
+ items.push({
62
+ id: msgId,
63
+ subject: getHeader('Subject'),
64
+ from: getHeader('From'),
65
+ date: getHeader('Date'),
66
+ snippet: msg.snippet ?? '',
67
+ body: extractGmailBody(msg.payload),
68
+ });
69
+ }
70
+ catch { /* skip */ }
71
+ }
72
+ const markdown = items.map(m => `## ${m.subject}\n` +
73
+ `**From:** ${m.from}\n` +
74
+ `**Date:** ${m.date}\n\n` +
75
+ `${m.body}\n`).join('\n---\n\n');
76
+ res.json({
77
+ status: 'ok',
78
+ data: { items, markdown, count: items.length, tokens: Math.ceil(markdown.length / 4) },
79
+ });
80
+ }
81
+ catch (err) {
82
+ connectorError(res, 'Gmail', err);
83
+ }
84
+ });
85
+ function extractGmailBody(payload) {
86
+ if (!payload)
87
+ return '';
88
+ // Try to find text/plain first, then text/html
89
+ const plain = findPart(payload, 'text/plain');
90
+ if (plain?.body?.data) {
91
+ return Buffer.from(plain.body.data, 'base64url').toString('utf-8');
92
+ }
93
+ const html = findPart(payload, 'text/html');
94
+ if (html?.body?.data) {
95
+ const raw = Buffer.from(html.body.data, 'base64url').toString('utf-8');
96
+ // Simple HTML strip (shared htmlToMarkdown is overkill for email)
97
+ return raw.replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ').replace(/\n{3,}/g, '\n\n').trim();
98
+ }
99
+ // Direct body
100
+ if (payload.body?.data) {
101
+ return Buffer.from(payload.body.data, 'base64url').toString('utf-8');
102
+ }
103
+ return '';
104
+ }
105
+ function findPart(part, mimeType) {
106
+ if (part.mimeType === mimeType)
107
+ return part;
108
+ for (const child of part.parts ?? []) {
109
+ const found = findPart(child, mimeType);
110
+ if (found)
111
+ return found;
112
+ }
113
+ return undefined;
114
+ }
115
+ export default router;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Google Docs Connector
3
+ * Issue #91
4
+ *
5
+ * Requires OAuth token (from Google Drive OAuth flow in connectors.ts).
6
+ * Fetches document structural elements and converts to markdown.
7
+ */
8
+ declare const router: import("express-serve-static-core").Router;
9
+ export default router;
10
+ //# sourceMappingURL=google-docs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google-docs.d.ts","sourceRoot":"","sources":["../../../../server/routes/connectors/google-docs.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,QAAA,MAAM,MAAM,4CAAW,CAAC;AA8LxB,eAAe,MAAM,CAAC"}
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Google Docs Connector
3
+ * Issue #91
4
+ *
5
+ * Requires OAuth token (from Google Drive OAuth flow in connectors.ts).
6
+ * Fetches document structural elements and converts to markdown.
7
+ */
8
+ import { Router } from 'express';
9
+ import { rateLimitedFetch, connectorError, getApiKey } from './shared.js';
10
+ const router = Router();
11
+ const sessionKeys = new Map();
12
+ const DOCS_API = 'https://docs.googleapis.com/v1/documents';
13
+ const DRIVE_API = 'https://www.googleapis.com/drive/v3';
14
+ function gHeaders(token) {
15
+ return { Authorization: `Bearer ${token}` };
16
+ }
17
+ // ── Test ──
18
+ router.post('/test', async (req, res) => {
19
+ const { apiKey } = req.body;
20
+ if (!apiKey) {
21
+ res.status(400).json({ status: 'error', error: 'Missing OAuth token' });
22
+ return;
23
+ }
24
+ try {
25
+ const resp = await rateLimitedFetch(`${DRIVE_API}/about?fields=user(displayName,emailAddress)`, { headers: gHeaders(apiKey) });
26
+ if (!resp.ok) {
27
+ res.status(401).json({ status: 'error', error: 'Invalid Google token' });
28
+ return;
29
+ }
30
+ const data = await resp.json();
31
+ sessionKeys.set('google-docs', apiKey);
32
+ res.json({ status: 'ok', data: { user: data.user.displayName, email: data.user.emailAddress } });
33
+ }
34
+ catch (err) {
35
+ connectorError(res, 'Google Docs', err);
36
+ }
37
+ });
38
+ // ── Fetch Document(s) ──
39
+ router.post('/fetch', async (req, res) => {
40
+ const body = req.body;
41
+ const token = getApiKey('google-docs', body, sessionKeys);
42
+ if (!token) {
43
+ res.status(401).json({ status: 'error', error: 'No Google token.' });
44
+ return;
45
+ }
46
+ const documentIds = body.documentIds ?? [];
47
+ const folderId = body.folderId;
48
+ try {
49
+ let docIds = [...documentIds];
50
+ // If folderId provided, list docs in folder
51
+ if (folderId) {
52
+ const resp = await rateLimitedFetch(`${DRIVE_API}/files?q='${folderId}'+in+parents+and+mimeType='application/vnd.google-apps.document'&fields=files(id,name)&pageSize=50`, { headers: gHeaders(token) });
53
+ if (resp.ok) {
54
+ const data = await resp.json();
55
+ docIds.push(...data.files.map(f => f.id));
56
+ }
57
+ }
58
+ if (docIds.length === 0) {
59
+ // List recent docs
60
+ const resp = await rateLimitedFetch(`${DRIVE_API}/files?q=mimeType='application/vnd.google-apps.document'&fields=files(id,name)&orderBy=modifiedTime+desc&pageSize=20`, { headers: gHeaders(token) });
61
+ if (resp.ok) {
62
+ const data = await resp.json();
63
+ docIds = data.files.map(f => f.id);
64
+ }
65
+ }
66
+ const items = [];
67
+ for (const docId of docIds.slice(0, 20)) {
68
+ try {
69
+ const resp = await rateLimitedFetch(`${DOCS_API}/${docId}`, { headers: gHeaders(token) });
70
+ if (!resp.ok)
71
+ continue;
72
+ const doc = await resp.json();
73
+ const markdown = googleDocToMarkdown(doc);
74
+ items.push({
75
+ id: docId,
76
+ title: doc.title ?? docId,
77
+ markdown,
78
+ tokens: Math.ceil(markdown.length / 4),
79
+ });
80
+ }
81
+ catch { /* skip individual doc errors */ }
82
+ }
83
+ const fullMarkdown = items.map(i => `# ${i.title}\n\n${i.markdown}`).join('\n\n---\n\n');
84
+ res.json({
85
+ status: 'ok',
86
+ data: { items, markdown: fullMarkdown, count: items.length, tokens: Math.ceil(fullMarkdown.length / 4) },
87
+ });
88
+ }
89
+ catch (err) {
90
+ connectorError(res, 'Google Docs', err);
91
+ }
92
+ });
93
+ // ── Search ──
94
+ router.post('/search', async (req, res) => {
95
+ const body = req.body;
96
+ const token = getApiKey('google-docs', body, sessionKeys);
97
+ if (!token) {
98
+ res.status(401).json({ status: 'error', error: 'No Google token.' });
99
+ return;
100
+ }
101
+ const query = body.query ?? '';
102
+ try {
103
+ const q = query
104
+ ? `mimeType='application/vnd.google-apps.document' and fullText contains '${query.replace(/'/g, "\\'")}'`
105
+ : `mimeType='application/vnd.google-apps.document'`;
106
+ const resp = await rateLimitedFetch(`${DRIVE_API}/files?q=${encodeURIComponent(q)}&fields=files(id,name,modifiedTime)&orderBy=modifiedTime+desc&pageSize=20`, { headers: gHeaders(token) });
107
+ if (!resp.ok)
108
+ throw new Error(`Drive search ${resp.status}`);
109
+ const data = await resp.json();
110
+ res.json({ status: 'ok', data: data.files });
111
+ }
112
+ catch (err) {
113
+ connectorError(res, 'Google Docs', err);
114
+ }
115
+ });
116
+ function googleDocToMarkdown(doc) {
117
+ const elements = doc.body?.content ?? [];
118
+ const lines = [];
119
+ for (const el of elements) {
120
+ if (el.paragraph) {
121
+ const style = el.paragraph.paragraphStyle?.namedStyleType ?? '';
122
+ const text = (el.paragraph.elements ?? []).map(e => {
123
+ if (!e.textRun?.content)
124
+ return '';
125
+ let t = e.textRun.content;
126
+ const ts = e.textRun.textStyle;
127
+ if (ts?.bold)
128
+ t = `**${t.trim()}** `;
129
+ if (ts?.italic)
130
+ t = `*${t.trim()}* `;
131
+ if (ts?.link?.url)
132
+ t = `[${t.trim()}](${ts.link.url}) `;
133
+ return t;
134
+ }).join('').trimEnd();
135
+ if (!text || text === '\n')
136
+ continue;
137
+ if (style === 'HEADING_1')
138
+ lines.push(`# ${text}`);
139
+ else if (style === 'HEADING_2')
140
+ lines.push(`## ${text}`);
141
+ else if (style === 'HEADING_3')
142
+ lines.push(`### ${text}`);
143
+ else if (style === 'HEADING_4')
144
+ lines.push(`#### ${text}`);
145
+ else
146
+ lines.push(text);
147
+ }
148
+ if (el.table) {
149
+ const rows = (el.table.tableRows ?? []).map(row => (row.tableCells ?? []).map(cell => {
150
+ return (cell.content ?? [])
151
+ .map(c => (c.paragraph?.elements ?? []).map(e => e.textRun?.content ?? '').join(''))
152
+ .join(' ').trim();
153
+ }));
154
+ if (rows.length > 0) {
155
+ lines.push('| ' + rows[0].join(' | ') + ' |');
156
+ lines.push('| ' + rows[0].map(() => '---').join(' | ') + ' |');
157
+ for (const row of rows.slice(1)) {
158
+ lines.push('| ' + row.join(' | ') + ' |');
159
+ }
160
+ }
161
+ }
162
+ }
163
+ return lines.join('\n');
164
+ }
165
+ export default router;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Google Drive Connector — Files + Content
3
+ * Issue #101
4
+ */
5
+ declare const router: import("express-serve-static-core").Router;
6
+ export default router;
7
+ //# sourceMappingURL=google-drive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google-drive.d.ts","sourceRoot":"","sources":["../../../../server/routes/connectors/google-drive.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,QAAA,MAAM,MAAM,4CAAW,CAAC;AAuLxB,eAAe,MAAM,CAAC"}
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Google Drive Connector — Files + Content
3
+ * Issue #101
4
+ */
5
+ import { Router } from 'express';
6
+ import { rateLimitedFetch, connectorError, getApiKey } from './shared.js';
7
+ const router = Router();
8
+ const sessionKeys = new Map();
9
+ const DRIVE_API = 'https://www.googleapis.com/drive/v3';
10
+ function gHeaders(token) {
11
+ return { Authorization: `Bearer ${token}` };
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 OAuth token' });
18
+ return;
19
+ }
20
+ try {
21
+ const resp = await rateLimitedFetch(`${DRIVE_API}/about?fields=user(displayName,emailAddress)`, { headers: gHeaders(apiKey) });
22
+ if (!resp.ok) {
23
+ res.status(401).json({ status: 'error', error: 'Invalid Google token' });
24
+ return;
25
+ }
26
+ const data = await resp.json();
27
+ sessionKeys.set('google-drive', apiKey);
28
+ res.json({ status: 'ok', data: { user: data.user.displayName, email: data.user.emailAddress } });
29
+ }
30
+ catch (err) {
31
+ connectorError(res, 'Google Drive', err);
32
+ }
33
+ });
34
+ // ── List Files ──
35
+ router.post('/list', async (req, res) => {
36
+ const body = req.body;
37
+ const token = getApiKey('google-drive', body, sessionKeys);
38
+ if (!token) {
39
+ res.status(401).json({ status: 'error', error: 'No Google token.' });
40
+ return;
41
+ }
42
+ const folderId = body.folderId;
43
+ const query = body.query;
44
+ const mimeType = body.mimeType;
45
+ try {
46
+ const qParts = [];
47
+ if (folderId)
48
+ qParts.push(`'${folderId}' in parents`);
49
+ if (mimeType)
50
+ qParts.push(`mimeType='${mimeType}'`);
51
+ if (query)
52
+ qParts.push(`fullText contains '${query.replace(/'/g, "\\'")}'`);
53
+ qParts.push('trashed=false');
54
+ const resp = await rateLimitedFetch(`${DRIVE_API}/files?q=${encodeURIComponent(qParts.join(' and '))}&fields=files(id,name,mimeType,size,modifiedTime,parents)&orderBy=modifiedTime+desc&pageSize=50`, { headers: gHeaders(token) });
55
+ if (!resp.ok)
56
+ throw new Error(`Drive list ${resp.status}`);
57
+ const data = await resp.json();
58
+ res.json({
59
+ status: 'ok',
60
+ data: data.files.map(f => ({
61
+ id: f.id,
62
+ name: f.name,
63
+ mimeType: f.mimeType,
64
+ size: f.size,
65
+ modifiedTime: f.modifiedTime,
66
+ })),
67
+ });
68
+ }
69
+ catch (err) {
70
+ connectorError(res, 'Google Drive', err);
71
+ }
72
+ });
73
+ // ── Fetch File Content ──
74
+ router.post('/fetch', async (req, res) => {
75
+ const body = req.body;
76
+ const token = getApiKey('google-drive', body, sessionKeys);
77
+ if (!token) {
78
+ res.status(401).json({ status: 'error', error: 'No Google token.' });
79
+ return;
80
+ }
81
+ const fileIds = body.fileIds ?? [];
82
+ const folderId = body.folderId;
83
+ try {
84
+ let targetIds = [...fileIds];
85
+ // List folder contents if folderId provided
86
+ if (folderId && targetIds.length === 0) {
87
+ const resp = await rateLimitedFetch(`${DRIVE_API}/files?q='${folderId}'+in+parents+and+trashed=false&fields=files(id,name,mimeType)&pageSize=50`, { headers: gHeaders(token) });
88
+ if (resp.ok) {
89
+ const data = await resp.json();
90
+ targetIds = data.files
91
+ .filter(f => isContentFetchable(f.mimeType))
92
+ .map(f => f.id);
93
+ }
94
+ }
95
+ const items = [];
96
+ for (const fid of targetIds.slice(0, 20)) {
97
+ try {
98
+ // Get metadata first
99
+ const metaResp = await rateLimitedFetch(`${DRIVE_API}/files/${fid}?fields=id,name,mimeType`, { headers: gHeaders(token) });
100
+ if (!metaResp.ok)
101
+ continue;
102
+ const meta = await metaResp.json();
103
+ let content = '';
104
+ if (meta.mimeType === 'application/vnd.google-apps.document') {
105
+ // Export Google Doc as plain text
106
+ const exportResp = await rateLimitedFetch(`${DRIVE_API}/files/${fid}/export?mimeType=text/plain`, { headers: gHeaders(token) });
107
+ if (exportResp.ok)
108
+ content = await exportResp.text();
109
+ }
110
+ else if (meta.mimeType === 'application/vnd.google-apps.spreadsheet') {
111
+ // Export spreadsheet as CSV
112
+ const exportResp = await rateLimitedFetch(`${DRIVE_API}/files/${fid}/export?mimeType=text/csv`, { headers: gHeaders(token) });
113
+ if (exportResp.ok) {
114
+ const csv = await exportResp.text();
115
+ content = csvToMarkdownTable(csv);
116
+ }
117
+ }
118
+ else if (meta.mimeType?.startsWith('text/') || meta.mimeType === 'application/json') {
119
+ // Download text files directly
120
+ const dlResp = await rateLimitedFetch(`${DRIVE_API}/files/${fid}?alt=media`, { headers: gHeaders(token) });
121
+ if (dlResp.ok)
122
+ content = (await dlResp.text()).slice(0, 100000); // Cap at 100K chars
123
+ }
124
+ else {
125
+ // Non-text files: metadata only
126
+ content = `[Binary file: ${meta.name} (${meta.mimeType})]`;
127
+ }
128
+ items.push({
129
+ id: fid,
130
+ name: meta.name,
131
+ mimeType: meta.mimeType,
132
+ content,
133
+ tokens: Math.ceil(content.length / 4),
134
+ });
135
+ }
136
+ catch { /* skip individual file errors */ }
137
+ }
138
+ const markdown = items.map(i => `# ${i.name}\n\n${i.content}`).join('\n\n---\n\n');
139
+ res.json({
140
+ status: 'ok',
141
+ data: { items, markdown, count: items.length, tokens: Math.ceil(markdown.length / 4) },
142
+ });
143
+ }
144
+ catch (err) {
145
+ connectorError(res, 'Google Drive', err);
146
+ }
147
+ });
148
+ function isContentFetchable(mimeType) {
149
+ return (mimeType.startsWith('text/') ||
150
+ mimeType === 'application/json' ||
151
+ mimeType === 'application/vnd.google-apps.document' ||
152
+ mimeType === 'application/vnd.google-apps.spreadsheet');
153
+ }
154
+ function csvToMarkdownTable(csv) {
155
+ const rows = csv.split('\n').map(r => r.split(',').map(c => c.replace(/^"|"$/g, '').trim()));
156
+ if (rows.length === 0)
157
+ return '';
158
+ const headers = rows[0];
159
+ const dataRows = rows.slice(1).filter(r => r.some(c => c));
160
+ return `| ${headers.join(' | ')} |\n| ${headers.map(() => '---').join(' | ')} |\n` +
161
+ dataRows.map(r => `| ${r.join(' | ')} |`).join('\n');
162
+ }
163
+ export default router;