@braidhq/source-loader-gdrive 0.0.1 → 0.2.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/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # @braidhq/source-loader-gdrive
2
+
3
+ Braid keeps a product's intent and its code aligned in one knowledge graph, and lets plugins feed that graph from outside sources. `@braidhq/source-loader-gdrive` is the plugin that exports Google Docs from a Drive folder as markdown, so a team's design docs become a Braid source.
4
+
5
+ ## Role
6
+
7
+ The drive loader walks a Drive folder, exports every Google Doc it finds as markdown, and keeps a manifest so later syncs touch only what changed.
8
+
9
+ - **The Export**: Each doc is exported to `<destination>/<sanitised-title>/index.md`, and its inlined base64 images are decoded into sibling files, so the markdown stays readable for humans and models.
10
+ - **The Manifest**: A `.braid-gdrive-manifest.json` records each doc's id, title, and modified time, so sync can add, update, rename, or remove docs against the prior state instead of re-downloading everything.
11
+ - **The Auth**: A fresh OAuth access token is resolved per workspace and source through an injected callback, so credentials stay in the composition root, never in config.
12
+
13
+ ## Structure
14
+
15
+ ```
16
+ src/
17
+ ├── GoogleDriveSourceLoaderPlugin.ts the gdrive loader, config, walk, export, manifest-diff sync
18
+ ├── DriveClient.ts the Drive REST client, list children and export a doc
19
+ ├── Manifest.ts the on-disk sync-state manifest, read and write
20
+ └── index.ts re-exports the factory and its config type
21
+ ```
22
+
23
+ - **GoogleDriveSourceLoaderPlugin**: The `createGoogleDriveLoader` factory. It walks the folder, filters by title, exports each doc, extracts inline images, and diffs against the manifest on sync.
24
+ - **DriveClient**: The thin Drive REST wrapper, listing folder children and exporting a doc to a given mime type.
25
+ - **Manifest**: The read and write of `.braid-gdrive-manifest.json`, the record sync diffs against.
26
+
27
+ ## Export and Layout
28
+
29
+ Drive folder hierarchy is flattened. Every matched doc lands in its own directory directly under the destination, named from the sanitised Drive title, so two docs that sanitise to the same name collide and one must be renamed in Drive. Auto-created `Copy of` duplicates are skipped, and title `include` and `exclude` regexes narrow what is exported while subfolders are still traversed when `recursive` is set.
30
+
31
+ Only Google Docs are in scope. Sheets, Slides, Drawings, and Forms are left for a different plugin. The `folderId` alias `root` is rejected outright, since it would mirror an entire My Drive.
32
+
33
+ ## Boundaries
34
+
35
+ - **Owns Its Directory**: The destination and its manifest are the loader's to write. Provision wipes and rebuilds it, sync reconciles it against Drive.
36
+ - **No Credentials On Disk**: The access token is resolved at run time through the injected callback. Only exported markdown and images land in the destination.
37
+ - **A Plugin, Not A Service**: It implements core's `SourceLoader` port through the sdk factory, and depends on the host to supply OAuth token resolution.
38
+
39
+ ## Dependencies
40
+
41
+ - **Depends On**: `@braidhq/core` for the port, `@braidhq/schema` for shared types, `@braidhq/sdk` for the factory, and `zod`.
42
+ - **Consumed By**: The server composition root, where `composeFsApp` registers the gdrive loader and wires `resolveAccessToken` to its OAuth store.
@@ -3,7 +3,7 @@ import { Buffer } from 'node:buffer';
3
3
  * Tiny Google Drive v3 client wrapper. We only use a few endpoints and
4
4
  * don't need the full `googleapis` package (which ships > 1 MB of types).
5
5
  *
6
- * Inject `fetchFn` for tests; real callers use globalThis.fetch.
6
+ * Inject `fetchFn` for tests. Real callers use globalThis.fetch.
7
7
  */
8
8
  export type FetchFn = typeof globalThis.fetch;
