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
|
@@ -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 @@
|
|
|
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(/ /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 @@
|
|
|
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;
|