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.
- package/dist/assets/Badge-Bsy2H_p2.js +1 -0
- package/dist/assets/GraphPanel-D4X3faxA.js +47 -0
- package/dist/assets/{Input-ndEGQSgx.js → Input-Dyb88Erk.js} +1 -1
- package/dist/assets/KnowledgeTab-BccWz7Np.js +5 -0
- package/dist/assets/MemoryTab-Y_66cE01.js +16 -0
- package/dist/assets/QualificationTab-Dm9dEIpM.js +1 -0
- package/dist/assets/ReviewTab-BrfXSyyf.js +104 -0
- package/dist/assets/{Section-CgmwAj_2.js → Section-68XDCFTl.js} +1 -1
- package/dist/assets/TestTab-CLKRT63X.js +42 -0
- package/dist/assets/{ToolsTab-C10Ulm8b.js → ToolsTab-xumi9Uds.js} +1 -1
- package/dist/assets/icons-CS8RUPBi.js +1 -0
- package/dist/assets/index-B2bm0161.css +1 -0
- package/dist/assets/index-C626nWuA.js +422 -0
- package/dist/assets/services-BDk6yY4o.js +369 -0
- package/dist/index.html +4 -4
- package/dist-server/bin/modular-mcp.js +1 -0
- package/dist-server/server/index.d.ts.map +1 -1
- package/dist-server/server/index.js +30 -0
- package/dist-server/server/mcp/manager.d.ts +3 -0
- package/dist-server/server/mcp/manager.d.ts.map +1 -1
- package/dist-server/server/mcp/manager.js +64 -3
- package/dist-server/server/migrations/index.d.ts +11 -0
- package/dist-server/server/migrations/index.d.ts.map +1 -0
- package/dist-server/server/migrations/index.js +57 -0
- package/dist-server/server/routes/analytics.d.ts +3 -0
- package/dist-server/server/routes/analytics.d.ts.map +1 -0
- package/dist-server/server/routes/analytics.js +24 -0
- package/dist-server/server/routes/connectors/airtable.d.ts +7 -0
- package/dist-server/server/routes/connectors/airtable.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/airtable.js +119 -0
- package/dist-server/server/routes/connectors/confluence.d.ts +7 -0
- package/dist-server/server/routes/connectors/confluence.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/confluence.js +176 -0
- package/dist-server/server/routes/connectors/github.d.ts +7 -0
- package/dist-server/server/routes/connectors/github.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/github.js +195 -0
- package/dist-server/server/routes/connectors/gmail.d.ts +7 -0
- package/dist-server/server/routes/connectors/gmail.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/gmail.js +115 -0
- package/dist-server/server/routes/connectors/google-docs.d.ts +10 -0
- package/dist-server/server/routes/connectors/google-docs.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/google-docs.js +165 -0
- package/dist-server/server/routes/connectors/google-drive.d.ts +7 -0
- package/dist-server/server/routes/connectors/google-drive.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/google-drive.js +163 -0
- package/dist-server/server/routes/connectors/google-sheets.d.ts +7 -0
- package/dist-server/server/routes/connectors/google-sheets.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/google-sheets.js +90 -0
- package/dist-server/server/routes/connectors/hubspot.d.ts +7 -0
- package/dist-server/server/routes/connectors/hubspot.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/hubspot.js +134 -0
- package/dist-server/server/routes/connectors/index.d.ts +6 -0
- package/dist-server/server/routes/connectors/index.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/index.js +38 -0
- package/dist-server/server/routes/connectors/jira.d.ts +7 -0
- package/dist-server/server/routes/connectors/jira.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/jira.js +151 -0
- package/dist-server/server/routes/connectors/linear.d.ts +7 -0
- package/dist-server/server/routes/connectors/linear.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/linear.js +154 -0
- package/dist-server/server/routes/connectors/notion.d.ts +10 -0
- package/dist-server/server/routes/connectors/notion.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/notion.js +201 -0
- package/dist-server/server/routes/connectors/plane.d.ts +10 -0
- package/dist-server/server/routes/connectors/plane.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/plane.js +189 -0
- package/dist-server/server/routes/connectors/shared.d.ts +25 -0
- package/dist-server/server/routes/connectors/shared.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/shared.js +202 -0
- package/dist-server/server/routes/connectors/slack.d.ts +7 -0
- package/dist-server/server/routes/connectors/slack.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/slack.js +153 -0
- package/dist-server/server/routes/cost.d.ts +3 -0
- package/dist-server/server/routes/cost.d.ts.map +1 -0
- package/dist-server/server/routes/cost.js +113 -0
- package/dist-server/server/routes/graph.d.ts +11 -0
- package/dist-server/server/routes/graph.d.ts.map +1 -0
- package/dist-server/server/routes/graph.js +213 -0
- package/dist-server/server/routes/lessons.d.ts.map +1 -1
- package/dist-server/server/routes/lessons.js +119 -5
- package/dist-server/server/routes/llm.d.ts.map +1 -1
- package/dist-server/server/routes/llm.js +85 -18
- package/dist-server/server/routes/metaprompt-v2.d.ts +3 -0
- package/dist-server/server/routes/metaprompt-v2.d.ts.map +1 -0
- package/dist-server/server/routes/metaprompt-v2.js +104 -0
- package/dist-server/server/routes/qualification.d.ts.map +1 -1
- package/dist-server/server/routes/qualification.js +61 -11
- package/dist-server/server/routes/skills-search.d.ts.map +1 -1
- package/dist-server/server/routes/skills-search.js +10 -0
- package/dist-server/server/routes/tool-analytics.d.ts +3 -0
- package/dist-server/server/routes/tool-analytics.d.ts.map +1 -0
- package/dist-server/server/routes/tool-analytics.js +47 -0
- package/dist-server/server/services/adapters/sqliteAdapter.d.ts +1 -0
- package/dist-server/server/services/adapters/sqliteAdapter.d.ts.map +1 -1
- package/dist-server/server/services/adapters/sqliteAdapter.js +78 -48
- package/dist-server/server/services/credentialStore.d.ts +10 -0
- package/dist-server/server/services/credentialStore.d.ts.map +1 -0
- package/dist-server/server/services/credentialStore.js +123 -0
- package/dist-server/server/services/hindsightClient.d.ts.map +1 -1
- package/dist-server/server/services/hindsightClient.js +1 -0
- package/dist-server/server/services/lessonExtractor.d.ts +2 -0
- package/dist-server/server/services/lessonExtractor.d.ts.map +1 -1
- package/dist-server/server/services/lessonExtractor.js +7 -2
- package/dist-server/server/services/repoIndexer.d.ts +7 -1
- package/dist-server/server/services/repoIndexer.d.ts.map +1 -1
- package/dist-server/server/services/repoIndexer.js +295 -94
- package/dist-server/server/services/sqliteStore.d.ts +64 -0
- package/dist-server/server/services/sqliteStore.d.ts.map +1 -1
- package/dist-server/server/services/sqliteStore.js +238 -0
- package/dist-server/src/config.d.ts +2 -0
- package/dist-server/src/config.d.ts.map +1 -0
- package/dist-server/src/config.js +3 -0
- package/dist-server/src/graph/db.d.ts +46 -0
- package/dist-server/src/graph/db.d.ts.map +1 -0
- package/dist-server/src/graph/db.js +241 -0
- package/dist-server/src/graph/extractors/code.d.ts +12 -0
- package/dist-server/src/graph/extractors/code.d.ts.map +1 -0
- package/dist-server/src/graph/extractors/code.js +239 -0
- package/dist-server/src/graph/extractors/cross-type.d.ts +16 -0
- package/dist-server/src/graph/extractors/cross-type.d.ts.map +1 -0
- package/dist-server/src/graph/extractors/cross-type.js +67 -0
- package/dist-server/src/graph/extractors/markdown.d.ts +29 -0
- package/dist-server/src/graph/extractors/markdown.d.ts.map +1 -0
- package/dist-server/src/graph/extractors/markdown.js +224 -0
- package/dist-server/src/graph/extractors/yaml.d.ts +15 -0
- package/dist-server/src/graph/extractors/yaml.d.ts.map +1 -0
- package/dist-server/src/graph/extractors/yaml.js +104 -0
- package/dist-server/src/graph/index.d.ts +62 -0
- package/dist-server/src/graph/index.d.ts.map +1 -0
- package/dist-server/src/graph/index.js +67 -0
- package/dist-server/src/graph/packer.d.ts +19 -0
- package/dist-server/src/graph/packer.d.ts.map +1 -0
- package/dist-server/src/graph/packer.js +134 -0
- package/dist-server/src/graph/resolver.d.ts +12 -0
- package/dist-server/src/graph/resolver.d.ts.map +1 -0
- package/dist-server/src/graph/resolver.js +81 -0
- package/dist-server/src/graph/scanner.d.ts +34 -0
- package/dist-server/src/graph/scanner.d.ts.map +1 -0
- package/dist-server/src/graph/scanner.js +252 -0
- package/dist-server/src/graph/traverser.d.ts +17 -0
- package/dist-server/src/graph/traverser.d.ts.map +1 -0
- package/dist-server/src/graph/traverser.js +185 -0
- package/dist-server/src/graph/types.d.ts +117 -0
- package/dist-server/src/graph/types.d.ts.map +1 -0
- package/dist-server/src/graph/types.js +63 -0
- package/dist-server/src/metaprompt/v2/assembler.d.ts +3 -0
- package/dist-server/src/metaprompt/v2/assembler.d.ts.map +1 -0
- package/dist-server/src/metaprompt/v2/assembler.js +261 -0
- package/dist-server/src/metaprompt/v2/context-strategist.d.ts +3 -0
- package/dist-server/src/metaprompt/v2/context-strategist.d.ts.map +1 -0
- package/dist-server/src/metaprompt/v2/context-strategist.js +173 -0
- package/dist-server/src/metaprompt/v2/evaluator.d.ts +3 -0
- package/dist-server/src/metaprompt/v2/evaluator.d.ts.map +1 -0
- package/dist-server/src/metaprompt/v2/evaluator.js +281 -0
- package/dist-server/src/metaprompt/v2/index.d.ts +41 -0
- package/dist-server/src/metaprompt/v2/index.d.ts.map +1 -0
- package/dist-server/src/metaprompt/v2/index.js +90 -0
- package/dist-server/src/metaprompt/v2/parser.d.ts +3 -0
- package/dist-server/src/metaprompt/v2/parser.d.ts.map +1 -0
- package/dist-server/src/metaprompt/v2/parser.js +138 -0
- package/dist-server/src/metaprompt/v2/pattern-selector.d.ts +3 -0
- package/dist-server/src/metaprompt/v2/pattern-selector.d.ts.map +1 -0
- package/dist-server/src/metaprompt/v2/pattern-selector.js +154 -0
- package/dist-server/src/metaprompt/v2/researcher.d.ts +3 -0
- package/dist-server/src/metaprompt/v2/researcher.d.ts.map +1 -0
- package/dist-server/src/metaprompt/v2/researcher.js +194 -0
- package/dist-server/src/metaprompt/v2/tool-discovery.d.ts +74 -0
- package/dist-server/src/metaprompt/v2/tool-discovery.d.ts.map +1 -0
- package/dist-server/src/metaprompt/v2/tool-discovery.js +290 -0
- package/dist-server/src/metaprompt/v2/types.d.ts +154 -0
- package/dist-server/src/metaprompt/v2/types.d.ts.map +1 -0
- package/dist-server/src/metaprompt/v2/types.js +2 -0
- package/dist-server/src/services/contradictionDetector.js +1 -1
- package/dist-server/src/services/llmService.d.ts +61 -0
- package/dist-server/src/services/llmService.d.ts.map +1 -0
- package/dist-server/src/services/llmService.js +222 -0
- package/dist-server/src/store/knowledgeBase.d.ts +5 -1
- package/dist-server/src/store/knowledgeBase.d.ts.map +1 -1
- package/dist-server/src/store/knowledgeBase.js +0 -1
- package/dist-server/src/store/mcp-registry.d.ts +29 -0
- package/dist-server/src/store/mcp-registry.d.ts.map +1 -0
- package/dist-server/src/store/mcp-registry.js +1303 -0
- package/dist-server/src/types/registry.types.d.ts +13 -0
- package/dist-server/src/types/registry.types.d.ts.map +1 -0
- package/dist-server/src/types/registry.types.js +2 -0
- package/dist-server/tsconfig.server.tsbuildinfo +1 -1
- package/package.json +118 -105
- package/scripts/cleanup-worktrees.ps1 +29 -0
- package/dist/assets/Badge-DrUmDAXz.js +0 -1
- package/dist/assets/KnowledgeTab-CxlC76Rf.js +0 -4
- package/dist/assets/MemoryTab-CUScYWs9.js +0 -16
- package/dist/assets/QualificationTab-BqnWSQHm.js +0 -1
- package/dist/assets/ReviewTab-DKYl6cR9.js +0 -103
- package/dist/assets/TestTab-iJ2vCf9l.js +0 -33
- package/dist/assets/icons-MKpPNvV8.js +0 -1
- package/dist/assets/index-B_ip7Amg.css +0 -1
- package/dist/assets/index-gBy3427k.js +0 -143
- package/dist/assets/services-CTWXQK6j.js +0 -356
package/dist/index.html
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
<link href="https://cdn.jsdelivr.net/npm/geist@1.3.1/dist/fonts/geist-sans/style.min.css" rel="stylesheet" />
|
|
8
8
|
<link href="https://cdn.jsdelivr.net/npm/geist@1.3.1/dist/fonts/geist-mono/style.min.css" rel="stylesheet" />
|
|
9
9
|
<title>MODULAR — Agent Patchbay</title>
|
|
10
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
+
<script type="module" crossorigin src="/assets/index-C626nWuA.js"></script>
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/vendor-D1h_O76p.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/stores-CeKWz7ou.js">
|
|
13
|
-
<link rel="modulepreload" crossorigin href="/assets/services-
|
|
14
|
-
<link rel="modulepreload" crossorigin href="/assets/icons-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
13
|
+
<link rel="modulepreload" crossorigin href="/assets/services-BDk6yY4o.js">
|
|
14
|
+
<link rel="modulepreload" crossorigin href="/assets/icons-CS8RUPBi.js">
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-B2bm0161.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body style="margin:0;padding:0;height:100vh;overflow:hidden">
|
|
18
18
|
<div id="root" style="height:100%;overflow:hidden"></div>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../server/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../server/index.ts"],"names":[],"mappings":"AAwCA,wBAAgB,SAAS,gDAwHxB;AA+BD,wBAAgB,WAAW,CAAC,IAAI,GAAE,MAAa,sGAwB9C"}
|
|
@@ -29,6 +29,12 @@ import conversationRoutes from './routes/conversations.js';
|
|
|
29
29
|
import memoryRoutes from './routes/memory.js';
|
|
30
30
|
import cacheRoutes from './routes/cache.js';
|
|
31
31
|
import lessonRoutes from './routes/lessons.js';
|
|
32
|
+
import metapromptV2Routes from './routes/metaprompt-v2.js';
|
|
33
|
+
import graphRoutes from './routes/graph.js';
|
|
34
|
+
import connectorSubRoutes from './routes/connectors/index.js';
|
|
35
|
+
import costRoutes from './routes/cost.js';
|
|
36
|
+
import toolAnalyticsRoutes from './routes/tool-analytics.js';
|
|
37
|
+
import analyticsRoutes from './routes/analytics.js';
|
|
32
38
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
33
39
|
export function createApp() {
|
|
34
40
|
const app = express();
|
|
@@ -42,6 +48,24 @@ export function createApp() {
|
|
|
42
48
|
],
|
|
43
49
|
}));
|
|
44
50
|
app.use(express.json({ limit: '10mb' }));
|
|
51
|
+
// Request logging middleware — correlation IDs + structured logs
|
|
52
|
+
app.use((req, res, next) => {
|
|
53
|
+
const requestId = crypto.randomUUID();
|
|
54
|
+
res.locals['requestId'] = requestId;
|
|
55
|
+
res.setHeader('X-Request-Id', requestId);
|
|
56
|
+
const start = Date.now();
|
|
57
|
+
res.on('finish', () => {
|
|
58
|
+
const duration = Date.now() - start;
|
|
59
|
+
console.log(JSON.stringify({
|
|
60
|
+
requestId,
|
|
61
|
+
method: req.method,
|
|
62
|
+
path: req.path,
|
|
63
|
+
status: res.statusCode,
|
|
64
|
+
durationMs: duration,
|
|
65
|
+
}));
|
|
66
|
+
});
|
|
67
|
+
next();
|
|
68
|
+
});
|
|
45
69
|
// Basic security headers (lightweight helmet-like)
|
|
46
70
|
app.use((_req, res, next) => {
|
|
47
71
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
@@ -93,6 +117,12 @@ export function createApp() {
|
|
|
93
117
|
app.use('/api/memory', memoryRoutes);
|
|
94
118
|
app.use('/api/cache', cacheRoutes);
|
|
95
119
|
app.use('/api/lessons', lessonRoutes);
|
|
120
|
+
app.use('/api/metaprompt/v2', metapromptV2Routes);
|
|
121
|
+
app.use('/api/graph', graphRoutes);
|
|
122
|
+
app.use('/api/connectors/v2', connectorSubRoutes);
|
|
123
|
+
app.use('/api/cost', costRoutes);
|
|
124
|
+
app.use('/api/tool-analytics', toolAnalyticsRoutes);
|
|
125
|
+
app.use('/api/analytics', analyticsRoutes);
|
|
96
126
|
// API 404 catch-all — log unmatched API routes for debugging
|
|
97
127
|
app.use('/api', (_req, res) => {
|
|
98
128
|
console.warn(`[API 404] ${_req.method} ${_req.originalUrl}`);
|
|
@@ -13,6 +13,8 @@ interface McpConnection {
|
|
|
13
13
|
}>;
|
|
14
14
|
connectedAt: number | null;
|
|
15
15
|
lastError: string | null;
|
|
16
|
+
retryCount: number;
|
|
17
|
+
retryTimer: ReturnType<typeof setTimeout> | null;
|
|
16
18
|
}
|
|
17
19
|
export declare class McpManager {
|
|
18
20
|
private connections;
|
|
@@ -23,6 +25,7 @@ export declare class McpManager {
|
|
|
23
25
|
private normalizeConfig;
|
|
24
26
|
addServer(config: McpServerConfig): void;
|
|
25
27
|
removeServer(id: string): void;
|
|
28
|
+
private scheduleReconnect;
|
|
26
29
|
getServer(id: string): McpConnection | undefined;
|
|
27
30
|
listServers(): Array<McpServerConfig & {
|
|
28
31
|
status: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../../server/mcp/manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAGnD,UAAU,aAAa;IACrB,MAAM,EAAE,eAAe,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,EAAE,oBAAoB,GAAG,IAAI,CAAC;IACvC,MAAM,EAAE,cAAc,GAAG,YAAY,GAAG,WAAW,GAAG,OAAO,CAAC;IAC9D,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAC5E,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../../server/mcp/manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAGnD,UAAU,aAAa;IACrB,MAAM,EAAE,eAAe,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,EAAE,oBAAoB,GAAG,IAAI,CAAC;IACvC,MAAM,EAAE,cAAc,GAAG,YAAY,GAAG,WAAW,GAAG,OAAO,CAAC;IAC9D,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAC5E,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,GAAG,IAAI,CAAC;CAClD;AAMD,qBAAa,UAAU;IACrB,OAAO,CAAC,WAAW,CAAoC;IAGvD,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAElC;IAGH,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAEhC;IAEH,OAAO,CAAC,kBAAkB;IAyC1B,OAAO,CAAC,sBAAsB;IAc9B,OAAO,CAAC,eAAe;IASvB,SAAS,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI;IAoBxC,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAM9B,OAAO,CAAC,iBAAiB;IAyBzB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS;IAIhD,WAAW,IAAI,KAAK,CAAC,eAAe,GAAG;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;QAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAS7G,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;KAAE,CAAC;IAkG/E,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC;IA8BvF,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB3C,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;QAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;CAW1H;AAED,eAAO,MAAM,UAAU,YAAmB,CAAC"}
|
|
@@ -2,6 +2,9 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
|
2
2
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
3
3
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
4
4
|
import { getToken } from '../services/mcpOAuth.js';
|
|
5
|
+
const MAX_RETRIES = 5;
|
|
6
|
+
const BACKOFF_BASE_MS = 1_000;
|
|
7
|
+
const BACKOFF_MAX_MS = 30_000;
|
|
5
8
|
export class McpManager {
|
|
6
9
|
connections = new Map();
|
|
7
10
|
// Allowlist of safe MCP executables - SECURITY FIX
|
|
@@ -80,11 +83,43 @@ export class McpManager {
|
|
|
80
83
|
tools: [],
|
|
81
84
|
connectedAt: null,
|
|
82
85
|
lastError: null,
|
|
86
|
+
retryCount: 0,
|
|
87
|
+
retryTimer: null,
|
|
83
88
|
});
|
|
84
89
|
}
|
|
85
90
|
removeServer(id) {
|
|
91
|
+
const conn = this.connections.get(id);
|
|
92
|
+
if (conn?.retryTimer)
|
|
93
|
+
clearTimeout(conn.retryTimer);
|
|
86
94
|
this.connections.delete(id);
|
|
87
95
|
}
|
|
96
|
+
scheduleReconnect(id) {
|
|
97
|
+
const conn = this.connections.get(id);
|
|
98
|
+
if (!conn)
|
|
99
|
+
return;
|
|
100
|
+
if (conn.retryCount >= MAX_RETRIES) {
|
|
101
|
+
console.error(`[McpManager] "${id}" exceeded max retries (${MAX_RETRIES}), giving up`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const delayMs = Math.min(BACKOFF_BASE_MS * Math.pow(2, conn.retryCount), BACKOFF_MAX_MS);
|
|
105
|
+
conn.retryCount += 1;
|
|
106
|
+
console.log(`[McpManager] "${id}" will reconnect in ${delayMs}ms (attempt ${conn.retryCount}/${MAX_RETRIES})`);
|
|
107
|
+
conn.retryTimer = setTimeout(async () => {
|
|
108
|
+
const c = this.connections.get(id);
|
|
109
|
+
if (!c || c.status === 'connected')
|
|
110
|
+
return;
|
|
111
|
+
try {
|
|
112
|
+
await this.connect(id);
|
|
113
|
+
c.retryCount = 0; // reset on success
|
|
114
|
+
console.log(`[McpManager] "${id}" reconnected successfully`);
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
118
|
+
console.warn(`[McpManager] "${id}" reconnect attempt failed: ${msg}`);
|
|
119
|
+
this.scheduleReconnect(id);
|
|
120
|
+
}
|
|
121
|
+
}, delayMs);
|
|
122
|
+
}
|
|
88
123
|
getServer(id) {
|
|
89
124
|
return this.connections.get(id);
|
|
90
125
|
}
|
|
@@ -147,13 +182,15 @@ export class McpManager {
|
|
|
147
182
|
env: { ...process.env, ...conn.config.env },
|
|
148
183
|
});
|
|
149
184
|
client = new Client({ name: 'modular-studio', version: '1.0.0' });
|
|
150
|
-
// Handle process exit
|
|
185
|
+
// Handle process exit — attempt auto-reconnect with exponential backoff
|
|
151
186
|
transport.onclose = () => {
|
|
152
187
|
if (conn.status === 'connected') {
|
|
153
188
|
conn.status = 'error';
|
|
154
189
|
conn.lastError = 'Process exited unexpectedly';
|
|
155
190
|
conn.client = null;
|
|
156
191
|
conn.transport = null;
|
|
192
|
+
console.warn(`[McpManager] "${id}" process exited unexpectedly — scheduling reconnect`);
|
|
193
|
+
this.scheduleReconnect(id);
|
|
157
194
|
}
|
|
158
195
|
};
|
|
159
196
|
await client.connect(transport);
|
|
@@ -161,6 +198,11 @@ export class McpManager {
|
|
|
161
198
|
conn.client = client;
|
|
162
199
|
conn.transport = transport;
|
|
163
200
|
conn.status = 'connected';
|
|
201
|
+
conn.retryCount = 0;
|
|
202
|
+
if (conn.retryTimer) {
|
|
203
|
+
clearTimeout(conn.retryTimer);
|
|
204
|
+
conn.retryTimer = null;
|
|
205
|
+
}
|
|
164
206
|
conn.tools = tools.map((t) => ({
|
|
165
207
|
name: t.name,
|
|
166
208
|
description: t.description,
|
|
@@ -182,13 +224,32 @@ export class McpManager {
|
|
|
182
224
|
if (!conn.client || conn.status !== 'connected') {
|
|
183
225
|
throw new Error(`MCP server "${id}" is not connected`);
|
|
184
226
|
}
|
|
185
|
-
const
|
|
186
|
-
|
|
227
|
+
const TIMEOUT_MS = 30_000;
|
|
228
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`MCP tool "${toolName}" on server "${id}" timed out after 30s`)), TIMEOUT_MS));
|
|
229
|
+
try {
|
|
230
|
+
const result = await Promise.race([
|
|
231
|
+
conn.client.callTool({ name: toolName, arguments: args }),
|
|
232
|
+
timeoutPromise,
|
|
233
|
+
]);
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
238
|
+
if (msg.includes('timed out')) {
|
|
239
|
+
console.warn(`[McpManager] ${msg}`);
|
|
240
|
+
}
|
|
241
|
+
throw err;
|
|
242
|
+
}
|
|
187
243
|
}
|
|
188
244
|
async disconnect(id) {
|
|
189
245
|
const conn = this.connections.get(id);
|
|
190
246
|
if (!conn)
|
|
191
247
|
throw new Error(`MCP server "${id}" not found`);
|
|
248
|
+
if (conn.retryTimer) {
|
|
249
|
+
clearTimeout(conn.retryTimer);
|
|
250
|
+
conn.retryTimer = null;
|
|
251
|
+
}
|
|
252
|
+
conn.retryCount = 0;
|
|
192
253
|
if (conn.client) {
|
|
193
254
|
try {
|
|
194
255
|
await conn.client.close();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database migration system for Modular Studio.
|
|
3
|
+
* Tracks schema version and runs pending migrations on startup.
|
|
4
|
+
*/
|
|
5
|
+
import type { Database } from 'sql.js';
|
|
6
|
+
/**
|
|
7
|
+
* Run all pending migrations on the given database.
|
|
8
|
+
* Called once during server startup after initDb().
|
|
9
|
+
*/
|
|
10
|
+
export declare function runMigrations(db: Database): void;
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../server/migrations/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAyCvC;;;GAGG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CAuBhD"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** All migrations ordered by version number */
|
|
2
|
+
const MIGRATIONS = [
|
|
3
|
+
{
|
|
4
|
+
version: 1,
|
|
5
|
+
description: 'Initial schema (baseline)',
|
|
6
|
+
// Version 1 is the baseline — tables are created by sqliteStore.ts initDb().
|
|
7
|
+
// This migration just marks the schema as versioned.
|
|
8
|
+
up: (_db) => { },
|
|
9
|
+
},
|
|
10
|
+
];
|
|
11
|
+
const CURRENT_SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version;
|
|
12
|
+
/** Ensure the schema_version tracking table exists */
|
|
13
|
+
function ensureVersionTable(db) {
|
|
14
|
+
db.run(`CREATE TABLE IF NOT EXISTS schema_version (
|
|
15
|
+
version INTEGER NOT NULL,
|
|
16
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
17
|
+
)`);
|
|
18
|
+
}
|
|
19
|
+
/** Get the currently applied schema version (0 if none) */
|
|
20
|
+
function getCurrentVersion(db) {
|
|
21
|
+
try {
|
|
22
|
+
const result = db.exec(`SELECT MAX(version) as v FROM schema_version`);
|
|
23
|
+
if (result.length === 0 || result[0].values.length === 0)
|
|
24
|
+
return 0;
|
|
25
|
+
const v = result[0].values[0][0];
|
|
26
|
+
return typeof v === 'number' ? v : 0;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Run all pending migrations on the given database.
|
|
34
|
+
* Called once during server startup after initDb().
|
|
35
|
+
*/
|
|
36
|
+
export function runMigrations(db) {
|
|
37
|
+
ensureVersionTable(db);
|
|
38
|
+
const currentVersion = getCurrentVersion(db);
|
|
39
|
+
if (currentVersion >= CURRENT_SCHEMA_VERSION) {
|
|
40
|
+
console.log(`[DB] Schema up-to-date (v${currentVersion})`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const pending = MIGRATIONS.filter((m) => m.version > currentVersion);
|
|
44
|
+
console.log(`[DB] Running ${pending.length} migration(s) (v${currentVersion} → v${CURRENT_SCHEMA_VERSION})`);
|
|
45
|
+
for (const migration of pending) {
|
|
46
|
+
try {
|
|
47
|
+
migration.up(db);
|
|
48
|
+
db.run(`INSERT INTO schema_version (version) VALUES (?)`, [migration.version]);
|
|
49
|
+
console.log(`[DB] Applied migration v${migration.version}: ${migration.description}`);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
53
|
+
console.error(`[DB] Migration v${migration.version} failed: ${msg}`);
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../../../server/routes/analytics.ts"],"names":[],"mappings":"AAIA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA2BxB,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { trackUsageEvent, getUsageStats } from '../services/sqliteStore.js';
|
|
3
|
+
const router = Router();
|
|
4
|
+
/** POST /api/analytics/track — record a usage event */
|
|
5
|
+
router.post('/track', async (req, res) => {
|
|
6
|
+
const { event, agentId, metadata } = req.body;
|
|
7
|
+
if (!event) {
|
|
8
|
+
res.status(400).json({ status: 'error', error: 'event is required' });
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
await trackUsageEvent(event, agentId, metadata);
|
|
12
|
+
res.json({ status: 'ok' });
|
|
13
|
+
});
|
|
14
|
+
/** GET /api/analytics/stats — get usage summary */
|
|
15
|
+
router.get('/stats', async (_req, res) => {
|
|
16
|
+
try {
|
|
17
|
+
const stats = await getUsageStats();
|
|
18
|
+
res.json({ status: 'ok', data: stats });
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
res.status(500).json({ status: 'error', error: err instanceof Error ? err.message : String(err) });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
export default router;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"airtable.d.ts","sourceRoot":"","sources":["../../../../server/routes/connectors/airtable.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,QAAA,MAAM,MAAM,4CAAW,CAAC;AA2GxB,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Airtable Connector — Bases + Tables
|
|
3
|
+
* Issue #95
|
|
4
|
+
*/
|
|
5
|
+
import { Router } from 'express';
|
|
6
|
+
import { rateLimitedFetch, fetchPaginated, toMarkdownTable, connectorError, getApiKey } from './shared.js';
|
|
7
|
+
const router = Router();
|
|
8
|
+
const sessionKeys = new Map();
|
|
9
|
+
const AT_API = 'https://api.airtable.com/v0';
|
|
10
|
+
function atHeaders(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 personal access token' });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const resp = await rateLimitedFetch('https://api.airtable.com/v0/meta/whoami', { headers: atHeaders(apiKey) });
|
|
22
|
+
if (!resp.ok) {
|
|
23
|
+
res.status(401).json({ status: 'error', error: 'Invalid Airtable token' });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const user = await resp.json();
|
|
27
|
+
sessionKeys.set('airtable', apiKey);
|
|
28
|
+
res.json({ status: 'ok', data: { userId: user.id } });
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
connectorError(res, 'Airtable', err);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
// ── List Bases ──
|
|
35
|
+
router.post('/bases', async (req, res) => {
|
|
36
|
+
const body = req.body;
|
|
37
|
+
const token = getApiKey('airtable', body, sessionKeys);
|
|
38
|
+
if (!token) {
|
|
39
|
+
res.status(401).json({ status: 'error', error: 'No Airtable token.' });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const resp = await rateLimitedFetch('https://api.airtable.com/v0/meta/bases', { headers: atHeaders(token) });
|
|
44
|
+
if (!resp.ok)
|
|
45
|
+
throw new Error(`Airtable API ${resp.status}`);
|
|
46
|
+
const data = await resp.json();
|
|
47
|
+
res.json({ status: 'ok', data: data.bases });
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
connectorError(res, 'Airtable', err);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
// ── Fetch Table Records ──
|
|
54
|
+
router.post('/fetch', async (req, res) => {
|
|
55
|
+
const body = req.body;
|
|
56
|
+
const token = getApiKey('airtable', body, sessionKeys);
|
|
57
|
+
if (!token) {
|
|
58
|
+
res.status(401).json({ status: 'error', error: 'No Airtable token.' });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const baseId = body.baseId;
|
|
62
|
+
const tableId = body.tableId;
|
|
63
|
+
const viewName = body.viewName;
|
|
64
|
+
if (!baseId || !tableId) {
|
|
65
|
+
res.status(400).json({ status: 'error', error: 'baseId and tableId required' });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const items = await fetchPaginated({
|
|
70
|
+
maxPages: 10,
|
|
71
|
+
maxItems: 500,
|
|
72
|
+
fetchPage: async (cursor) => {
|
|
73
|
+
const params = new URLSearchParams({ pageSize: '100' });
|
|
74
|
+
if (viewName)
|
|
75
|
+
params.set('view', viewName);
|
|
76
|
+
if (cursor)
|
|
77
|
+
params.set('offset', cursor);
|
|
78
|
+
const resp = await rateLimitedFetch(`${AT_API}/${baseId}/${encodeURIComponent(tableId)}?${params}`, { headers: atHeaders(token) });
|
|
79
|
+
if (!resp.ok)
|
|
80
|
+
throw new Error(`Airtable API ${resp.status}`);
|
|
81
|
+
const data = await resp.json();
|
|
82
|
+
return {
|
|
83
|
+
items: data.records.map((r) => ({
|
|
84
|
+
id: r.id,
|
|
85
|
+
fields: r.fields ?? {},
|
|
86
|
+
createdTime: r.createdTime,
|
|
87
|
+
})),
|
|
88
|
+
nextCursor: data.offset,
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
// Build markdown table from fields
|
|
93
|
+
const allKeys = new Set();
|
|
94
|
+
for (const item of items) {
|
|
95
|
+
for (const key of Object.keys(item.fields))
|
|
96
|
+
allKeys.add(key);
|
|
97
|
+
}
|
|
98
|
+
const headers = Array.from(allKeys);
|
|
99
|
+
const rows = items.map((item) => headers.map(h => {
|
|
100
|
+
const val = item.fields[h];
|
|
101
|
+
if (val === null || val === undefined)
|
|
102
|
+
return '';
|
|
103
|
+
if (Array.isArray(val))
|
|
104
|
+
return val.map(v => typeof v === 'object' ? JSON.stringify(v) : String(v)).join(', ');
|
|
105
|
+
if (typeof val === 'object')
|
|
106
|
+
return JSON.stringify(val);
|
|
107
|
+
return String(val);
|
|
108
|
+
}));
|
|
109
|
+
const markdown = `# ${tableId} (${items.length} records)\n\n` + toMarkdownTable(headers, rows);
|
|
110
|
+
res.json({
|
|
111
|
+
status: 'ok',
|
|
112
|
+
data: { items, markdown, count: items.length, tokens: Math.ceil(markdown.length / 4) },
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
connectorError(res, 'Airtable', err);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
export default router;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"confluence.d.ts","sourceRoot":"","sources":["../../../../server/routes/connectors/confluence.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,QAAA,MAAM,MAAM,4CAAW,CAAC;AAqLxB,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Confluence Connector — Pages + Spaces
|
|
3
|
+
* Issue #97
|
|
4
|
+
*/
|
|
5
|
+
import { Router } from 'express';
|
|
6
|
+
import { rateLimitedFetch, fetchPaginated, htmlToMarkdown, connectorError } from './shared.js';
|
|
7
|
+
const router = Router();
|
|
8
|
+
const sessionKeys = new Map();
|
|
9
|
+
function confHeaders(email, token) {
|
|
10
|
+
return {
|
|
11
|
+
Authorization: `Basic ${Buffer.from(`${email}:${token}`).toString('base64')}`,
|
|
12
|
+
Accept: 'application/json',
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
// ── Test ──
|
|
16
|
+
router.post('/test', async (req, res) => {
|
|
17
|
+
const { apiKey, email, domain } = req.body;
|
|
18
|
+
if (!apiKey || !email || !domain) {
|
|
19
|
+
res.status(400).json({ status: 'error', error: 'Missing apiKey, email, or domain (e.g., yoursite.atlassian.net)' });
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const resp = await rateLimitedFetch(`https://${domain}/wiki/api/v2/spaces?limit=1`, { headers: confHeaders(email, apiKey) });
|
|
24
|
+
if (!resp.ok) {
|
|
25
|
+
res.status(401).json({ status: 'error', error: 'Invalid Confluence credentials' });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
sessionKeys.set('confluence', JSON.stringify({ apiKey, email, domain }));
|
|
29
|
+
res.json({ status: 'ok', data: { connected: true } });
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
connectorError(res, 'Confluence', err);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
// ── List Spaces ──
|
|
36
|
+
router.post('/spaces', async (req, res) => {
|
|
37
|
+
const body = req.body;
|
|
38
|
+
const creds = getConfCreds(body);
|
|
39
|
+
if (!creds) {
|
|
40
|
+
res.status(401).json({ status: 'error', error: 'No Confluence credentials.' });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const resp = await rateLimitedFetch(`https://${creds.domain}/wiki/api/v2/spaces?limit=50`, { headers: confHeaders(creds.email, creds.apiKey) });
|
|
45
|
+
if (!resp.ok)
|
|
46
|
+
throw new Error(`Confluence API ${resp.status}`);
|
|
47
|
+
const data = await resp.json();
|
|
48
|
+
res.json({ status: 'ok', data: data.results });
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
connectorError(res, 'Confluence', err);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
// ── Fetch Pages ──
|
|
55
|
+
router.post('/fetch', async (req, res) => {
|
|
56
|
+
const body = req.body;
|
|
57
|
+
const creds = getConfCreds(body);
|
|
58
|
+
if (!creds) {
|
|
59
|
+
res.status(401).json({ status: 'error', error: 'No Confluence credentials.' });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const spaceKey = body.spaceKey;
|
|
63
|
+
const pageIds = body.pageIds;
|
|
64
|
+
try {
|
|
65
|
+
const items = [];
|
|
66
|
+
if (pageIds?.length) {
|
|
67
|
+
for (const id of pageIds.slice(0, 20)) {
|
|
68
|
+
const page = await fetchConfPage(id, creds);
|
|
69
|
+
if (page)
|
|
70
|
+
items.push(page);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else if (spaceKey) {
|
|
74
|
+
const pages = await fetchPaginated({
|
|
75
|
+
maxPages: 5,
|
|
76
|
+
maxItems: 50,
|
|
77
|
+
fetchPage: async (cursor) => {
|
|
78
|
+
const params = new URLSearchParams({ limit: '25', 'body-format': 'storage' });
|
|
79
|
+
if (cursor)
|
|
80
|
+
params.set('cursor', cursor);
|
|
81
|
+
const resp = await rateLimitedFetch(`https://${creds.domain}/wiki/api/v2/spaces/${spaceKey}/pages?${params}`, { headers: confHeaders(creds.email, creds.apiKey) });
|
|
82
|
+
if (!resp.ok)
|
|
83
|
+
throw new Error(`Confluence API ${resp.status}`);
|
|
84
|
+
const data = await resp.json();
|
|
85
|
+
const nextCursor = data._links?.next
|
|
86
|
+
? new URL(data._links.next, `https://${creds.domain}`).searchParams.get('cursor') ?? undefined
|
|
87
|
+
: undefined;
|
|
88
|
+
return {
|
|
89
|
+
items: data.results.map((p) => ({
|
|
90
|
+
id: p.id,
|
|
91
|
+
title: p.title,
|
|
92
|
+
body: p.body?.storage?.value ?? '',
|
|
93
|
+
})),
|
|
94
|
+
nextCursor,
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
for (const p of pages) {
|
|
99
|
+
const md = htmlToMarkdown(p.body);
|
|
100
|
+
items.push({ id: p.id, title: p.title, markdown: md, tokens: Math.ceil(md.length / 4) });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const fullMarkdown = items.map(i => `# ${i.title}\n\n${i.markdown}`).join('\n\n---\n\n');
|
|
104
|
+
res.json({
|
|
105
|
+
status: 'ok',
|
|
106
|
+
data: { items, markdown: fullMarkdown, count: items.length, tokens: Math.ceil(fullMarkdown.length / 4) },
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
connectorError(res, 'Confluence', err);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
// ── Search ──
|
|
114
|
+
router.post('/search', async (req, res) => {
|
|
115
|
+
const body = req.body;
|
|
116
|
+
const creds = getConfCreds(body);
|
|
117
|
+
if (!creds) {
|
|
118
|
+
res.status(401).json({ status: 'error', error: 'No Confluence credentials.' });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const query = body.query;
|
|
122
|
+
const spaceKey = body.spaceKey;
|
|
123
|
+
if (!query) {
|
|
124
|
+
res.status(400).json({ status: 'error', error: 'query required' });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const cql = spaceKey
|
|
129
|
+
? `type=page AND space="${spaceKey}" AND text~"${query}"`
|
|
130
|
+
: `type=page AND text~"${query}"`;
|
|
131
|
+
const resp = await rateLimitedFetch(`https://${creds.domain}/wiki/rest/api/search?cql=${encodeURIComponent(cql)}&limit=20`, { headers: confHeaders(creds.email, creds.apiKey) });
|
|
132
|
+
if (!resp.ok)
|
|
133
|
+
throw new Error(`Confluence search ${resp.status}`);
|
|
134
|
+
const data = await resp.json();
|
|
135
|
+
res.json({
|
|
136
|
+
status: 'ok',
|
|
137
|
+
data: data.results.map(r => ({
|
|
138
|
+
id: r.content.id,
|
|
139
|
+
title: r.content.title,
|
|
140
|
+
type: r.content.type,
|
|
141
|
+
})),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
connectorError(res, 'Confluence', err);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
function getConfCreds(body) {
|
|
149
|
+
const stored = sessionKeys.get('confluence');
|
|
150
|
+
if (stored) {
|
|
151
|
+
const parsed = JSON.parse(stored);
|
|
152
|
+
return {
|
|
153
|
+
apiKey: body.apiKey ?? parsed.apiKey,
|
|
154
|
+
email: body.email ?? parsed.email,
|
|
155
|
+
domain: body.domain ?? parsed.domain,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (body.apiKey && body.email && body.domain) {
|
|
159
|
+
return { apiKey: body.apiKey, email: body.email, domain: body.domain };
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
async function fetchConfPage(id, creds) {
|
|
164
|
+
try {
|
|
165
|
+
const resp = await rateLimitedFetch(`https://${creds.domain}/wiki/api/v2/pages/${id}?body-format=storage`, { headers: confHeaders(creds.email, creds.apiKey) });
|
|
166
|
+
if (!resp.ok)
|
|
167
|
+
return null;
|
|
168
|
+
const page = await resp.json();
|
|
169
|
+
const md = htmlToMarkdown(page.body?.storage?.value ?? '');
|
|
170
|
+
return { id: page.id, title: page.title, markdown: md, tokens: Math.ceil(md.length / 4) };
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
export default router;
|