9
9
  export interface DriveFileMetadata {
@@ -21,8 +21,8 @@ export declare class DriveClient {
21
21
  listChildren(folderId: string): Promise<readonly DriveFileMetadata[]>;
22
22
  /** Export a Google native doc (gdoc / gsheet / gslides) to a chosen format. */
23
23
  exportDoc(fileId: string, mimeType: string): Promise<Buffer>;
24
- /** Download a binary file (image, PDF, ) as-is. */
24
+ /** Download a binary file (image, PDF, and so on) as-is. */
25
25
  downloadFile(fileId: string): Promise<Buffer>;
26
26
  private authHeader;
27
27
  }
28
- //# sourceMappingURL=driveClient.d.ts.map
28
+ //# sourceMappingURL=DriveClient.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"driveClient.d.ts","sourceRoot":"","sources":["../src/driveClient.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAMpC;;;;;GAKG;AACH,MAAM,MAAM,OAAO,GAAG,OAAO,UAAU,CAAC,KAAK,CAAA;AAE7C,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACrC;AAED,qBAAa,WAAW;IAEpB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO;gBADP,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,OAA0B;IAGtD,sDAAsD;IAChD,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC;IAsB3E,+EAA+E;IACzE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAQlE,oDAAoD;IAC9C,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAQnD,OAAO,CAAC,UAAU;CAGnB"}
1
+ {"version":3,"file":"DriveClient.d.ts","sourceRoot":"","sources":["../src/DriveClient.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAMpC;;;;;GAKG;AACH,MAAM,MAAM,OAAO,GAAG,OAAO,UAAU,CAAC,KAAK,CAAA;AAE7C,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACrC;AAED,qBAAa,WAAW;IAEpB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO;gBADP,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,OAA0B;IAGtD,sDAAsD;IAChD,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC;IAsB3E,+EAA+E;IACzE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAQlE,4DAA4D;IACtD,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAQnD,OAAO,CAAC,UAAU;CAGnB"}
@@ -39,7 +39,7 @@ export class DriveClient {
39
39
  throw await driveError(response, `exportDoc(${fileId},${mimeType})`);
40
40
  return globalBuffer().from(await response.arrayBuffer());
41
41
  }
42
- /** Download a binary file (image, PDF, ) as-is. */
42
+ /** Download a binary file (image, PDF, and so on) as-is. */
43
43
  async downloadFile(fileId) {
44
44
  const url = `https://www.googleapis.com/drive/v3/files/${fileId}?alt=media`;
45
45
  const response = await this.fetchFn(url, { headers: this.authHeader() });
@@ -55,4 +55,4 @@ async function driveError(response, op) {
55
55
  const text = await response.text().catch(() => '');
56
56
  return new Error(`Drive ${op} failed: ${response.status} ${response.statusText} ${text}`);
57
57
  }
58
- //# sourceMappingURL=driveClient.js.map
58
+ //# sourceMappingURL=DriveClient.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"driveClient.js","sourceRoot":"","sources":["../src/driveClient.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"DriveClient.js","sourceRoot":"","sources":["../src/DriveClient.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ import type { SourceLoaderContext, SourceLoaderPlugin } from '@braidhq/core';
2
+ import { z } from 'zod';
3
+ import { type DriveFileMetadata, type FetchFn } from './DriveClient.js';
4
+ export declare const GoogleDriveLoaderConfig: z.ZodObject<{
5
+ folderId: z.ZodString;
6
+ recursive: z.ZodDefault<z.ZodBoolean>;
7
+ include: z.ZodOptional<z.ZodString>;
8
+ exclude: z.ZodOptional<z.ZodString>;
9
+ }, z.core.$strip>;
10
+ export type GoogleDriveLoaderConfig = z.infer<typeof GoogleDriveLoaderConfig>;
11
+ export interface GoogleDriveLoaderDeps {
12
+ /**
13
+ * Resolve a fresh access token for a given `(workspaceId, sourceId)`.
14
+ * The composition root implements this against its OAuth refresh-token store.
15
+ * Tests can return a static token regardless of context.
16
+ */
17
+ resolveAccessToken: (context: SourceLoaderContext) => Promise<string>;
18
+ /** Inject for tests. Real callers use globalThis.fetch. */
19
+ fetchFn?: FetchFn;
20
+ }
21
+ /**
22
+ * Build a Google Drive source loader. Walks a Drive folder, exports every
23
+ * Google Doc inside as markdown, extracts inlined base64 images into
24
+ * sibling files, and lays everything out as:
25
+ *
26
+ * <destination>/
27
+ * <sanitised-doc-title>/
28
+ * index.md
29
+ * <image-label>.png (one per inlined image)
30
+ * <sanitised-doc-title-2>/
31
+ * index.md
32
+ * .braid-gdrive-manifest.json (sync state)
33
+ *
34
+ * Drive folder hierarchy is not preserved on disk.
35
+ * Every matched doc is flattened into a sibling directory under `destination/`.
36
+ * Two docs with the same sanitised title will collide,
37
+ * so rename one in Drive to disambiguate.
38
+ *
39
+ * Out of scope (intentional): Google Sheets, Slides, Drawings, Forms, and standalone binaries.
40
+ * Mirrors redoc's PRD-focused workflow.
41
+ * When you need spreadsheets or slide decks, provision them through a different source-loader plugin.
42
+ */
43
+ export declare function createGoogleDriveLoader(deps: GoogleDriveLoaderDeps): SourceLoaderPlugin;
44
+ /** Note: the helper used by DriveClient.exportDoc returns Buffer. */
45
+ export type { DriveFileMetadata, FetchFn };
46
+ //# sourceMappingURL=GoogleDriveSourceLoaderPlugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GoogleDriveSourceLoaderPlugin.d.ts","sourceRoot":"","sources":["../src/GoogleDriveSourceLoaderPlugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAK5E,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,EAAe,KAAK,iBAAiB,EAAE,KAAK,OAAO,EAAE,MAAM,kBAAkB,CAAA;AA4BpF,eAAO,MAAM,uBAAuB;;;;;iBAiClC,CAAA;AACF,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAA;AAE7E,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,kBAAkB,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IACrE,2DAA2D;IAC3D,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAaD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,qBAAqB,GAAG,kBAAkB,CAgHvF;AA+GD,qEAAqE;AACrE,YAAY,EAAE,iBAAiB,EAAE,OAAO,EAAE,CAAA"}
@@ -0,0 +1,289 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { mkdir, rm, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { defineSourceLoaderPlugin } from '@braidhq/sdk';
5
+ import { z } from 'zod';
6
+ import { DriveClient } from './DriveClient.js';
7
+ import { readManifest, writeManifest } from './Manifest.js';
8
+ const FOLDER_MIME = 'application/vnd.google-apps.folder';
9
+ const DOC_MIME = 'application/vnd.google-apps.document';
10
+ /**
11
+ * Drive auto-creates duplicates named "Copy of" (English) or ending in "的副本" (Chinese),
12
+ * when users use the Make a Copy menu.
13
+ * They almost always carry stale content that would conflict with the canonical doc,
14
+ * so we skip them silently. Same rule as redoc.
15
+ */
16
+ const COPY_PATTERNS = [/^Copy of /, /的副本$/];
17
+ /**
18
+ * Drive's `text/markdown` export inlines images as base64 data URIs,
19
+ * in reference-style link form:
20
+ *
21
+ * [image0]: <data:image/png;base64,iVBORw0KG...>
22
+ *
23
+ * Leaving those base64 blobs in the markdown bloats the file by ~33 % per image,
24
+ * and is unreadable for both humans and LLMs.
25
+ * This regex matches one such reference line,
26
+ * so we can extract the image to its own file and rewrite the link to a relative path.
27
+ * Same approach as redoc.
28
+ */
29
+ const INLINE_IMAGE_RE = /^\[([^\]]+)\]:\s*<data:image\/([\w+]+);base64,([^>]+)>$/gm;
30
+ export const GoogleDriveLoaderConfig = z.object({
31
+ /**
32
+ * Drive folder id (the long alphanumeric in the URL).
33
+ * Reject the alias `root` outright,
34
+ * since it expands to the user's entire My Drive and would mirror every file they own,
35
+ * which is almost never what someone wants from an "intent docs" source,
36
+ * and easily costs gigabytes and minutes.
37
+ * Make people pick a specific subfolder.
38
+ */
39
+ folderId: z.string()
40
+ .min(1)
41
+ .refine(value => value !== 'root', {
42
+ message: 'folderId "root" refers to the entire My Drive and is rejected. Create a dedicated subfolder and use its id instead.',
43
+ }),
44
+ /**
45
+ * Whether to traverse subfolders. Default true.
46
+ * Subfolder hierarchy is not preserved on disk.
47
+ * Every matching doc lives in its own dir directly under `destination/`.
48
+ * Disable to limit to immediate children of `folderId`.
49
+ */
50
+ recursive: z.boolean().default(true),
51
+ /**
52
+ * Optional regex (string).
53
+ * When set, only docs whose *Drive title* matches this pattern are downloaded.
54
+ * Subfolders are still traversed.
55
+ */
56
+ include: z.string().optional(),
57
+ /**
58
+ * Optional regex (string).
59
+ * When set, docs whose title matches are skipped.
60
+ * Evaluated after `include`, so exclude takes priority.
61
+ */
62
+ exclude: z.string().optional(),
63
+ });
64
+ /**
65
+ * Build a Google Drive source loader. Walks a Drive folder, exports every
66
+ * Google Doc inside as markdown, extracts inlined base64 images into
67
+ * sibling files, and lays everything out as:
68
+ *
69
+ * <destination>/
70
+ * <sanitised-doc-title>/
71
+ * index.md
72
+ * <image-label>.png (one per inlined image)
73
+ * <sanitised-doc-title-2>/
74
+ * index.md
75
+ * .braid-gdrive-manifest.json (sync state)
76
+ *
77
+ * Drive folder hierarchy is not preserved on disk.
78
+ * Every matched doc is flattened into a sibling directory under `destination/`.
79
+ * Two docs with the same sanitised title will collide,
80
+ * so rename one in Drive to disambiguate.
81
+ *
82
+ * Out of scope (intentional): Google Sheets, Slides, Drawings, Forms, and standalone binaries.
83
+ * Mirrors redoc's PRD-focused workflow.
84
+ * When you need spreadsheets or slide decks, provision them through a different source-loader plugin.
85
+ */
86
+ export function createGoogleDriveLoader(deps) {
87
+ return defineSourceLoaderPlugin({
88
+ kind: 'gdrive',
89
+ configSchema: GoogleDriveLoaderConfig,
90
+ provision: async (config, destination, context) => {
91
+ await rm(destination, { recursive: true, force: true });
92
+ await mkdir(destination, { recursive: true });
93
+ const client = await buildClient(deps, context);
94
+ const candidates = await walk(client, config);
95
+ const manifest = {
96
+ folderId: config.folderId,
97
+ include: config.include,
98
+ exclude: config.exclude,
99
+ files: {},
100
+ };
101
+ for (const doc of candidates) {
102
+ await downloadOne(client, doc, destination);
103
+ manifest.files[doc.id] = entryOf(doc);
104
+ }
105
+ await writeManifest(destination, manifest);
106
+ return {
107
+ localPath: destination,
108
+ metadata: { folderId: config.folderId, fileCount: candidates.length },
109
+ fetchedAt: new Date().toISOString(),
110
+ };
111
+ },
112
+ sync: async (config, destination, context) => {
113
+ const cached = await readManifest(destination);
114
+ if (!cached) {
115
+ // No manifest yet (first sync after upgrade or cache wiped).
116
+ // Fall back to a clean provision so we end up in a known-good state.
117
+ const client = await buildClient(deps, context);
118
+ await rm(destination, { recursive: true, force: true });
119
+ await mkdir(destination, { recursive: true });
120
+ const candidates = await walk(client, config);
121
+ const manifest = {
122
+ folderId: config.folderId,
123
+ include: config.include,
124
+ exclude: config.exclude,
125
+ files: {},
126
+ };
127
+ for (const doc of candidates) {
128
+ await downloadOne(client, doc, destination);
129
+ manifest.files[doc.id] = entryOf(doc);
130
+ }
131
+ await writeManifest(destination, manifest);
132
+ return {
133
+ changed: true,
134
+ added: candidates.length,
135
+ updated: 0,
136
+ removed: 0,
137
+ unchanged: 0,
138
+ metadata: { folderId: config.folderId, fileCount: candidates.length },
139
+ fetchedAt: new Date().toISOString(),
140
+ };
141
+ }
142
+ const client = await buildClient(deps, context);
143
+ const candidates = await walk(client, config);
144
+ const seen = new Set();
145
+ let added = 0;
146
+ let updated = 0;
147
+ let unchanged = 0;
148
+ const next = {
149
+ folderId: config.folderId,
150
+ include: config.include,
151
+ exclude: config.exclude,
152
+ files: {},
153
+ };
154
+ for (const doc of candidates) {
155
+ seen.add(doc.id);
156
+ const prior = cached.files[doc.id];
157
+ if (!prior) {
158
+ await downloadOne(client, doc, destination);
159
+ added++;
160
+ }
161
+ else if (prior.localDir !== doc.localDir || prior.modifiedTime !== doc.modifiedTime) {
162
+ // Either content updated or the doc was renamed in Drive.
163
+ // Both cases re-download to the new dir, then rm the old dir.
164
+ if (prior.localDir !== doc.localDir)
165
+ await rm(join(destination, prior.localDir), { recursive: true, force: true });
166
+ await downloadOne(client, doc, destination);
167
+ updated++;
168
+ }
169
+ else {
170
+ unchanged++;
171
+ }
172
+ next.files[doc.id] = entryOf(doc);
173
+ }
174
+ let removed = 0;
175
+ for (const [id, entry] of Object.entries(cached.files)) {
176
+ if (seen.has(id))
177
+ continue;
178
+ await rm(join(destination, entry.localDir), { recursive: true, force: true });
179
+ removed++;
180
+ }
181
+ await writeManifest(destination, next);
182
+ return {
183
+ changed: added + updated + removed > 0,
184
+ added,
185
+ updated,
186
+ removed,
187
+ unchanged,
188
+ metadata: { folderId: config.folderId },
189
+ fetchedAt: new Date().toISOString(),
190
+ };
191
+ },
192
+ });
193
+ }
194
+ async function buildClient(deps, context) {
195
+ const token = await deps.resolveAccessToken(context);
196
+ return new DriveClient(token, deps.fetchFn);
197
+ }
198
+ async function walk(client, config) {
199
+ const includeRe = compileRegex(config.include, 'include');
200
+ const excludeRe = compileRegex(config.exclude, 'exclude');
201
+ const out = [];
202
+ const seenDirs = new Map(); // localDir -> first doc id, to detect collisions
203
+ const visit = async (folderId) => {
204
+ const children = await client.listChildren(folderId);
205
+ for (const child of children) {
206
+ if (child.mimeType === FOLDER_MIME) {
207
+ if (config.recursive)
208
+ await visit(child.id);
209
+ continue;
210
+ }
211
+ if (child.mimeType !== DOC_MIME)
212
+ continue;
213
+ if (COPY_PATTERNS.some(re => re.test(child.name)))
214
+ continue;
215
+ if (includeRe && !includeRe.test(child.name))
216
+ continue;
217
+ if (excludeRe && excludeRe.test(child.name))
218
+ continue;
219
+ const localDir = sanitiseName(child.name);
220
+ const collides = seenDirs.get(localDir);
221
+ if (collides && collides !== child.id) {
222
+ // Two distinct Drive docs sanitise to the same dir name.
223
+ // Keep the first one we walked into.
224
+ // Skipping the second is safer than silently overwriting.
225
+ // User can rename in Drive.
226
+ continue;
227
+ }
228
+ seenDirs.set(localDir, child.id);
229
+ out.push({
230
+ id: child.id,
231
+ title: child.name,
232
+ modifiedTime: child.modifiedTime,
233
+ localDir,
234
+ });
235
+ }
236
+ };
237
+ await visit(config.folderId);
238
+ return out;
239
+ }
240
+ async function downloadOne(client, doc, destination) {
241
+ const docDir = join(destination, doc.localDir);
242
+ // Clean any prior content for this doc so removed images don't linger.
243
+ await rm(docDir, { recursive: true, force: true });
244
+ await mkdir(docDir, { recursive: true });
245
+ const bytes = await client.exportDoc(doc.id, 'text/markdown');
246
+ const markdown = bytes.toString('utf-8');
247
+ const rewritten = await extractInlineImages(markdown, docDir);
248
+ await writeFile(join(docDir, 'index.md'), rewritten, 'utf-8');
249
+ }
250
+ /**
251
+ * Pull every `[label]: <data:image/ext;base64,...>` reference out of the markdown,
252
+ * write the decoded bytes as `<label>.<ext>` in `docDir`,
253
+ * and rewrite the reference to point at the new local file.
254
+ * Returns the cleaned markdown. Matches redoc's behaviour.
255
+ */
256
+ async function extractInlineImages(markdown, docDir) {
257
+ const writes = [];
258
+ const rewritten = markdown.replace(INLINE_IMAGE_RE, (_match, label, ext, data) => {
259
+ const filename = `${sanitiseName(label)}.${ext}`;
260
+ writes.push(writeFile(join(docDir, filename), Buffer.from(data, 'base64')));
261
+ return `[${label}]: <${filename}>`;
262
+ });
263
+ await Promise.all(writes);
264
+ return rewritten;
265
+ }
266
+ function compileRegex(pattern, field) {
267
+ if (!pattern)
268
+ return undefined;
269
+ try {
270
+ return new RegExp(pattern);
271
+ }
272
+ catch (err) {
273
+ throw new Error(`googleDriveLoader: ${field} is not a valid regex (${err.message})`);
274
+ }
275
+ }
276
+ function entryOf(doc) {
277
+ return {
278
+ localDir: doc.localDir,
279
+ modifiedTime: doc.modifiedTime,
280
+ title: doc.title,
281
+ };
282
+ }
283
+ function sanitiseName(name) {
284
+ // Replace path separators, control chars, and common shell-hostile characters with `_`.
285
+ // Spaces stay so human-readable titles survive,
286
+ // for example "Roadmap Q3 2026" stays "Roadmap Q3 2026".
287
+ return name.replace(/[/\\:*?"<>|]/g, '_');
288
+ }
289
+ //# sourceMappingURL=GoogleDriveSourceLoaderPlugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GoogleDriveSourceLoaderPlugin.js","sourceRoot":"","sources":["../src/GoogleDriveSourceLoaderPlugin.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACvD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAA;AACvD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,EAAE,WAAW,EAAwC,MAAM,kBAAkB,CAAA;AACpF,OAAO,EAAqC,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAE9F,MAAM,WAAW,GAAG,oCAAoC,CAAA;AACxD,MAAM,QAAQ,GAAG,sCAAsC,CAAA;AAEvD;;;;;GAKG;AACH,MAAM,aAAa,GAAG,CAAC,WAAW,EAAE,MAAM,CAAC,CAAA;AAE3C;;;;;;;;;;;GAWG;AACH,MAAM,eAAe,GAAG,2DAA2D,CAAA;AAEnF,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9C;;;;;;;OAOG;IACH,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;SACjB,GAAG,CAAC,CAAC,CAAC;SACN,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,KAAK,MAAM,EAAE;QACjC,OAAO,EAAE,qHAAqH;KAC/H,CAAC;IACJ;;;;;OAKG;IACH,SAAS,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;IACpC;;;;OAIG;IACH,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC9B;;;;OAIG;IACH,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC/B,CAAC,CAAA;AAyBF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,uBAAuB,CAAC,IAA2B;IACjE,OAAO,wBAAwB,CAAC;QAC9B,IAAI,EAAE,QAAQ;QACd,YAAY,EAAE,uBAAuB;QACrC,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,EAAE;YAChD,MAAM,EAAE,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;YACvD,MAAM,KAAK,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;YAC7C,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;YAC/C,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;YAC7C,MAAM,QAAQ,GAAa;gBACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;gBACzB,OAAO,EAAE,MAAM,CAAC,OAAO;gBACvB,OAAO,EAAE,MAAM,CAAC,OAAO;gBACvB,KAAK,EAAE,EAAE;aACV,CAAA;YACD,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;gBAC7B,MAAM,WAAW,CAAC,MAAM,EAAE,GAAG,EAAE,WAAW,CAAC,CAAA;gBAC3C,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAA;YACvC,CAAC;YACD,MAAM,aAAa,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAA;YAC1C,OAAO;gBACL,SAAS,EAAE,WAAW;gBACtB,QAAQ,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,SAAS,EAAE,UAAU,CAAC,MAAM,EAAE;gBACrE,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAW;aAC7C,CAAA;QACH,CAAC;QACD,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,EAAE;YAC3C,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,WAAW,CAAC,CAAA;YAC9C,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,6DAA6D;gBAC7D,qEAAqE;gBACrE,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;gBAC/C,MAAM,EAAE,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;gBACvD,MAAM,KAAK,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;gBAC7C,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;gBAC7C,MAAM,QAAQ,GAAa;oBACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;oBACzB,OAAO,EAAE,MAAM,CAAC,OAAO;oBACvB,OAAO,EAAE,MAAM,CAAC,OAAO;oBACvB,KAAK,EAAE,EAAE;iBACV,CAAA;gBACD,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;oBAC7B,MAAM,WAAW,CAAC,MAAM,EAAE,GAAG,EAAE,WAAW,CAAC,CAAA;oBAC3C,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAA;gBACvC,CAAC;gBACD,MAAM,aAAa,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAA;gBAC1C,OAAO;oBACL,OAAO,EAAE,IAAI;oBACb,KAAK,EAAE,UAAU,CAAC,MAAM;oBACxB,OAAO,EAAE,CAAC;oBACV,OAAO,EAAE,CAAC;oBACV,SAAS,EAAE,CAAC;oBACZ,QAAQ,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,SAAS,EAAE,UAAU,CAAC,MAAM,EAAE;oBACrE,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAW;iBAC7C,CAAA;YACH,CAAC;YAED,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;YAC/C,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;YAC7C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAA;YAC9B,IAAI,KAAK,GAAG,CAAC,CAAA;YACb,IAAI,OAAO,GAAG,CAAC,CAAA;YACf,IAAI,SAAS,GAAG,CAAC,CAAA;YACjB,MAAM,IAAI,GAAa;gBACrB,QAAQ,EAAE,MAAM,CAAC,QAAQ;gBACzB,OAAO,EAAE,MAAM,CAAC,OAAO;gBACvB,OAAO,EAAE,MAAM,CAAC,OAAO;gBACvB,KAAK,EAAE,EAAE;aACV,CAAA;YAED,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;gBAC7B,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;gBAChB,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;gBAClC,IAAI,CAAC,KAAK,EAAE,CAAC;oBACX,MAAM,WAAW,CAAC,MAAM,EAAE,GAAG,EAAE,WAAW,CAAC,CAAA;oBAC3C,KAAK,EAAE,CAAA;gBACT,CAAC;qBACI,IAAI,KAAK,CAAC,QAAQ,KAAK,GAAG,CAAC,QAAQ,IAAI,KAAK,CAAC,YAAY,KAAK,GAAG,CAAC,YAAY,EAAE,CAAC;oBACpF,0DAA0D;oBAC1D,8DAA8D;oBAC9D,IAAI,KAAK,CAAC,QAAQ,KAAK,GAAG,CAAC,QAAQ;wBACjC,MAAM,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;oBAC/E,MAAM,WAAW,CAAC,MAAM,EAAE,GAAG,EAAE,WAAW,CAAC,CAAA;oBAC3C,OAAO,EAAE,CAAA;gBACX,CAAC;qBACI,CAAC;oBACJ,SAAS,EAAE,CAAA;gBACb,CAAC;gBACD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAA;YACnC,CAAC;YAED,IAAI,OAAO,GAAG,CAAC,CAAA;YACf,KAAK,MAAM,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;gBACvD,IAAI,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;oBACd,SAAQ;gBACV,MAAM,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;gBAC7E,OAAO,EAAE,CAAA;YACX,CAAC;YAED,MAAM,aAAa,CAAC,WAAW,EAAE,IAAI,CAAC,CAAA;YAEtC,OAAO;gBACL,OAAO,EAAE,KAAK,GAAG,OAAO,GAAG,OAAO,GAAG,CAAC;gBACtC,KAAK;gBACL,OAAO;gBACP,OAAO;gBACP,SAAS;gBACT,QAAQ,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE;gBACvC,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAW;aAC7C,CAAA;QACH,CAAC;KACF,CAAC,CAAA;AACJ,CAAC;AAED,KAAK,UAAU,WAAW,CAAC,IAA2B,EAAE,OAA4B;IAClF,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAA;IACpD,OAAO,IAAI,WAAW,CAAC,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,CAAA;AAC7C,CAAC;AAED,KAAK,UAAU,IAAI,CACjB,MAAmB,EACnB,MAA+B;IAE/B,MAAM,SAAS,GAAG,YAAY,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,CAAA;IACzD,MAAM,SAAS,GAAG,YAAY,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,CAAA;IACzD,MAAM,GAAG,GAAmB,EAAE,CAAA;IAC9B,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAA,CAAC,iDAAiD;IAC5F,MAAM,KAAK,GAAG,KAAK,EAAE,QAAgB,EAAiB,EAAE;QACtD,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAA;QACpD,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;YAC7B,IAAI,KAAK,CAAC,QAAQ,KAAK,WAAW,EAAE,CAAC;gBACnC,IAAI,MAAM,CAAC,SAAS;oBAClB,MAAM,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;gBACvB,SAAQ;YACV,CAAC;YACD,IAAI,KAAK,CAAC,QAAQ,KAAK,QAAQ;gBAC7B,SAAQ;YACV,IAAI,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBAC/C,SAAQ;YACV,IAAI,SAAS,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;gBAC1C,SAAQ;YACV,IAAI,SAAS,IAAI,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;gBACzC,SAAQ;YACV,MAAM,QAAQ,GAAG,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;YACzC,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;YACvC,IAAI,QAAQ,IAAI,QAAQ,KAAK,KAAK,CAAC,EAAE,EAAE,CAAC;gBACtC,yDAAyD;gBACzD,qCAAqC;gBACrC,0DAA0D;gBAC1D,4BAA4B;gBAC5B,SAAQ;YACV,CAAC;YACD,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,EAAE,CAAC,CAAA;YAChC,GAAG,CAAC,IAAI,CAAC;gBACP,EAAE,EAAE,KAAK,CAAC,EAAE;gBACZ,KAAK,EAAE,KAAK,CAAC,IAAI;gBACjB,YAAY,EAAE,KAAK,CAAC,YAAY;gBAChC,QAAQ;aACT,CAAC,CAAA;QACJ,CAAC;IACH,CAAC,CAAA;IACD,MAAM,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IAC5B,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,KAAK,UAAU,WAAW,CACxB,MAAmB,EACnB,GAAiB,EACjB,WAAmB;IAEnB,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAA;IAC9C,uEAAuE;IACvE,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;IAClD,MAAM,KAAK,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACxC,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,EAAE,eAAe,CAAC,CAAA;IAC7D,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;IACxC,MAAM,SAAS,GAAG,MAAM,mBAAmB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;IAC7D,MAAM,SAAS,CAAC,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,CAAA;AAC/D,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,mBAAmB,CAAC,QAAgB,EAAE,MAAc;IACjE,MAAM,MAAM,GAAoB,EAAE,CAAA;IAClC,MAAM,SAAS,GAAG,QAAQ,CAAC,OAAO,CAAC,eAAe,EAAE,CAAC,MAAM,EAAE,KAAa,EAAE,GAAW,EAAE,IAAY,EAAE,EAAE;QACvG,MAAM,QAAQ,GAAG,GAAG,YAAY,CAAC,KAAK,CAAC,IAAI,GAAG,EAAE,CAAA;QAChD,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAA;QAC3E,OAAO,IAAI,KAAK,OAAO,QAAQ,GAAG,CAAA;IACpC,CAAC,CAAC,CAAA;IACF,MAAM,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IACzB,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,SAAS,YAAY,CAAC,OAA2B,EAAE,KAA4B;IAC7E,IAAI,CAAC,OAAO;QACV,OAAO,SAAS,CAAA;IAClB,IAAI,CAAC;QACH,OAAO,IAAI,MAAM,CAAC,OAAO,CAAC,CAAA;IAC5B,CAAC;IACD,OAAO,GAAG,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,sBAAsB,KAAK,0BAA2B,GAAa,CAAC,OAAO,GAAG,CAAC,CAAA;IACjG,CAAC;AACH,CAAC;AAED,SAAS,OAAO,CAAC,GAAiB;IAChC,OAAO;QACL,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,YAAY,EAAE,GAAG,CAAC,YAAY;QAC9B,KAAK,EAAE,GAAG,CAAC,KAAK;KACjB,CAAA;AACH,CAAC;AAED,SAAS,YAAY,CAAC,IAAY;IAChC,wFAAwF;IACxF,gDAAgD;IAChD,yDAAyD;IACzD,OAAO,IAAI,CAAC,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC,CAAA;AAC3C,CAAC"}
@@ -0,0 +1,24 @@
1
+ /** Filename written inside `destination` to track per-file sync state. */
2
+ export declare const MANIFEST_FILENAME = ".braid-gdrive-manifest.json";
3
+ export interface ManifestEntry {
4
+ /** Posix relative dir under `destination`, the per-doc package. */
5
+ localDir: string;
6
+ modifiedTime: string;
7
+ /** Drive title at the time of the last sync, for debugging only. */
8
+ title: string;
9
+ }
10
+ export interface Manifest {
11
+ folderId: string;
12
+ include: string | undefined;
13
+ exclude: string | undefined;
14
+ files: Record<string, ManifestEntry>;
15
+ }
16
+ /**
17
+ * Read the manifest file from `destination`.
18
+ * Returns `undefined` when the file is missing or unparseable.
19
+ * Callers fall back to a clean provision in that case,
20
+ * since no manifest means no incremental state to diff against.
21
+ */
22
+ export declare function readManifest(destination: string): Promise<Manifest | undefined>;
23
+ export declare function writeManifest(destination: string, manifest: Manifest): Promise<void>;
24
+ //# sourceMappingURL=Manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Manifest.d.ts","sourceRoot":"","sources":["../src/Manifest.ts"],"names":[],"mappings":"AAGA,0EAA0E;AAC1E,eAAO,MAAM,iBAAiB,gCAAgC,CAAA;AAE9D,MAAM,WAAW,aAAa;IAC5B,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;IACpB,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;IAC3B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;IAC3B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;CACrC;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAQrF;AAED,wBAAsB,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAM1F"}
@@ -0,0 +1,23 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ /** Filename written inside `destination` to track per-file sync state. */
4
+ export const MANIFEST_FILENAME = '.braid-gdrive-manifest.json';
5
+ /**
6
+ * Read the manifest file from `destination`.
7
+ * Returns `undefined` when the file is missing or unparseable.
8
+ * Callers fall back to a clean provision in that case,
9
+ * since no manifest means no incremental state to diff against.
10
+ */
11
+ export async function readManifest(destination) {
12
+ try {
13
+ const raw = await readFile(join(destination, MANIFEST_FILENAME), 'utf-8');
14
+ return JSON.parse(raw);
15
+ }
16
+ catch {
17
+ return undefined;
18
+ }
19
+ }
20
+ export async function writeManifest(destination, manifest) {
21
+ await writeFile(join(destination, MANIFEST_FILENAME), `${JSON.stringify(manifest, null, 2)}\n`, 'utf-8');
22
+ }
23
+ //# sourceMappingURL=Manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Manifest.js","sourceRoot":"","sources":["../src/Manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACtD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEhC,0EAA0E;AAC1E,MAAM,CAAC,MAAM,iBAAiB,GAAG,6BAA6B,CAAA;AAiB9D;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,WAAmB;IACpD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,WAAW,EAAE,iBAAiB,CAAC,EAAE,OAAO,CAAC,CAAA;QACzE,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAa,CAAA;IACpC,CAAC;IACD,MAAM,CAAC;QACL,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,WAAmB,EAAE,QAAkB;IACzE,MAAM,SAAS,CACb,IAAI,CAAC,WAAW,EAAE,iBAAiB,CAAC,EACpC,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EACxC,OAAO,CACR,CAAA;AACH,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export * from './driveClient.js';
2
- export * from './GoogleDriveLoader.js';
1
+ export * from './DriveClient.js';
2
+ export * from './GoogleDriveSourceLoaderPlugin.js';
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAA;AAChC,cAAc,wBAAwB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAA;AAChC,cAAc,oCAAoC,CAAA"}
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- export * from './driveClient.js';
2
- export * from './GoogleDriveLoader.js';
1
+ export * from './DriveClient.js';
2
+ export * from './GoogleDriveSourceLoaderPlugin.js';
3
3
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAA;AAChC,cAAc,wBAAwB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAA;AAChC,cAAc,oCAAoC,CAAA"}
package/package.json CHANGED
@@ -1,37 +1,49 @@
1
1
  {
2
2
  "name": "@braidhq/source-loader-gdrive",
3
3
  "type": "module",
4
- "version": "0.0.1",
5
- "description": "Google Drive source-loader plugin: download text + images from a Drive folder into a workspace source path.",
4
+ "version": "0.2.0",
5
+ "description": "Google Drive source-loader plugin for Braid. Export docs from Google Drive, requires OAuth on first use.",
6
6
  "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/mroops0111/braid.git",
10
+ "directory": "packages/source-loader-gdrive"
11
+ },
7
12
  "exports": {
8
- ".": {
9
- "types": "./dist/index.d.ts",
10
- "default": "./dist/index.js"
11
- }
13
+ ".": "./src/index.ts"
12
14
  },
13
- "main": "./dist/index.js",
14
- "types": "./dist/index.d.ts",
15
+ "main": "./src/index.ts",
16
+ "types": "./src/index.ts",
15
17
  "files": [
16
18
  "README.md",
17
19
  "dist"
18
20
  ],
21
+ "scripts": {
22
+ "build": "tsc -p tsconfig.json",
23
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json",
24
+ "test": "vitest run --coverage",
25
+ "clean": "rm -rf dist .turbo *.tsbuildinfo coverage"
26
+ },
19
27
  "dependencies": {
20
- "zod": "^3.24.0",
21
- "@braidhq/core": "0.0.1",
22
- "@braidhq/schema": "0.0.1"
28
+ "@braidhq/core": "workspace:*",
29
+ "@braidhq/schema": "workspace:*",
30
+ "@braidhq/sdk": "workspace:*",
31
+ "zod": "^4.2.1"
23
32
  },
24
33
  "devDependencies": {
25
- "typescript": "^5.7.0",
26
- "vitest": "^2.1.0"
34
+ "@braidhq/test-utils": "workspace:*",
35
+ "typescript": "^6.0.3",
36
+ "vitest": "^4.1.10"
27
37
  },
28
38
  "publishConfig": {
29
- "access": "public"
30
- },
31
- "scripts": {
32
- "build": "tsc -p tsconfig.json",
33
- "typecheck": "tsc -p tsconfig.json --noEmit",
34
- "test": "vitest run",
35
- "clean": "rm -rf dist .turbo *.tsbuildinfo coverage"
39
+ "access": "public",
40
+ "main": "./dist/index.js",
41
+ "types": "./dist/index.d.ts",
42
+ "exports": {
43
+ ".": {
44
+ "types": "./dist/index.d.ts",
45
+ "default": "./dist/index.js"
46
+ }
47
+ }
36
48
  }
37
- }
49
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 mroops0111
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
@@ -1,80 +0,0 @@
1
- import type { IngestReport, SourceLoader, SourceLoaderContext, SyncReport } from '@braidhq/core';
2
- import type { AbsolutePath, LoaderKind, PluginId } from '@braidhq/schema';
3
- import { z } from 'zod';
4
- import { type FetchFn } from './driveClient.js';
5
- export declare const GoogleDriveLoaderConfig: z.ZodObject<{
6
- /**
7
- * Drive folder id (the long alphanumeric in the URL). Reject the alias
8
- * `root` outright: it expands to the user's entire My Drive and would
9
- * mirror every file they own, which is almost never what someone wants
10
- * from an "intent docs" source and easily costs gigabytes + minutes.
11
- * Make people pick a specific subfolder.
12
- */
13
- folderId: z.ZodEffects<z.ZodString, string, string>;
14
- /**
15
- * Whether to follow subfolders recursively. Default true. Disable for
16
- * flat folder mirrors where nested folders should be skipped.
17
- */
18
- recursive: z.ZodDefault<z.ZodBoolean>;
19
- }, "strip", z.ZodTypeAny, {
20
- folderId: string;
21
- recursive: boolean;
22
- }, {
23
- folderId: string;
24
- recursive?: boolean | undefined;
25
- }>;
26
- export type GoogleDriveLoaderConfig = z.infer<typeof GoogleDriveLoaderConfig>;
27
- export interface GoogleDriveLoaderDeps {
28
- /**
29
- * Resolve a fresh access token for a given `(workspaceId, sourceId)`.
30
- * The composition root implements this against its OAuth refresh-token
31
- * store; tests can return a static token regardless of context.
32
- */
33
- resolveAccessToken: (context: SourceLoaderContext) => Promise<string>;
34
- /** Inject for tests. Real callers use globalThis.fetch. */
35
- fetchFn?: FetchFn;
36
- }
37
- /**
38
- * Google Drive source loader. Walks a Drive folder, exports Google-native
39
- * docs to text/markdown/csv/pdf depending on type, downloads binaries
40
- * (images, PDFs) as-is, and mirrors the folder layout under `destination/`.
41
- *
42
- * Loader owns destination contents: it `rm -rf`'s `destination` on each
43
- * `ingest` so previously-fetched files don't linger. `sync` re-walks and
44
- * compares modifiedTime per file; reports `changed = true` if any file
45
- * was added / updated / removed.
46
- */
47
- export declare class GoogleDriveLoader implements SourceLoader {
48
- private readonly deps;
49
- readonly id: PluginId;
50
- readonly type: "source-loader";
51
- readonly kind: LoaderKind;
52
- readonly configSchema: z.ZodObject<{
53
- /**
54
- * Drive folder id (the long alphanumeric in the URL). Reject the alias
55
- * `root` outright: it expands to the user's entire My Drive and would
56
- * mirror every file they own, which is almost never what someone wants
57
- * from an "intent docs" source and easily costs gigabytes + minutes.
58
- * Make people pick a specific subfolder.
59
- */
60
- folderId: z.ZodEffects<z.ZodString, string, string>;
61
- /**
62
- * Whether to follow subfolders recursively. Default true. Disable for
63
- * flat folder mirrors where nested folders should be skipped.
64
- */
65
- recursive: z.ZodDefault<z.ZodBoolean>;
66
- }, "strip", z.ZodTypeAny, {
67
- folderId: string;
68
- recursive: boolean;
69
- }, {
70
- folderId: string;
71
- recursive?: boolean | undefined;
72
- }>;
73
- constructor(deps: GoogleDriveLoaderDeps);
74
- ingest(rawConfig: unknown, destination: AbsolutePath, context: SourceLoaderContext): Promise<IngestReport>;
75
- sync(rawConfig: unknown, destination: AbsolutePath, context: SourceLoaderContext): Promise<SyncReport>;
76
- private client;
77
- private walk;
78
- private writeOneFile;
79
- }
80
- //# sourceMappingURL=GoogleDriveLoader.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"GoogleDriveLoader.d.ts","sourceRoot":"","sources":["../src/GoogleDriveLoader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAChG,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAGzE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,EAAuC,KAAK,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAiBpF,eAAO,MAAM,uBAAuB;IAClC;;;;;;OAMG;;IAMH;;;OAGG;;;;;;;;EAEH,CAAA;AACF,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAA;AAE7E,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,kBAAkB,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IACrE,2DAA2D;IAC3D,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED;;;;;;;;;GASG;AACH,qBAAa,iBAAkB,YAAW,YAAY;IAMxC,OAAO,CAAC,QAAQ,CAAC,IAAI;IALjC,QAAQ,CAAC,EAAE,EAA6B,QAAQ,CAAA;IAChD,QAAQ,CAAC,IAAI,EAAG,eAAe,CAAS;IACxC,QAAQ,CAAC,IAAI,EAAe,UAAU,CAAA;IACtC,QAAQ,CAAC,YAAY;QA7CrB;;;;;;WAMG;;QAMH;;;WAGG;;;;;;;;OA8B4C;gBAElB,IAAI,EAAE,qBAAqB;IAElD,MAAM,CAAC,SAAS,EAAE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,YAAY,CAAC;IAa1G,IAAI,CAAC,SAAS,EAAE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,UAAU,CAAC;YAa9F,MAAM;YAKN,IAAI;YAyBJ,YAAY;CAoB3B"}
@@ -1,125 +0,0 @@
1
- import { mkdir, rm, writeFile } from 'node:fs/promises';
2
- import { join } from 'node:path';
3
- import { z } from 'zod';
4
- import { DriveClient } from './driveClient.js';
5
- const FOLDER_MIME = 'application/vnd.google-apps.folder';
6
- /**
7
- * Map of Google-native mime types to (Drive export format, filename
8
- * extension we write to disk). Skip everything else: only Drive
9
- * "applications" need exporting. Regular binaries (images, PDFs, etc.)
10
- * go through `downloadFile`.
11
- */
12
- const GOOGLE_NATIVE_EXPORT = {
13
- 'application/vnd.google-apps.document': { mimeType: 'text/markdown', extension: '.md' },
14
- 'application/vnd.google-apps.spreadsheet': { mimeType: 'text/csv', extension: '.csv' },
15
- 'application/vnd.google-apps.presentation': { mimeType: 'application/pdf', extension: '.pdf' },
16
- 'application/vnd.google-apps.drawing': { mimeType: 'image/png', extension: '.png' },
17
- };
18
- export const GoogleDriveLoaderConfig = z.object({
19
- /**
20
- * Drive folder id (the long alphanumeric in the URL). Reject the alias
21
- * `root` outright: it expands to the user's entire My Drive and would
22
- * mirror every file they own, which is almost never what someone wants
23
- * from an "intent docs" source and easily costs gigabytes + minutes.
24
- * Make people pick a specific subfolder.
25
- */
26
- folderId: z.string()
27
- .min(1)
28
- .refine(value => value !== 'root', {
29
- message: 'folderId "root" refers to the entire My Drive and is rejected. Create a dedicated subfolder and use its id instead.',
30
- }),
31
- /**
32
- * Whether to follow subfolders recursively. Default true. Disable for
33
- * flat folder mirrors where nested folders should be skipped.
34
- */
35
- recursive: z.boolean().default(true),
36
- });
37
- /**
38
- * Google Drive source loader. Walks a Drive folder, exports Google-native
39
- * docs to text/markdown/csv/pdf depending on type, downloads binaries
40
- * (images, PDFs) as-is, and mirrors the folder layout under `destination/`.
41
- *
42
- * Loader owns destination contents: it `rm -rf`'s `destination` on each
43
- * `ingest` so previously-fetched files don't linger. `sync` re-walks and
44
- * compares modifiedTime per file; reports `changed = true` if any file
45
- * was added / updated / removed.
46
- */
47
- export class GoogleDriveLoader {
48
- deps;
49
- id = 'source-loader-gdrive';
50
- type = 'source-loader';
51
- kind = 'gdrive';
52
- configSchema = GoogleDriveLoaderConfig;
53
- constructor(deps) {
54
- this.deps = deps;
55
- }
56
- async ingest(rawConfig, destination, context) {
57
- const config = GoogleDriveLoaderConfig.parse(rawConfig);
58
- await rm(destination, { recursive: true, force: true });
59
- await mkdir(destination, { recursive: true });
60
- const client = await this.client(context);
61
- const result = await this.walk(client, config.folderId, destination, { recursive: config.recursive });
62
- return {
63
- localPath: destination,
64
- metadata: { folderId: config.folderId, fileCount: result.fileCount },
65
- fetchedAt: new Date().toISOString(),
66
- };
67
- }
68
- async sync(rawConfig, destination, context) {
69
- // v0.1: incremental sync needs per-file mtime cache. Until we add
70
- // that, sync is "blow away + re-ingest". Always safe, just slow.
71
- const config = GoogleDriveLoaderConfig.parse(rawConfig);
72
- const before = Date.now();
73
- await this.ingest(config, destination, context);
74
- return {
75
- changed: true,
76
- metadata: { folderId: config.folderId, syncedAt: new Date(before).toISOString() },
77
- fetchedAt: new Date().toISOString(),
78
- };
79
- }
80
- async client(context) {
81
- const token = await this.deps.resolveAccessToken(context);
82
- return new DriveClient(token, this.deps.fetchFn);
83
- }
84
- async walk(client, folderId, destination, options) {
85
- let fileCount = 0;
86
- const children = await client.listChildren(folderId);
87
- for (const child of children) {
88
- if (child.mimeType === FOLDER_MIME) {
89
- if (!options.recursive)
90
- continue;
91
- const subDest = join(destination, sanitiseName(child.name));
92
- await mkdir(subDest, { recursive: true });
93
- const nested = await this.walk(client, child.id, subDest, options);
94
- fileCount += nested.fileCount;
95
- continue;
96
- }
97
- const written = await this.writeOneFile(client, child, destination);
98
- if (written)
99
- fileCount++;
100
- }
101
- return { fileCount };
102
- }
103
- async writeOneFile(client, file, destination) {
104
- const exportSpec = GOOGLE_NATIVE_EXPORT[file.mimeType];
105
- if (exportSpec) {
106
- const bytes = await client.exportDoc(file.id, exportSpec.mimeType);
107
- const filename = `${sanitiseName(file.name)}${exportSpec.extension}`;
108
- await writeFile(join(destination, filename), bytes);
109
- return true;
110
- }
111
- // Skip native Drive types we don't have an export for (e.g. forms).
112
- if (file.mimeType.startsWith('application/vnd.google-apps.'))
113
- return false;
114
- // Regular binary: download as-is, preserve the name.
115
- const bytes = await client.downloadFile(file.id);
116
- await writeFile(join(destination, sanitiseName(file.name)), bytes);
117
- return true;
118
- }
119
- }
120
- function sanitiseName(name) {
121
- // Replace path separators and control chars; leave the rest alone so
122
- // human-readable filenames survive ('My Doc — v3.md').
123
- return name.replace(/[/\\]/g, '_');
124
- }
125
- //# sourceMappingURL=GoogleDriveLoader.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"GoogleDriveLoader.js","sourceRoot":"","sources":["../src/GoogleDriveLoader.ts"],"names":[],"mappings":"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"}