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,201 @@
1
+ /**
2
+ * Notion Connector (v2) — delegates to the main connectors.ts Notion endpoints.
3
+ *
4
+ * The full Notion implementation (test, fetch, search, OAuth) lives in
5
+ * server/routes/connectors.ts. This v2 route file re-exports the same
6
+ * functionality so the frontend can access it via /api/connectors/v2/notion/*.
7
+ */
8
+ import { Router } from 'express';
9
+ import { connectorError, getApiKey } from './shared.js';
10
+ const router = Router();
11
+ const sessionKeys = new Map();
12
+ const NOTION_API = 'https://api.notion.com/v1';
13
+ function notionHeaders(key) {
14
+ return {
15
+ Authorization: `Bearer ${key}`,
16
+ 'Notion-Version': '2022-06-28',
17
+ 'Content-Type': 'application/json',
18
+ };
19
+ }
20
+ // ── Test ──
21
+ router.post('/test', async (req, res) => {
22
+ const { apiKey } = req.body;
23
+ if (!apiKey) {
24
+ res.status(400).json({ status: 'error', error: 'Missing apiKey' });
25
+ return;
26
+ }
27
+ try {
28
+ const resp = await fetch(`${NOTION_API}/users/me`, { headers: notionHeaders(apiKey) });
29
+ if (resp.status === 401) {
30
+ res.status(401).json({ status: 'error', error: 'Invalid Notion API key. Create one at notion.so/my-integrations' });
31
+ return;
32
+ }
33
+ if (!resp.ok) {
34
+ res.status(resp.status).json({ status: 'error', error: `Notion API error: ${resp.status}` });
35
+ return;
36
+ }
37
+ const user = await resp.json();
38
+ sessionKeys.set('notion', apiKey);
39
+ res.json({ status: 'ok', data: { user: user.name ?? user.id } });
40
+ }
41
+ catch (err) {
42
+ connectorError(res, 'Notion', err);
43
+ }
44
+ });
45
+ // ── Search ──
46
+ router.post('/search', async (req, res) => {
47
+ const apiKey = getApiKey('notion', req.body, sessionKeys);
48
+ if (!apiKey) {
49
+ res.status(401).json({ status: 'error', error: 'No API key. Test connection first.' });
50
+ return;
51
+ }
52
+ const { query } = req.body;
53
+ try {
54
+ const resp = await fetch(`${NOTION_API}/search`, {
55
+ method: 'POST',
56
+ headers: notionHeaders(apiKey),
57
+ body: JSON.stringify(query ? { query, page_size: 20 } : { page_size: 20 }),
58
+ });
59
+ if (!resp.ok) {
60
+ res.status(resp.status).json({ status: 'error', error: `Notion search failed: ${resp.status}` });
61
+ return;
62
+ }
63
+ const data = await resp.json();
64
+ const results = data.results.map(r => {
65
+ let title = r.id;
66
+ if (r.object === 'database' && r.title?.[0]) {
67
+ title = r.title[0].plain_text;
68
+ }
69
+ else if (r.properties) {
70
+ const titleProp = Object.values(r.properties).find((p) => p.type === 'title');
71
+ title = titleProp?.title?.[0]?.plain_text ?? r.id;
72
+ }
73
+ return { id: r.id, title, type: r.object };
74
+ });
75
+ res.json({ status: 'ok', data: results });
76
+ }
77
+ catch (err) {
78
+ connectorError(res, 'Notion', err);
79
+ }
80
+ });
81
+ // ── Fetch page content ──
82
+ router.post('/fetch', async (req, res) => {
83
+ const apiKey = getApiKey('notion', req.body, sessionKeys);
84
+ if (!apiKey) {
85
+ res.status(401).json({ status: 'error', error: 'No API key. Test connection first.' });
86
+ return;
87
+ }
88
+ const { pageIds, databaseIds } = req.body;
89
+ try {
90
+ const items = [];
91
+ for (const pageId of pageIds ?? []) {
92
+ const page = await fetchNotionPage(pageId, apiKey);
93
+ if (page)
94
+ items.push(page);
95
+ }
96
+ for (const dbId of databaseIds ?? []) {
97
+ const rows = await queryNotionDatabase(dbId, apiKey);
98
+ items.push(...rows);
99
+ }
100
+ // If nothing specified, search recent pages
101
+ if (!pageIds?.length && !databaseIds?.length) {
102
+ const resp = await fetch(`${NOTION_API}/search`, {
103
+ method: 'POST',
104
+ headers: notionHeaders(apiKey),
105
+ body: JSON.stringify({ sort: { direction: 'descending', timestamp: 'last_edited_time' }, page_size: 10 }),
106
+ });
107
+ if (resp.ok) {
108
+ const data = await resp.json();
109
+ for (const r of data.results) {
110
+ if (r.object === 'page') {
111
+ const page = await fetchNotionPage(r.id, apiKey);
112
+ if (page)
113
+ items.push(page);
114
+ }
115
+ }
116
+ }
117
+ }
118
+ res.json({ status: 'ok', data: items });
119
+ }
120
+ catch (err) {
121
+ connectorError(res, 'Notion', err);
122
+ }
123
+ });
124
+ // ── Helpers ──
125
+ async function fetchNotionPage(pageId, apiKey) {
126
+ const pageResp = await fetch(`${NOTION_API}/pages/${pageId}`, { headers: notionHeaders(apiKey) });
127
+ if (!pageResp.ok)
128
+ return null;
129
+ const page = await pageResp.json();
130
+ // Get title
131
+ const titleProp = Object.values(page.properties).find((p) => p.type === 'title');
132
+ const title = titleProp?.title?.[0]?.plain_text ?? page.id;
133
+ // Get blocks
134
+ const blocks = await fetchAllBlocks(pageId, apiKey);
135
+ const content = blocksToMarkdown(blocks);
136
+ return { id: pageId, title, content, tokens: Math.ceil(content.length / 4) };
137
+ }
138
+ async function queryNotionDatabase(dbId, apiKey) {
139
+ const items = [];
140
+ let cursor;
141
+ do {
142
+ const body = JSON.stringify(cursor ? { start_cursor: cursor } : {});
143
+ const resp = await fetch(`${NOTION_API}/databases/${dbId}/query`, {
144
+ method: 'POST', headers: notionHeaders(apiKey), body,
145
+ });
146
+ if (!resp.ok)
147
+ break;
148
+ const data = await resp.json();
149
+ for (const row of data.results) {
150
+ const titleProp = Object.values(row.properties).find((p) => p.type === 'title');
151
+ const title = titleProp?.title?.[0]?.plain_text ?? row.id;
152
+ items.push({ id: row.id, title, content: `# ${title}`, tokens: Math.ceil(title.length / 4) + 10 });
153
+ }
154
+ cursor = data.has_more && data.next_cursor ? data.next_cursor : undefined;
155
+ } while (cursor);
156
+ return items;
157
+ }
158
+ async function fetchAllBlocks(pageId, apiKey) {
159
+ const blocks = [];
160
+ let cursor;
161
+ do {
162
+ const qs = cursor ? `?start_cursor=${encodeURIComponent(cursor)}` : '';
163
+ const resp = await fetch(`${NOTION_API}/blocks/${pageId}/children${qs}`, { headers: notionHeaders(apiKey) });
164
+ if (!resp.ok)
165
+ break;
166
+ const data = await resp.json();
167
+ blocks.push(...data.results);
168
+ cursor = data.has_more && data.next_cursor ? data.next_cursor : undefined;
169
+ } while (cursor);
170
+ return blocks;
171
+ }
172
+ function blocksToMarkdown(blocks) {
173
+ const lines = [];
174
+ for (const b of blocks) {
175
+ const content = b[b.type];
176
+ if (!content || typeof content !== 'object')
177
+ continue;
178
+ const rt = content.rich_text;
179
+ if (!Array.isArray(rt))
180
+ continue;
181
+ const text = rt.map((r) => r.plain_text ?? '').join('');
182
+ if (!text)
183
+ continue;
184
+ if (b.type === 'heading_1')
185
+ lines.push(`# ${text}`);
186
+ else if (b.type === 'heading_2')
187
+ lines.push(`## ${text}`);
188
+ else if (b.type === 'heading_3')
189
+ lines.push(`### ${text}`);
190
+ else if (b.type === 'bulleted_list_item')
191
+ lines.push(`- ${text}`);
192
+ else if (b.type === 'numbered_list_item')
193
+ lines.push(`1. ${text}`);
194
+ else if (b.type === 'code')
195
+ lines.push(`\`\`\`\n${text}\n\`\`\``);
196
+ else
197
+ lines.push(text);
198
+ }
199
+ return lines.join('\n');
200
+ }
201
+ export default router;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Plane Connector — Work Items (Issues), Projects, Cycles, Modules
3
+ *
4
+ * Plane is an open-source project management tool (alternative to Jira/Linear).
5
+ * API: REST, auth via X-API-Key header.
6
+ * Docs: https://developers.plane.so/api-reference
7
+ */
8
+ declare const router: import("express-serve-static-core").Router;
9
+ export default router;
10
+ //# sourceMappingURL=plane.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plane.d.ts","sourceRoot":"","sources":["../../../../server/routes/connectors/plane.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,QAAA,MAAM,MAAM,4CAAW,CAAC;AAoMxB,eAAe,MAAM,CAAC"}
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Plane Connector — Work Items (Issues), Projects, Cycles, Modules
3
+ *
4
+ * Plane is an open-source project management tool (alternative to Jira/Linear).
5
+ * API: REST, auth via X-API-Key header.
6
+ * Docs: https://developers.plane.so/api-reference
7
+ */
8
+ import { Router } from 'express';
9
+ import { rateLimitedFetch, fetchPaginated, connectorError, formatTimestamp } from './shared.js';
10
+ const router = Router();
11
+ const sessionKeys = new Map();
12
+ // Default to Plane Cloud; self-hosted users override via `baseUrl` param
13
+ const DEFAULT_BASE = 'https://api.plane.so';
14
+ function planeHeaders(token) {
15
+ return { 'X-API-Key': token, 'Content-Type': 'application/json' };
16
+ }
17
+ function getBase(body) {
18
+ return (body.baseUrl ?? DEFAULT_BASE).replace(/\/$/, '');
19
+ }
20
+ // ── Test ──
21
+ router.post('/test', async (req, res) => {
22
+ const { apiKey, baseUrl } = req.body;
23
+ if (!apiKey) {
24
+ res.status(400).json({ status: 'error', error: 'Missing API key (Personal Access Token from Profile Settings)' });
25
+ return;
26
+ }
27
+ const base = (baseUrl ?? DEFAULT_BASE).replace(/\/$/, '');
28
+ try {
29
+ // List workspaces to validate token
30
+ const resp = await rateLimitedFetch(`${base}/api/v1/workspaces/`, { headers: planeHeaders(apiKey) });
31
+ if (!resp.ok) {
32
+ res.status(401).json({ status: 'error', error: `Invalid Plane token (${resp.status})` });
33
+ return;
34
+ }
35
+ const data = await resp.json();
36
+ sessionKeys.set('plane', JSON.stringify({ apiKey, baseUrl: base }));
37
+ res.json({
38
+ status: 'ok',
39
+ data: {
40
+ workspaces: (data.results ?? data).slice(0, 10).map((w) => ({ slug: w.slug, name: w.name })),
41
+ },
42
+ });
43
+ }
44
+ catch (err) {
45
+ connectorError(res, 'Plane', err);
46
+ }
47
+ });
48
+ // ── List Projects ──
49
+ router.post('/projects', async (req, res) => {
50
+ const body = req.body;
51
+ const creds = getPlaneCreds(body);
52
+ if (!creds) {
53
+ res.status(401).json({ status: 'error', error: 'No Plane credentials. Test connection first.' });
54
+ return;
55
+ }
56
+ const workspace = body.workspace;
57
+ if (!workspace) {
58
+ res.status(400).json({ status: 'error', error: 'workspace slug required' });
59
+ return;
60
+ }
61
+ try {
62
+ const resp = await rateLimitedFetch(`${creds.baseUrl}/api/v1/workspaces/${workspace}/projects/`, { headers: planeHeaders(creds.apiKey) });
63
+ if (!resp.ok)
64
+ throw new Error(`Plane API ${resp.status}`);
65
+ const data = await resp.json();
66
+ res.json({
67
+ status: 'ok',
68
+ data: (data.results ?? data).map((p) => ({
69
+ id: p.id,
70
+ name: p.name,
71
+ identifier: p.identifier,
72
+ description: p.description ?? '',
73
+ })),
74
+ });
75
+ }
76
+ catch (err) {
77
+ connectorError(res, 'Plane', err);
78
+ }
79
+ });
80
+ // ── Fetch Work Items (Issues) ──
81
+ router.post('/fetch', async (req, res) => {
82
+ const body = req.body;
83
+ const creds = getPlaneCreds(body);
84
+ if (!creds) {
85
+ res.status(401).json({ status: 'error', error: 'No Plane credentials.' });
86
+ return;
87
+ }
88
+ const workspace = body.workspace;
89
+ const projectId = body.projectId;
90
+ if (!workspace || !projectId) {
91
+ res.status(400).json({ status: 'error', error: 'workspace and projectId required' });
92
+ return;
93
+ }
94
+ try {
95
+ const items = await fetchPaginated({
96
+ maxPages: 5,
97
+ maxItems: 200,
98
+ fetchPage: async (cursor) => {
99
+ const params = new URLSearchParams();
100
+ if (cursor)
101
+ params.set('cursor', cursor);
102
+ const resp = await rateLimitedFetch(`${creds.baseUrl}/api/v1/workspaces/${workspace}/projects/${projectId}/work-items/?${params}`, { headers: planeHeaders(creds.apiKey) });
103
+ if (!resp.ok)
104
+ throw new Error(`Plane API ${resp.status}`);
105
+ const data = await resp.json();
106
+ return {
107
+ items: (data.results ?? data).map((issue) => ({
108
+ id: issue.id,
109
+ sequence_id: issue.sequence_id,
110
+ name: issue.name ?? issue.title ?? '',
111
+ description_html: issue.description_html ?? '',
112
+ description: issue.description_stripped ?? issue.description ?? '',
113
+ state: issue.state_detail?.name ?? issue.state ?? '',
114
+ priority: issue.priority ?? 'none',
115
+ assignees: (issue.assignee_details ?? issue.assignees ?? []).map((a) => typeof a === 'string' ? a : (a.display_name ?? a.email ?? a.id)),
116
+ labels: (issue.label_details ?? issue.labels ?? []).map((l) => typeof l === 'string' ? l : (l.name ?? l.id)),
117
+ created_at: issue.created_at,
118
+ updated_at: issue.updated_at,
119
+ })),
120
+ nextCursor: data.next_page_results ? data.next_cursor : undefined,
121
+ };
122
+ },
123
+ });
124
+ const markdown = items.map((issue) => `## ${issue.sequence_id ? `#${issue.sequence_id}: ` : ''}${issue.name}\n` +
125
+ `**Status:** ${issue.state} · **Priority:** ${issue.priority}\n` +
126
+ `**Assignees:** ${issue.assignees.join(', ') || 'unassigned'}\n` +
127
+ `**Labels:** ${issue.labels.join(', ') || 'none'}\n` +
128
+ `**Created:** ${formatTimestamp(issue.created_at)} · **Updated:** ${formatTimestamp(issue.updated_at)}\n\n` +
129
+ `${issue.description}\n`).join('\n---\n\n');
130
+ res.json({
131
+ status: 'ok',
132
+ data: { items, markdown, count: items.length, tokens: Math.ceil(markdown.length / 4) },
133
+ });
134
+ }
135
+ catch (err) {
136
+ connectorError(res, 'Plane', err);
137
+ }
138
+ });
139
+ // ── Fetch Cycles ──
140
+ router.post('/cycles', async (req, res) => {
141
+ const body = req.body;
142
+ const creds = getPlaneCreds(body);
143
+ if (!creds) {
144
+ res.status(401).json({ status: 'error', error: 'No Plane credentials.' });
145
+ return;
146
+ }
147
+ const workspace = body.workspace;
148
+ const projectId = body.projectId;
149
+ if (!workspace || !projectId) {
150
+ res.status(400).json({ status: 'error', error: 'workspace and projectId required' });
151
+ return;
152
+ }
153
+ try {
154
+ const resp = await rateLimitedFetch(`${creds.baseUrl}/api/v1/workspaces/${workspace}/projects/${projectId}/cycles/`, { headers: planeHeaders(creds.apiKey) });
155
+ if (!resp.ok)
156
+ throw new Error(`Plane API ${resp.status}`);
157
+ const data = await resp.json();
158
+ res.json({
159
+ status: 'ok',
160
+ data: (data.results ?? data).map((c) => ({
161
+ id: c.id,
162
+ name: c.name,
163
+ start_date: c.start_date,
164
+ end_date: c.end_date,
165
+ status: c.status,
166
+ total_issues: c.total_issues ?? 0,
167
+ completed_issues: c.completed_issues ?? 0,
168
+ })),
169
+ });
170
+ }
171
+ catch (err) {
172
+ connectorError(res, 'Plane', err);
173
+ }
174
+ });
175
+ function getPlaneCreds(body) {
176
+ const stored = sessionKeys.get('plane');
177
+ if (stored) {
178
+ const parsed = JSON.parse(stored);
179
+ return {
180
+ apiKey: body.apiKey ?? parsed.apiKey,
181
+ baseUrl: getBase(body) !== DEFAULT_BASE ? getBase(body) : parsed.baseUrl,
182
+ };
183
+ }
184
+ if (body.apiKey) {
185
+ return { apiKey: body.apiKey, baseUrl: getBase(body) };
186
+ }
187
+ return null;
188
+ }
189
+ export default router;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Connector Shared Utilities
3
+ *
4
+ * Pagination, rate limiting, HTML→markdown, error handling, markdown tables.
5
+ */
6
+ export declare function rateLimitedFetch(url: string, options?: RequestInit, maxRetries?: number): Promise<globalThis.Response>;
7
+ export interface PaginationConfig<T> {
8
+ fetchPage: (cursor?: string) => Promise<{
9
+ items: T[];
10
+ nextCursor?: string;
11
+ }>;
12
+ maxPages?: number;
13
+ maxItems?: number;
14
+ }
15
+ export declare function fetchPaginated<T>(config: PaginationConfig<T>): Promise<T[]>;
16
+ export declare function htmlToMarkdown(html: string): string;
17
+ export declare function toMarkdownTable(headers: string[], rows: string[][]): string;
18
+ export declare function connectorError(res: any, service: string, error: unknown): void;
19
+ export declare function getApiKey(service: string, body: Record<string, unknown>, sessionKeys: Map<string, string>): string | null;
20
+ /**
21
+ * Persist an API key after successful test — survives server restarts.
22
+ */
23
+ export declare function persistApiKey(service: string, apiKey: string): void;
24
+ export declare function formatTimestamp(ts: string | number): string;
25
+ //# sourceMappingURL=shared.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../../../server/routes/connectors/shared.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAkBH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,WAAgB,EACzB,UAAU,SAAc,GACvB,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,CA0C9B;AAID,MAAM,WAAW,gBAAgB,CAAC,CAAC;IACjC,SAAS,EAAE,CAAC,MAAM,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,KAAK,EAAE,CAAC,EAAE,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAsB,cAAc,CAAC,CAAC,EAAE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAiBjF;AAID,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAqEnD;AAID,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,MAAM,CAa3E;AAID,wBAAgB,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAiB9E;AAID,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CAUzH;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAMnE;AAID,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAO3D"}
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Connector Shared Utilities
3
+ *
4
+ * Pagination, rate limiting, HTML→markdown, error handling, markdown tables.
5
+ */
6
+ // ── Rate-Limited Fetch ────────────────────────────────────────────────────────
7
+ const MAX_RETRIES = 3;
8
+ const BASE_DELAY_MS = 1000;
9
+ function sleep(ms) {
10
+ return new Promise(resolve => setTimeout(resolve, ms));
11
+ }
12
+ /** Compute exponential backoff with ±25% jitter to avoid thundering herd. */
13
+ function backoffMs(attempt, baseMs) {
14
+ const exp = baseMs * Math.pow(2, attempt);
15
+ const jitter = exp * 0.25 * (Math.random() * 2 - 1); // ±25%
16
+ return Math.min(Math.round(exp + jitter), 30_000);
17
+ }
18
+ export async function rateLimitedFetch(url, options = {}, maxRetries = MAX_RETRIES) {
19
+ let lastError = null;
20
+ let rateLimitHits = 0;
21
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
22
+ try {
23
+ const resp = await fetch(url, options);
24
+ if (resp.status === 429) {
25
+ rateLimitHits++;
26
+ if (attempt === maxRetries) {
27
+ throw new Error(`Rate limit (429) exceeded after ${maxRetries + 1} attempts. ` +
28
+ `Wait before retrying or check your API quota.`);
29
+ }
30
+ const retryAfter = resp.headers.get('Retry-After');
31
+ const delayMs = retryAfter
32
+ ? parseInt(retryAfter, 10) * 1000
33
+ : backoffMs(attempt, BASE_DELAY_MS);
34
+ await sleep(Math.min(delayMs, 30_000));
35
+ continue;
36
+ }
37
+ return resp;
38
+ }
39
+ catch (err) {
40
+ // Re-throw rate-limit errors immediately — no more retries
41
+ if (err instanceof Error && err.message.startsWith('Rate limit (429)'))
42
+ throw err;
43
+ lastError = err instanceof Error ? err : new Error(String(err));
44
+ if (attempt < maxRetries) {
45
+ await sleep(backoffMs(attempt, BASE_DELAY_MS));
46
+ }
47
+ }
48
+ }
49
+ if (rateLimitHits > 0) {
50
+ throw new Error(`Rate limit (429) exceeded after ${maxRetries + 1} attempts. ` +
51
+ `Wait before retrying or check your API quota.`);
52
+ }
53
+ throw lastError ?? new Error('Fetch failed after retries');
54
+ }
55
+ export async function fetchPaginated(config) {
56
+ const { fetchPage, maxPages = 10, maxItems = 1000 } = config;
57
+ const allItems = [];
58
+ let cursor;
59
+ let page = 0;
60
+ do {
61
+ const { items, nextCursor } = await fetchPage(cursor);
62
+ allItems.push(...items);
63
+ cursor = nextCursor;
64
+ page++;
65
+ if (allItems.length >= maxItems)
66
+ break;
67
+ if (page >= maxPages)
68
+ break;
69
+ } while (cursor);
70
+ return allItems.slice(0, maxItems);
71
+ }
72
+ // ── HTML → Markdown ───────────────────────────────────────────────────────────
73
+ export function htmlToMarkdown(html) {
74
+ if (!html)
75
+ return '';
76
+ let md = html;
77
+ // Remove scripts, styles, comments
78
+ md = md.replace(/<script[\s\S]*?<\/script>/gi, '');
79
+ md = md.replace(/<style[\s\S]*?<\/style>/gi, '');
80
+ md = md.replace(/<!--[\s\S]*?-->/g, '');
81
+ // Headings
82
+ md = md.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, '\n# $1\n');
83
+ md = md.replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, '\n## $1\n');
84
+ md = md.replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, '\n### $1\n');
85
+ md = md.replace(/<h4[^>]*>([\s\S]*?)<\/h4>/gi, '\n#### $1\n');
86
+ md = md.replace(/<h5[^>]*>([\s\S]*?)<\/h5>/gi, '\n##### $1\n');
87
+ md = md.replace(/<h6[^>]*>([\s\S]*?)<\/h6>/gi, '\n###### $1\n');
88
+ // Bold, italic
89
+ md = md.replace(/<(strong|b)>([\s\S]*?)<\/\1>/gi, '**$2**');
90
+ md = md.replace(/<(em|i)>([\s\S]*?)<\/\1>/gi, '*$2*');
91
+ // Links
92
+ md = md.replace(/<a[^>]+href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, '[$2]($1)');
93
+ // Images
94
+ md = md.replace(/<img[^>]+src="([^"]*)"[^>]*alt="([^"]*)"[^>]*\/?>/gi, '![$2]($1)');
95
+ md = md.replace(/<img[^>]+src="([^"]*)"[^>]*\/?>/gi, '![]($1)');
96
+ // Code blocks
97
+ md = md.replace(/<pre[^>]*><code[^>]*>([\s\S]*?)<\/code><\/pre>/gi, '\n```\n$1\n```\n');
98
+ md = md.replace(/<code>([\s\S]*?)<\/code>/gi, '`$1`');
99
+ // Lists
100
+ md = md.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, '- $1\n');
101
+ md = md.replace(/<\/?[ou]l[^>]*>/gi, '\n');
102
+ // Paragraphs, line breaks
103
+ md = md.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, '\n$1\n');
104
+ md = md.replace(/<br\s*\/?>/gi, '\n');
105
+ md = md.replace(/<hr\s*\/?>/gi, '\n---\n');
106
+ // Tables
107
+ md = md.replace(/<table[\s\S]*?<\/table>/gi, (table) => {
108
+ const rows = [];
109
+ const rowMatches = table.match(/<tr[\s\S]*?<\/tr>/gi) ?? [];
110
+ for (const row of rowMatches) {
111
+ const cells = (row.match(/<t[dh][^>]*>([\s\S]*?)<\/t[dh]>/gi) ?? [])
112
+ .map(c => c.replace(/<[^>]+>/g, '').trim());
113
+ rows.push(cells);
114
+ }
115
+ if (rows.length === 0)
116
+ return '';
117
+ return toMarkdownTable(rows[0], rows.slice(1));
118
+ });
119
+ // Strip remaining HTML tags
120
+ md = md.replace(/<[^>]+>/g, '');
121
+ // Decode HTML entities
122
+ md = md.replace(/&amp;/g, '&');
123
+ md = md.replace(/&lt;/g, '<');
124
+ md = md.replace(/&gt;/g, '>');
125
+ md = md.replace(/&quot;/g, '"');
126
+ md = md.replace(/&#39;/g, "'");
127
+ md = md.replace(/&nbsp;/g, ' ');
128
+ // Clean up whitespace
129
+ md = md.replace(/\n{3,}/g, '\n\n');
130
+ return md.trim();
131
+ }
132
+ // ── Markdown Table ────────────────────────────────────────────────────────────
133
+ export function toMarkdownTable(headers, rows) {
134
+ if (headers.length === 0)
135
+ return '';
136
+ const lines = [];
137
+ lines.push('| ' + headers.join(' | ') + ' |');
138
+ lines.push('| ' + headers.map(() => '---').join(' | ') + ' |');
139
+ for (const row of rows) {
140
+ // Pad row to match header length
141
+ const padded = [...row];
142
+ while (padded.length < headers.length)
143
+ padded.push('');
144
+ lines.push('| ' + padded.slice(0, headers.length).join(' | ') + ' |');
145
+ }
146
+ return lines.join('\n');
147
+ }
148
+ // ── Error Handling ────────────────────────────────────────────────────────────
149
+ export function connectorError(res, service, error) {
150
+ const msg = error instanceof Error ? error.message : String(error);
151
+ if (msg.includes('401') || msg.includes('Unauthorized') || msg.includes('Invalid')) {
152
+ res.status(401).json({ status: 'error', error: `${service}: Authentication failed. Check your API key.` });
153
+ return;
154
+ }
155
+ if (msg.includes('403') || msg.includes('Forbidden')) {
156
+ res.status(403).json({ status: 'error', error: `${service}: Permission denied. Check API key scopes.` });
157
+ return;
158
+ }
159
+ if (msg.includes('429') || msg.includes('Rate')) {
160
+ res.status(429).json({ status: 'error', error: `${service}: Rate limit hit. Try again in a minute.` });
161
+ return;
162
+ }
163
+ res.status(500).json({ status: 'error', error: `${service}: ${msg}` });
164
+ }
165
+ // ── Auth Helper ───────────────────────────────────────────────────────────────
166
+ export function getApiKey(service, body, sessionKeys) {
167
+ if (typeof body.apiKey === 'string' && body.apiKey)
168
+ return body.apiKey;
169
+ const session = sessionKeys.get(service);
170
+ if (session)
171
+ return session;
172
+ // Fallback: persistent credential store (survives restarts)
173
+ try {
174
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
175
+ const mod = require('../services/credentialStore.js');
176
+ return mod.getCredential(service);
177
+ }
178
+ catch {
179
+ return null;
180
+ }
181
+ }
182
+ /**
183
+ * Persist an API key after successful test — survives server restarts.
184
+ */
185
+ export function persistApiKey(service, apiKey) {
186
+ try {
187
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
188
+ const mod = require('../services/credentialStore.js');
189
+ mod.setCredential(service, apiKey);
190
+ }
191
+ catch { /* credential store not available */ }
192
+ }
193
+ // ── Timestamp Formatting ──────────────────────────────────────────────────────
194
+ export function formatTimestamp(ts) {
195
+ try {
196
+ const d = new Date(ts);
197
+ return d.toISOString().replace('T', ' ').slice(0, 16);
198
+ }
199
+ catch {
200
+ return String(ts);
201
+ }
202
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Slack Connector — Channels + Messages
3
+ * Issue #92
4
+ */
5
+ declare const router: import("express-serve-static-core").Router;
6
+ export default router;
7
+ //# sourceMappingURL=slack.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack.d.ts","sourceRoot":"","sources":["../../../../server/routes/connectors/slack.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,QAAA,MAAM,MAAM,4CAAW,CAAC;AAkJxB,eAAe,MAAM,CAAC"}