@soederpop/luca 0.0.2
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/CLAUDE.md +71 -0
- package/README.md +78 -0
- package/bun.lock +2928 -0
- package/bunfig.toml +3 -0
- package/commands/audit-docs.ts +740 -0
- package/commands/build-scaffolds.ts +154 -0
- package/commands/generate-api-docs.ts +114 -0
- package/commands/update-introspection.ts +67 -0
- package/docs/CLI.md +335 -0
- package/docs/README.md +88 -0
- package/docs/TABLE-OF-CONTENTS.md +157 -0
- package/docs/apis/clients/elevenlabs.md +84 -0
- package/docs/apis/clients/graph.md +56 -0
- package/docs/apis/clients/openai.md +69 -0
- package/docs/apis/clients/rest.md +41 -0
- package/docs/apis/clients/websocket.md +107 -0
- package/docs/apis/features/agi/assistant.md +471 -0
- package/docs/apis/features/agi/assistants-manager.md +154 -0
- package/docs/apis/features/agi/claude-code.md +602 -0
- package/docs/apis/features/agi/conversation-history.md +352 -0
- package/docs/apis/features/agi/conversation.md +333 -0
- package/docs/apis/features/agi/docs-reader.md +121 -0
- package/docs/apis/features/agi/openai-codex.md +318 -0
- package/docs/apis/features/agi/openapi.md +138 -0
- package/docs/apis/features/agi/semantic-search.md +387 -0
- package/docs/apis/features/agi/skills-library.md +216 -0
- package/docs/apis/features/node/container-link.md +133 -0
- package/docs/apis/features/node/content-db.md +313 -0
- package/docs/apis/features/node/disk-cache.md +379 -0
- package/docs/apis/features/node/dns.md +651 -0
- package/docs/apis/features/node/docker.md +705 -0
- package/docs/apis/features/node/downloader.md +81 -0
- package/docs/apis/features/node/esbuild.md +59 -0
- package/docs/apis/features/node/file-manager.md +182 -0
- package/docs/apis/features/node/fs.md +581 -0
- package/docs/apis/features/node/git.md +330 -0
- package/docs/apis/features/node/google-auth.md +174 -0
- package/docs/apis/features/node/google-calendar.md +187 -0
- package/docs/apis/features/node/google-docs.md +151 -0
- package/docs/apis/features/node/google-drive.md +225 -0
- package/docs/apis/features/node/google-sheets.md +179 -0
- package/docs/apis/features/node/grep.md +290 -0
- package/docs/apis/features/node/helpers.md +135 -0
- package/docs/apis/features/node/ink.md +334 -0
- package/docs/apis/features/node/ipc-socket.md +260 -0
- package/docs/apis/features/node/json-tree.md +86 -0
- package/docs/apis/features/node/launcher-app-command-listener.md +145 -0
- package/docs/apis/features/node/networking.md +281 -0
- package/docs/apis/features/node/nlp.md +133 -0
- package/docs/apis/features/node/opener.md +97 -0
- package/docs/apis/features/node/os.md +118 -0
- package/docs/apis/features/node/package-finder.md +402 -0
- package/docs/apis/features/node/postgres.md +212 -0
- package/docs/apis/features/node/proc.md +430 -0
- package/docs/apis/features/node/process-manager.md +210 -0
- package/docs/apis/features/node/python.md +278 -0
- package/docs/apis/features/node/repl.md +88 -0
- package/docs/apis/features/node/runpod.md +673 -0
- package/docs/apis/features/node/secure-shell.md +169 -0
- package/docs/apis/features/node/semantic-search.md +401 -0
- package/docs/apis/features/node/sqlite.md +211 -0
- package/docs/apis/features/node/telegram.md +254 -0
- package/docs/apis/features/node/tts.md +118 -0
- package/docs/apis/features/node/ui.md +703 -0
- package/docs/apis/features/node/vault.md +64 -0
- package/docs/apis/features/node/vm.md +84 -0
- package/docs/apis/features/node/window-manager.md +337 -0
- package/docs/apis/features/node/yaml-tree.md +85 -0
- package/docs/apis/features/node/yaml.md +176 -0
- package/docs/apis/features/web/asset-loader.md +47 -0
- package/docs/apis/features/web/container-link.md +133 -0
- package/docs/apis/features/web/esbuild.md +59 -0
- package/docs/apis/features/web/helpers.md +135 -0
- package/docs/apis/features/web/network.md +30 -0
- package/docs/apis/features/web/speech.md +55 -0
- package/docs/apis/features/web/vault.md +64 -0
- package/docs/apis/features/web/vm.md +84 -0
- package/docs/apis/features/web/voice.md +67 -0
- package/docs/apis/servers/express.md +127 -0
- package/docs/apis/servers/mcp.md +213 -0
- package/docs/apis/servers/websocket.md +99 -0
- package/docs/documentation-audit.md +134 -0
- package/docs/examples/content-db.md +77 -0
- package/docs/examples/disk-cache.md +83 -0
- package/docs/examples/docker.md +101 -0
- package/docs/examples/downloader.md +70 -0
- package/docs/examples/esbuild.md +80 -0
- package/docs/examples/file-manager.md +82 -0
- package/docs/examples/fs.md +83 -0
- package/docs/examples/git.md +85 -0
- package/docs/examples/google-auth.md +88 -0
- package/docs/examples/google-calendar.md +94 -0
- package/docs/examples/google-docs.md +82 -0
- package/docs/examples/google-drive.md +96 -0
- package/docs/examples/google-sheets.md +95 -0
- package/docs/examples/grep.md +85 -0
- package/docs/examples/ink-blocks.md +75 -0
- package/docs/examples/ink-renderer.md +41 -0
- package/docs/examples/ink.md +103 -0
- package/docs/examples/ipc-socket.md +103 -0
- package/docs/examples/json-tree.md +91 -0
- package/docs/examples/launcher-app-command-listener.md +120 -0
- package/docs/examples/networking.md +58 -0
- package/docs/examples/nlp.md +91 -0
- package/docs/examples/opener.md +78 -0
- package/docs/examples/os.md +72 -0
- package/docs/examples/package-finder.md +89 -0
- package/docs/examples/port-exposer.md +89 -0
- package/docs/examples/postgres.md +91 -0
- package/docs/examples/proc.md +81 -0
- package/docs/examples/process-manager.md +79 -0
- package/docs/examples/python.md +91 -0
- package/docs/examples/repl.md +93 -0
- package/docs/examples/runpod.md +119 -0
- package/docs/examples/secure-shell.md +92 -0
- package/docs/examples/sqlite.md +86 -0
- package/docs/examples/telegram.md +77 -0
- package/docs/examples/tts.md +86 -0
- package/docs/examples/ui.md +80 -0
- package/docs/examples/vault.md +70 -0
- package/docs/examples/vm.md +86 -0
- package/docs/examples/window-manager.md +125 -0
- package/docs/examples/yaml-tree.md +93 -0
- package/docs/examples/yaml.md +104 -0
- package/docs/ideas/class-registration-refactor-possibilities.md +197 -0
- package/docs/ideas/container-use-api.md +9 -0
- package/docs/ideas/easy-auth-for-express-servers-and-luca-serve.md +0 -0
- package/docs/ideas/feature-stacks.md +22 -0
- package/docs/ideas/luca-cli-self-sufficiency-demo.md +23 -0
- package/docs/ideas/mcp-design.md +9 -0
- package/docs/ideas/web-container-debugging-feature.md +13 -0
- package/docs/introspection-audit.md +49 -0
- package/docs/introspection.md +154 -0
- package/docs/mcp/readme.md +162 -0
- package/docs/models.ts +38 -0
- package/docs/philosophy.md +85 -0
- package/docs/principles.md +7 -0
- package/docs/prompts/audit-codebase-for-failures-to-use-the-container.md +34 -0
- package/docs/prompts/mcp-test-easy-command.md +27 -0
- package/docs/reports/assistant-bugs.md +38 -0
- package/docs/reports/attach-pattern-usage.md +18 -0
- package/docs/reports/code-audit-results.md +391 -0
- package/docs/reports/introspection-audit-tasks.md +378 -0
- package/docs/reports/luca-mcp-improvements.md +128 -0
- package/docs/scaffolds/client.md +140 -0
- package/docs/scaffolds/command.md +106 -0
- package/docs/scaffolds/endpoint.md +176 -0
- package/docs/scaffolds/feature.md +148 -0
- package/docs/scaffolds/server.md +187 -0
- package/docs/tasks/web-container-helper-discovery.md +71 -0
- package/docs/todos.md +1 -0
- package/docs/tutorials/01-getting-started.md +106 -0
- package/docs/tutorials/02-container.md +210 -0
- package/docs/tutorials/03-scripts.md +194 -0
- package/docs/tutorials/04-features-overview.md +196 -0
- package/docs/tutorials/05-state-and-events.md +171 -0
- package/docs/tutorials/06-servers.md +157 -0
- package/docs/tutorials/07-endpoints.md +198 -0
- package/docs/tutorials/08-commands.md +171 -0
- package/docs/tutorials/09-clients.md +162 -0
- package/docs/tutorials/10-creating-features.md +198 -0
- package/docs/tutorials/11-contentbase.md +191 -0
- package/docs/tutorials/12-assistants.md +215 -0
- package/docs/tutorials/13-introspection.md +147 -0
- package/docs/tutorials/14-type-system.md +174 -0
- package/docs/tutorials/15-project-patterns.md +222 -0
- package/docs/tutorials/16-google-features.md +534 -0
- package/docs/tutorials/17-tui-blocks.md +530 -0
- package/docs/tutorials/18-semantic-search.md +334 -0
- package/index.ts +1 -0
- package/luca.console.ts +9 -0
- package/main.py +6 -0
- package/package.json +154 -0
- package/pyproject.toml +7 -0
- package/scripts/animations/chrome-glitch.ts +55 -0
- package/scripts/animations/index.ts +16 -0
- package/scripts/animations/neon-pulse.ts +64 -0
- package/scripts/animations/types.ts +6 -0
- package/scripts/build-web.ts +28 -0
- package/scripts/examples/ask-luca-expert.ts +42 -0
- package/scripts/examples/assistant-questions.ts +12 -0
- package/scripts/examples/excalidraw-expert.ts +75 -0
- package/scripts/examples/expert-chat.ts +0 -0
- package/scripts/examples/file-manager.ts +14 -0
- package/scripts/examples/ideas.ts +12 -0
- package/scripts/examples/interactive-chat.ts +20 -0
- package/scripts/examples/openai-tool-calls.ts +113 -0
- package/scripts/examples/opening-a-web-browser.ts +5 -0
- package/scripts/examples/telegram-bot.ts +79 -0
- package/scripts/examples/telegram-ink-ui.ts +302 -0
- package/scripts/examples/using-assistant-with-mcp.ts +560 -0
- package/scripts/examples/using-claude-code.ts +10 -0
- package/scripts/examples/using-contentdb.ts +35 -0
- package/scripts/examples/using-conversations.ts +35 -0
- package/scripts/examples/using-disk-cache.ts +10 -0
- package/scripts/examples/using-docker-shell.ts +75 -0
- package/scripts/examples/using-elevenlabs.ts +25 -0
- package/scripts/examples/using-google-calendar.ts +57 -0
- package/scripts/examples/using-google-docs.ts +74 -0
- package/scripts/examples/using-google-drive.ts +74 -0
- package/scripts/examples/using-google-sheets.ts +89 -0
- package/scripts/examples/using-nlp.ts +55 -0
- package/scripts/examples/using-ollama.ts +10 -0
- package/scripts/examples/using-openai-codex.ts +23 -0
- package/scripts/examples/using-postgres.ts +55 -0
- package/scripts/examples/using-runpod.ts +32 -0
- package/scripts/examples/using-tts.ts +40 -0
- package/scripts/examples/vm-loading-esm-modules.ts +16 -0
- package/scripts/scaffold.ts +391 -0
- package/scripts/scratch.ts +15 -0
- package/scripts/test-command-listener.ts +123 -0
- package/scripts/test-window-manager-lifecycle.ts +86 -0
- package/scripts/test-window-manager.ts +43 -0
- package/scripts/update-introspection-data.ts +58 -0
- package/src/agi/README.md +14 -0
- package/src/agi/container.server.ts +114 -0
- package/src/agi/endpoints/ask.ts +60 -0
- package/src/agi/endpoints/conversations/[id].ts +45 -0
- package/src/agi/endpoints/conversations.ts +31 -0
- package/src/agi/endpoints/experts.ts +37 -0
- package/src/agi/features/assistant.ts +767 -0
- package/src/agi/features/assistants-manager.ts +260 -0
- package/src/agi/features/claude-code.ts +1111 -0
- package/src/agi/features/conversation-history.ts +497 -0
- package/src/agi/features/conversation.ts +799 -0
- package/src/agi/features/openai-codex.ts +631 -0
- package/src/agi/features/openapi.ts +438 -0
- package/src/agi/features/skills-library.ts +425 -0
- package/src/agi/index.ts +6 -0
- package/src/agi/lib/token-counter.ts +122 -0
- package/src/browser.ts +25 -0
- package/src/bus.ts +100 -0
- package/src/cli/cli.ts +70 -0
- package/src/client.ts +461 -0
- package/src/clients/civitai/index.ts +541 -0
- package/src/clients/client-template.ts +41 -0
- package/src/clients/comfyui/index.ts +597 -0
- package/src/clients/elevenlabs/index.ts +291 -0
- package/src/clients/openai/index.ts +451 -0
- package/src/clients/supabase/index.ts +366 -0
- package/src/command.ts +164 -0
- package/src/commands/chat.ts +182 -0
- package/src/commands/console.ts +192 -0
- package/src/commands/describe.ts +433 -0
- package/src/commands/eval.ts +116 -0
- package/src/commands/help.ts +214 -0
- package/src/commands/index.ts +14 -0
- package/src/commands/mcp.ts +64 -0
- package/src/commands/prompt.ts +807 -0
- package/src/commands/run.ts +257 -0
- package/src/commands/sandbox-mcp.ts +439 -0
- package/src/commands/scaffold.ts +79 -0
- package/src/commands/serve.ts +172 -0
- package/src/container.ts +781 -0
- package/src/endpoint.ts +340 -0
- package/src/feature.ts +75 -0
- package/src/hash-object.ts +97 -0
- package/src/helper.ts +543 -0
- package/src/introspection/generated.agi.ts +23388 -0
- package/src/introspection/generated.node.ts +18899 -0
- package/src/introspection/generated.web.ts +2021 -0
- package/src/introspection/index.ts +256 -0
- package/src/introspection/scan.ts +912 -0
- package/src/node/container.ts +354 -0
- package/src/node/feature.ts +13 -0
- package/src/node/features/container-link.ts +558 -0
- package/src/node/features/content-db.ts +475 -0
- package/src/node/features/disk-cache.ts +382 -0
- package/src/node/features/dns.ts +655 -0
- package/src/node/features/docker.ts +912 -0
- package/src/node/features/downloader.ts +92 -0
- package/src/node/features/esbuild.ts +68 -0
- package/src/node/features/file-manager.ts +357 -0
- package/src/node/features/fs.ts +534 -0
- package/src/node/features/git.ts +492 -0
- package/src/node/features/google-auth.ts +502 -0
- package/src/node/features/google-calendar.ts +300 -0
- package/src/node/features/google-docs.ts +404 -0
- package/src/node/features/google-drive.ts +339 -0
- package/src/node/features/google-sheets.ts +279 -0
- package/src/node/features/grep.ts +406 -0
- package/src/node/features/helpers.ts +374 -0
- package/src/node/features/ink.ts +490 -0
- package/src/node/features/ipc-socket.ts +459 -0
- package/src/node/features/json-tree.ts +188 -0
- package/src/node/features/launcher-app-command-listener.ts +388 -0
- package/src/node/features/networking.ts +925 -0
- package/src/node/features/nlp.ts +211 -0
- package/src/node/features/opener.ts +166 -0
- package/src/node/features/os.ts +157 -0
- package/src/node/features/package-finder.ts +539 -0
- package/src/node/features/port-exposer.ts +342 -0
- package/src/node/features/postgres.ts +273 -0
- package/src/node/features/proc.ts +502 -0
- package/src/node/features/process-manager.ts +542 -0
- package/src/node/features/python.ts +444 -0
- package/src/node/features/repl.ts +194 -0
- package/src/node/features/runpod.ts +802 -0
- package/src/node/features/secure-shell.ts +248 -0
- package/src/node/features/semantic-search.ts +924 -0
- package/src/node/features/sqlite.ts +289 -0
- package/src/node/features/telegram.ts +342 -0
- package/src/node/features/tts.ts +184 -0
- package/src/node/features/ui.ts +857 -0
- package/src/node/features/vault.ts +164 -0
- package/src/node/features/vm.ts +312 -0
- package/src/node/features/window-manager.ts +804 -0
- package/src/node/features/yaml-tree.ts +149 -0
- package/src/node/features/yaml.ts +132 -0
- package/src/node.ts +70 -0
- package/src/react/index.ts +175 -0
- package/src/registry.ts +199 -0
- package/src/scaffolds/generated.ts +1613 -0
- package/src/scaffolds/template.ts +37 -0
- package/src/schemas/base.ts +255 -0
- package/src/server.ts +135 -0
- package/src/servers/express.ts +209 -0
- package/src/servers/mcp.ts +805 -0
- package/src/servers/socket.ts +120 -0
- package/src/state.ts +101 -0
- package/src/web/clients/socket.ts +82 -0
- package/src/web/container.ts +74 -0
- package/src/web/extension.ts +30 -0
- package/src/web/feature.ts +12 -0
- package/src/web/features/asset-loader.ts +64 -0
- package/src/web/features/container-link.ts +385 -0
- package/src/web/features/esbuild.ts +79 -0
- package/src/web/features/helpers.ts +267 -0
- package/src/web/features/network.ts +61 -0
- package/src/web/features/speech.ts +87 -0
- package/src/web/features/vault.ts +189 -0
- package/src/web/features/vm.ts +78 -0
- package/src/web/features/voice-recognition.ts +129 -0
- package/src/web/shims/isomorphic-vm.ts +149 -0
- package/test/bus.test.ts +134 -0
- package/test/clients-servers.test.ts +216 -0
- package/test/container-link.test.ts +274 -0
- package/test/features.test.ts +160 -0
- package/test/integration.test.ts +787 -0
- package/test/node-container.test.ts +121 -0
- package/test/rate-limit.test.ts +272 -0
- package/test/semantic-search.test.ts +550 -0
- package/test/state.test.ts +121 -0
- package/test-integration/assistant.test.ts +138 -0
- package/test-integration/assistants-manager.test.ts +123 -0
- package/test-integration/claude-code.test.ts +98 -0
- package/test-integration/conversation-history.test.ts +205 -0
- package/test-integration/conversation.test.ts +137 -0
- package/test-integration/elevenlabs.test.ts +55 -0
- package/test-integration/google-services.test.ts +80 -0
- package/test-integration/helpers.ts +89 -0
- package/test-integration/openai-codex.test.ts +93 -0
- package/test-integration/runpod.test.ts +58 -0
- package/test-integration/server-endpoints.test.ts +97 -0
- package/test-integration/skills-library.test.ts +157 -0
- package/test-integration/telegram.test.ts +46 -0
- package/tsconfig.json +58 -0
- package/uv.lock +8 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
|
|
3
|
+
import { Feature, features } from '../feature.js'
|
|
4
|
+
import { google, type calendar_v3 } from 'googleapis'
|
|
5
|
+
import type { GoogleAuth } from './google-auth.js'
|
|
6
|
+
|
|
7
|
+
export type CalendarInfo = {
|
|
8
|
+
id: string
|
|
9
|
+
summary: string
|
|
10
|
+
description?: string
|
|
11
|
+
timeZone: string
|
|
12
|
+
primary?: boolean
|
|
13
|
+
backgroundColor?: string
|
|
14
|
+
accessRole: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type CalendarEvent = {
|
|
18
|
+
id: string
|
|
19
|
+
summary: string
|
|
20
|
+
description?: string
|
|
21
|
+
location?: string
|
|
22
|
+
start: { dateTime?: string; date?: string; timeZone?: string }
|
|
23
|
+
end: { dateTime?: string; date?: string; timeZone?: string }
|
|
24
|
+
status: string
|
|
25
|
+
htmlLink: string
|
|
26
|
+
creator?: { email?: string; displayName?: string }
|
|
27
|
+
organizer?: { email?: string; displayName?: string }
|
|
28
|
+
attendees?: Array<{ email?: string; displayName?: string; responseStatus?: string }>
|
|
29
|
+
recurrence?: string[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type CalendarEventList = {
|
|
33
|
+
events: CalendarEvent[]
|
|
34
|
+
nextPageToken?: string
|
|
35
|
+
timeZone?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type ListEventsOptions = {
|
|
39
|
+
calendarId?: string
|
|
40
|
+
timeMin?: string
|
|
41
|
+
timeMax?: string
|
|
42
|
+
maxResults?: number
|
|
43
|
+
query?: string
|
|
44
|
+
orderBy?: 'startTime' | 'updated'
|
|
45
|
+
pageToken?: string
|
|
46
|
+
singleEvents?: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const GoogleCalendarStateSchema = FeatureStateSchema.extend({
|
|
50
|
+
lastCalendarId: z.string().optional()
|
|
51
|
+
.describe('Last calendar ID queried'),
|
|
52
|
+
lastEventCount: z.number().optional()
|
|
53
|
+
.describe('Number of events returned in last query'),
|
|
54
|
+
lastError: z.string().optional()
|
|
55
|
+
.describe('Last Calendar API error message'),
|
|
56
|
+
})
|
|
57
|
+
export type GoogleCalendarState = z.infer<typeof GoogleCalendarStateSchema>
|
|
58
|
+
|
|
59
|
+
export const GoogleCalendarOptionsSchema = FeatureOptionsSchema.extend({
|
|
60
|
+
defaultCalendarId: z.string().optional()
|
|
61
|
+
.describe('Default calendar ID (default: "primary")'),
|
|
62
|
+
timeZone: z.string().optional()
|
|
63
|
+
.describe('Default timezone for event queries (e.g. "America/Chicago")'),
|
|
64
|
+
})
|
|
65
|
+
export type GoogleCalendarOptions = z.infer<typeof GoogleCalendarOptionsSchema>
|
|
66
|
+
|
|
67
|
+
export const GoogleCalendarEventsSchema = FeatureEventsSchema.extend({
|
|
68
|
+
eventsFetched: z.tuple([z.number().describe('Number of events returned')])
|
|
69
|
+
.describe('Events were fetched from Calendar'),
|
|
70
|
+
error: z.tuple([z.any().describe('The error')]).describe('Calendar API error occurred'),
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Google Calendar feature for listing calendars and reading events.
|
|
75
|
+
*
|
|
76
|
+
* Depends on the googleAuth feature for authentication. Creates a Calendar v3 API
|
|
77
|
+
* client lazily. Provides convenience methods for today's events and upcoming days.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```typescript
|
|
81
|
+
* const calendar = container.feature('googleCalendar')
|
|
82
|
+
*
|
|
83
|
+
* // List all calendars
|
|
84
|
+
* const calendars = await calendar.listCalendars()
|
|
85
|
+
*
|
|
86
|
+
* // Get today's events
|
|
87
|
+
* const today = await calendar.getToday()
|
|
88
|
+
*
|
|
89
|
+
* // Get next 7 days of events
|
|
90
|
+
* const upcoming = await calendar.getUpcoming(7)
|
|
91
|
+
*
|
|
92
|
+
* // Search events
|
|
93
|
+
* const meetings = await calendar.searchEvents('standup')
|
|
94
|
+
*
|
|
95
|
+
* // List events in a time range
|
|
96
|
+
* const events = await calendar.listEvents({
|
|
97
|
+
* timeMin: '2026-03-01T00:00:00Z',
|
|
98
|
+
* timeMax: '2026-03-31T23:59:59Z',
|
|
99
|
+
* })
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export class GoogleCalendar extends Feature<GoogleCalendarState, GoogleCalendarOptions> {
|
|
103
|
+
static override shortcut = 'features.googleCalendar' as const
|
|
104
|
+
static override stateSchema = GoogleCalendarStateSchema
|
|
105
|
+
static override optionsSchema = GoogleCalendarOptionsSchema
|
|
106
|
+
static override eventsSchema = GoogleCalendarEventsSchema
|
|
107
|
+
|
|
108
|
+
private _calendar?: calendar_v3.Calendar
|
|
109
|
+
|
|
110
|
+
override get initialState(): GoogleCalendarState {
|
|
111
|
+
return { ...super.initialState }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Access the google-auth feature lazily. */
|
|
115
|
+
get auth(): GoogleAuth {
|
|
116
|
+
return this.container.feature('googleAuth') as unknown as GoogleAuth
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Default calendar ID from options or 'primary'. */
|
|
120
|
+
get defaultCalendarId(): string {
|
|
121
|
+
return this.options.defaultCalendarId || 'primary'
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Get or create the Calendar v3 API client. */
|
|
125
|
+
private async getCalendar(): Promise<calendar_v3.Calendar> {
|
|
126
|
+
if (this._calendar) return this._calendar
|
|
127
|
+
const auth = await this.auth.getAuthClient()
|
|
128
|
+
this._calendar = google.calendar({ version: 'v3', auth: auth as any })
|
|
129
|
+
return this._calendar
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* List all calendars accessible to the authenticated user.
|
|
134
|
+
*
|
|
135
|
+
* @returns Array of calendar info objects
|
|
136
|
+
*/
|
|
137
|
+
async listCalendars(): Promise<CalendarInfo[]> {
|
|
138
|
+
try {
|
|
139
|
+
const cal = await this.getCalendar()
|
|
140
|
+
const res = await cal.calendarList.list({ maxResults: 250 })
|
|
141
|
+
return (res.data.items || []).map(c => ({
|
|
142
|
+
id: c.id || '',
|
|
143
|
+
summary: c.summary || '',
|
|
144
|
+
description: c.description || undefined,
|
|
145
|
+
timeZone: c.timeZone || '',
|
|
146
|
+
primary: c.primary || undefined,
|
|
147
|
+
backgroundColor: c.backgroundColor || undefined,
|
|
148
|
+
accessRole: c.accessRole || '',
|
|
149
|
+
}))
|
|
150
|
+
} catch (err: any) {
|
|
151
|
+
this.setState({ lastError: err.message })
|
|
152
|
+
this.emit('error', err)
|
|
153
|
+
throw err
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* List events from a calendar within a time range.
|
|
159
|
+
*
|
|
160
|
+
* @param options - Filtering options including timeMin, timeMax, query, maxResults
|
|
161
|
+
* @returns Events array with optional nextPageToken and timeZone
|
|
162
|
+
*/
|
|
163
|
+
async listEvents(options: ListEventsOptions = {}): Promise<CalendarEventList> {
|
|
164
|
+
const calendarId = options.calendarId || this.defaultCalendarId
|
|
165
|
+
try {
|
|
166
|
+
const cal = await this.getCalendar()
|
|
167
|
+
const res = await cal.events.list({
|
|
168
|
+
calendarId,
|
|
169
|
+
timeMin: options.timeMin || undefined,
|
|
170
|
+
timeMax: options.timeMax || undefined,
|
|
171
|
+
maxResults: options.maxResults || 250,
|
|
172
|
+
q: options.query || undefined,
|
|
173
|
+
orderBy: options.orderBy || 'startTime',
|
|
174
|
+
pageToken: options.pageToken || undefined,
|
|
175
|
+
singleEvents: options.singleEvents !== false,
|
|
176
|
+
timeZone: this.options.timeZone || undefined,
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
const events = (res.data.items || []).map(normalizeEvent)
|
|
180
|
+
this.setState({ lastCalendarId: calendarId, lastEventCount: events.length })
|
|
181
|
+
this.emit('eventsFetched', events.length)
|
|
182
|
+
return {
|
|
183
|
+
events,
|
|
184
|
+
nextPageToken: res.data.nextPageToken || undefined,
|
|
185
|
+
timeZone: res.data.timeZone || undefined,
|
|
186
|
+
}
|
|
187
|
+
} catch (err: any) {
|
|
188
|
+
this.setState({ lastError: err.message })
|
|
189
|
+
this.emit('error', err)
|
|
190
|
+
throw err
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Get today's events from a calendar.
|
|
196
|
+
*
|
|
197
|
+
* @param calendarId - Calendar ID (defaults to options.defaultCalendarId or 'primary')
|
|
198
|
+
* @returns Array of today's calendar events
|
|
199
|
+
*/
|
|
200
|
+
async getToday(calendarId?: string): Promise<CalendarEvent[]> {
|
|
201
|
+
const now = new Date()
|
|
202
|
+
const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
|
203
|
+
const endOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1)
|
|
204
|
+
|
|
205
|
+
const { events } = await this.listEvents({
|
|
206
|
+
calendarId,
|
|
207
|
+
timeMin: startOfDay.toISOString(),
|
|
208
|
+
timeMax: endOfDay.toISOString(),
|
|
209
|
+
})
|
|
210
|
+
return events
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Get upcoming events for the next N days.
|
|
215
|
+
*
|
|
216
|
+
* @param days - Number of days to look ahead (default: 7)
|
|
217
|
+
* @param calendarId - Calendar ID
|
|
218
|
+
* @returns Array of upcoming calendar events
|
|
219
|
+
*/
|
|
220
|
+
async getUpcoming(days: number = 7, calendarId?: string): Promise<CalendarEvent[]> {
|
|
221
|
+
const now = new Date()
|
|
222
|
+
const future = new Date(now.getTime() + days * 24 * 60 * 60 * 1000)
|
|
223
|
+
|
|
224
|
+
const { events } = await this.listEvents({
|
|
225
|
+
calendarId,
|
|
226
|
+
timeMin: now.toISOString(),
|
|
227
|
+
timeMax: future.toISOString(),
|
|
228
|
+
})
|
|
229
|
+
return events
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get a single event by ID.
|
|
234
|
+
*
|
|
235
|
+
* @param eventId - The event ID
|
|
236
|
+
* @param calendarId - Calendar ID
|
|
237
|
+
* @returns The calendar event
|
|
238
|
+
*/
|
|
239
|
+
async getEvent(eventId: string, calendarId?: string): Promise<CalendarEvent> {
|
|
240
|
+
const cid = calendarId || this.defaultCalendarId
|
|
241
|
+
try {
|
|
242
|
+
const cal = await this.getCalendar()
|
|
243
|
+
const res = await cal.events.get({ calendarId: cid, eventId })
|
|
244
|
+
return normalizeEvent(res.data)
|
|
245
|
+
} catch (err: any) {
|
|
246
|
+
this.setState({ lastError: err.message })
|
|
247
|
+
this.emit('error', err)
|
|
248
|
+
throw err
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Search events by text query across event summaries, descriptions, and locations.
|
|
254
|
+
*
|
|
255
|
+
* @param query - Freetext search term
|
|
256
|
+
* @param options - Additional listing options (timeMin, timeMax, calendarId, etc.)
|
|
257
|
+
* @returns Array of matching calendar events
|
|
258
|
+
*/
|
|
259
|
+
async searchEvents(query: string, options: ListEventsOptions = {}): Promise<CalendarEvent[]> {
|
|
260
|
+
const { events } = await this.listEvents({ ...options, query })
|
|
261
|
+
return events
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function normalizeEvent(e: calendar_v3.Schema$Event): CalendarEvent {
|
|
266
|
+
return {
|
|
267
|
+
id: e.id || '',
|
|
268
|
+
summary: e.summary || '',
|
|
269
|
+
description: e.description || undefined,
|
|
270
|
+
location: e.location || undefined,
|
|
271
|
+
start: {
|
|
272
|
+
dateTime: e.start?.dateTime || undefined,
|
|
273
|
+
date: e.start?.date || undefined,
|
|
274
|
+
timeZone: e.start?.timeZone || undefined,
|
|
275
|
+
},
|
|
276
|
+
end: {
|
|
277
|
+
dateTime: e.end?.dateTime || undefined,
|
|
278
|
+
date: e.end?.date || undefined,
|
|
279
|
+
timeZone: e.end?.timeZone || undefined,
|
|
280
|
+
},
|
|
281
|
+
status: e.status || '',
|
|
282
|
+
htmlLink: e.htmlLink || '',
|
|
283
|
+
creator: e.creator ? { email: e.creator.email || undefined, displayName: e.creator.displayName || undefined } : undefined,
|
|
284
|
+
organizer: e.organizer ? { email: e.organizer.email || undefined, displayName: e.organizer.displayName || undefined } : undefined,
|
|
285
|
+
attendees: e.attendees?.map(a => ({
|
|
286
|
+
email: a.email || undefined,
|
|
287
|
+
displayName: a.displayName || undefined,
|
|
288
|
+
responseStatus: a.responseStatus || undefined,
|
|
289
|
+
})) || undefined,
|
|
290
|
+
recurrence: e.recurrence || undefined,
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
declare module '../../feature' {
|
|
295
|
+
interface AvailableFeatures {
|
|
296
|
+
googleCalendar: typeof GoogleCalendar
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export default features.register('googleCalendar', GoogleCalendar)
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
|
|
3
|
+
import { Feature, features } from '../feature.js'
|
|
4
|
+
import { google, type docs_v1 } from 'googleapis'
|
|
5
|
+
import type { GoogleAuth } from './google-auth.js'
|
|
6
|
+
import type { GoogleDrive, DriveFile } from './google-drive.js'
|
|
7
|
+
|
|
8
|
+
export const GoogleDocsStateSchema = FeatureStateSchema.extend({
|
|
9
|
+
lastDocId: z.string().optional()
|
|
10
|
+
.describe('Last document ID accessed'),
|
|
11
|
+
lastDocTitle: z.string().optional()
|
|
12
|
+
.describe('Title of the last document accessed'),
|
|
13
|
+
lastError: z.string().optional()
|
|
14
|
+
.describe('Last Docs API error message'),
|
|
15
|
+
})
|
|
16
|
+
export type GoogleDocsState = z.infer<typeof GoogleDocsStateSchema>
|
|
17
|
+
|
|
18
|
+
export const GoogleDocsOptionsSchema = FeatureOptionsSchema.extend({})
|
|
19
|
+
export type GoogleDocsOptions = z.infer<typeof GoogleDocsOptionsSchema>
|
|
20
|
+
|
|
21
|
+
export const GoogleDocsEventsSchema = FeatureEventsSchema.extend({
|
|
22
|
+
documentFetched: z.tuple([z.string().describe('Document ID'), z.string().describe('Title')])
|
|
23
|
+
.describe('A document was fetched'),
|
|
24
|
+
error: z.tuple([z.any().describe('The error')]).describe('Docs API error occurred'),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Google Docs feature for reading documents and converting them to Markdown.
|
|
29
|
+
*
|
|
30
|
+
* Depends on googleAuth for authentication and optionally googleDrive for listing docs.
|
|
31
|
+
* The markdown converter handles headings, text formatting, links, lists, tables, and images.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* const docs = container.feature('googleDocs')
|
|
36
|
+
*
|
|
37
|
+
* // Get a doc as markdown
|
|
38
|
+
* const markdown = await docs.getAsMarkdown('1abc_document_id')
|
|
39
|
+
*
|
|
40
|
+
* // Save to file
|
|
41
|
+
* await docs.saveAsMarkdown('1abc_document_id', './output/doc.md')
|
|
42
|
+
*
|
|
43
|
+
* // List all Google Docs in Drive
|
|
44
|
+
* const allDocs = await docs.listDocs()
|
|
45
|
+
*
|
|
46
|
+
* // Get raw document structure
|
|
47
|
+
* const rawDoc = await docs.getDocument('1abc_document_id')
|
|
48
|
+
*
|
|
49
|
+
* // Plain text extraction
|
|
50
|
+
* const text = await docs.getAsText('1abc_document_id')
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export class GoogleDocs extends Feature<GoogleDocsState, GoogleDocsOptions> {
|
|
54
|
+
static override shortcut = 'features.googleDocs' as const
|
|
55
|
+
static override stateSchema = GoogleDocsStateSchema
|
|
56
|
+
static override optionsSchema = GoogleDocsOptionsSchema
|
|
57
|
+
static override eventsSchema = GoogleDocsEventsSchema
|
|
58
|
+
|
|
59
|
+
private _docs?: docs_v1.Docs
|
|
60
|
+
|
|
61
|
+
override get initialState(): GoogleDocsState {
|
|
62
|
+
return { ...super.initialState }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Access the google-auth feature lazily. */
|
|
66
|
+
get auth(): GoogleAuth {
|
|
67
|
+
return this.container.feature('googleAuth') as unknown as GoogleAuth
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Access the google-drive feature lazily. */
|
|
71
|
+
get drive(): GoogleDrive {
|
|
72
|
+
return this.container.feature('googleDrive') as unknown as GoogleDrive
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Get or create the Docs v1 API client. */
|
|
76
|
+
private async getDocs(): Promise<docs_v1.Docs> {
|
|
77
|
+
if (this._docs) return this._docs
|
|
78
|
+
const auth = await this.auth.getAuthClient()
|
|
79
|
+
this._docs = google.docs({ version: 'v1', auth: auth as any })
|
|
80
|
+
return this._docs
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get the raw document structure from the Docs API.
|
|
85
|
+
*
|
|
86
|
+
* @param documentId - The Google Docs document ID
|
|
87
|
+
* @returns Full document JSON including body, lists, inlineObjects, etc.
|
|
88
|
+
*/
|
|
89
|
+
async getDocument(documentId: string): Promise<docs_v1.Schema$Document> {
|
|
90
|
+
try {
|
|
91
|
+
const docs = await this.getDocs()
|
|
92
|
+
const res = await docs.documents.get({ documentId })
|
|
93
|
+
const doc = res.data
|
|
94
|
+
|
|
95
|
+
this.setState({
|
|
96
|
+
lastDocId: documentId,
|
|
97
|
+
lastDocTitle: doc.title || undefined,
|
|
98
|
+
})
|
|
99
|
+
this.emit('documentFetched', documentId, doc.title || '')
|
|
100
|
+
return doc
|
|
101
|
+
} catch (err: any) {
|
|
102
|
+
this.setState({ lastError: err.message })
|
|
103
|
+
this.emit('error', err)
|
|
104
|
+
throw err
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Read a Google Doc and convert it to Markdown.
|
|
110
|
+
*
|
|
111
|
+
* Handles headings, bold/italic/strikethrough, links, code fonts, ordered/unordered
|
|
112
|
+
* lists with nesting, tables, images, and section breaks.
|
|
113
|
+
*
|
|
114
|
+
* @param documentId - The Google Docs document ID
|
|
115
|
+
* @returns Markdown string representation of the document
|
|
116
|
+
*/
|
|
117
|
+
async getAsMarkdown(documentId: string): Promise<string> {
|
|
118
|
+
const doc = await this.getDocument(documentId)
|
|
119
|
+
return convertDocToMarkdown(doc)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Read a Google Doc as plain text (strips all formatting).
|
|
124
|
+
*
|
|
125
|
+
* @param documentId - The Google Docs document ID
|
|
126
|
+
*/
|
|
127
|
+
async getAsText(documentId: string): Promise<string> {
|
|
128
|
+
const doc = await this.getDocument(documentId)
|
|
129
|
+
return extractPlainText(doc)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Download a Google Doc as Markdown and save to a local file.
|
|
134
|
+
*
|
|
135
|
+
* @param documentId - The Google Docs document ID
|
|
136
|
+
* @param localPath - Local file path (resolved relative to container cwd)
|
|
137
|
+
* @returns Absolute path of the saved file
|
|
138
|
+
*/
|
|
139
|
+
async saveAsMarkdown(documentId: string, localPath: string): Promise<string> {
|
|
140
|
+
const markdown = await this.getAsMarkdown(documentId)
|
|
141
|
+
const outPath = this.container.paths.resolve(localPath)
|
|
142
|
+
await this.container.fs.writeFileAsync(outPath, markdown)
|
|
143
|
+
return outPath
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* List Google Docs in Drive (filters by Docs MIME type).
|
|
148
|
+
*
|
|
149
|
+
* @param query - Optional additional Drive search query
|
|
150
|
+
* @param options - Pagination options
|
|
151
|
+
* @returns Array of Google Docs as DriveFile objects
|
|
152
|
+
*/
|
|
153
|
+
async listDocs(query?: string, options?: { pageSize?: number; pageToken?: string }): Promise<DriveFile[]> {
|
|
154
|
+
const parts = ["mimeType = 'application/vnd.google-apps.document'", 'trashed = false']
|
|
155
|
+
if (query) parts.push(`name contains '${query.replace(/'/g, "\\'")}'`)
|
|
156
|
+
const { files } = await this.drive.listFiles(parts.join(' and '), options)
|
|
157
|
+
return files
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Search for Google Docs by name or content.
|
|
162
|
+
*
|
|
163
|
+
* @param term - Search term
|
|
164
|
+
* @returns Array of matching Google Docs as DriveFile objects
|
|
165
|
+
*/
|
|
166
|
+
async searchDocs(term: string): Promise<DriveFile[]> {
|
|
167
|
+
const { files } = await this.drive.search(term, {
|
|
168
|
+
mimeType: 'application/vnd.google-apps.document',
|
|
169
|
+
})
|
|
170
|
+
return files
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ─── Markdown Converter ─────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
type ListInfo = {
|
|
177
|
+
ordered: boolean
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Convert a Google Docs API document to Markdown.
|
|
182
|
+
*/
|
|
183
|
+
function convertDocToMarkdown(doc: docs_v1.Schema$Document): string {
|
|
184
|
+
const body = doc.body?.content || []
|
|
185
|
+
const lists = doc.lists || {}
|
|
186
|
+
const inlineObjects = doc.inlineObjects || {}
|
|
187
|
+
const lines: string[] = []
|
|
188
|
+
|
|
189
|
+
// Build list type lookup: listId + nestingLevel → ordered/unordered
|
|
190
|
+
const listLookup = new Map<string, ListInfo>()
|
|
191
|
+
for (const [listId, listDef] of Object.entries(lists)) {
|
|
192
|
+
const levels = listDef.listProperties?.nestingLevels || []
|
|
193
|
+
levels.forEach((level, i) => {
|
|
194
|
+
const glyphType = level.glyphType
|
|
195
|
+
// DECIMAL, ALPHA, ROMAN = ordered; everything else (bullet/none) = unordered
|
|
196
|
+
const ordered = glyphType === 'DECIMAL' || glyphType === 'ALPHA' || glyphType === 'UPPER_ALPHA'
|
|
197
|
+
|| glyphType === 'ROMAN' || glyphType === 'UPPER_ROMAN'
|
|
198
|
+
listLookup.set(`${listId}:${i}`, { ordered })
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
for (const element of body) {
|
|
203
|
+
if (element.paragraph) {
|
|
204
|
+
const para = element.paragraph
|
|
205
|
+
const line = convertParagraph(para, listLookup, inlineObjects)
|
|
206
|
+
lines.push(line)
|
|
207
|
+
} else if (element.table) {
|
|
208
|
+
const tableLines = convertTable(element.table, listLookup, inlineObjects)
|
|
209
|
+
lines.push('', ...tableLines, '')
|
|
210
|
+
} else if (element.sectionBreak) {
|
|
211
|
+
lines.push('', '---', '')
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Clean up: collapse 3+ consecutive blank lines into 2
|
|
216
|
+
let result = lines.join('\n')
|
|
217
|
+
result = result.replace(/\n{3,}/g, '\n\n')
|
|
218
|
+
return result.trim() + '\n'
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function convertParagraph(
|
|
222
|
+
para: docs_v1.Schema$Paragraph,
|
|
223
|
+
listLookup: Map<string, ListInfo>,
|
|
224
|
+
inlineObjects: Record<string, docs_v1.Schema$InlineObject>
|
|
225
|
+
): string {
|
|
226
|
+
const style = para.paragraphStyle?.namedStyleType || 'NORMAL_TEXT'
|
|
227
|
+
const elements = para.elements || []
|
|
228
|
+
const bullet = para.bullet
|
|
229
|
+
|
|
230
|
+
// Build the inline text content
|
|
231
|
+
let text = ''
|
|
232
|
+
for (const el of elements) {
|
|
233
|
+
if (el.textRun) {
|
|
234
|
+
text += formatTextRun(el.textRun)
|
|
235
|
+
} else if (el.inlineObjectElement) {
|
|
236
|
+
const objId = el.inlineObjectElement.inlineObjectId
|
|
237
|
+
if (objId && inlineObjects[objId]) {
|
|
238
|
+
const obj = inlineObjects[objId]
|
|
239
|
+
const embedded = obj.inlineObjectProperties?.embeddedObject
|
|
240
|
+
const uri = embedded?.imageProperties?.contentUri || embedded?.imageProperties?.sourceUri || ''
|
|
241
|
+
const alt = embedded?.title || embedded?.description || 'image'
|
|
242
|
+
if (uri) {
|
|
243
|
+
text += ``
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Trim trailing newline that Google Docs adds to every paragraph
|
|
250
|
+
text = text.replace(/\n$/, '')
|
|
251
|
+
|
|
252
|
+
// If the paragraph is empty, return a blank line
|
|
253
|
+
if (!text.trim()) return ''
|
|
254
|
+
|
|
255
|
+
// Apply heading prefix
|
|
256
|
+
const headingMap: Record<string, string> = {
|
|
257
|
+
TITLE: '# ',
|
|
258
|
+
SUBTITLE: '## ',
|
|
259
|
+
HEADING_1: '# ',
|
|
260
|
+
HEADING_2: '## ',
|
|
261
|
+
HEADING_3: '### ',
|
|
262
|
+
HEADING_4: '#### ',
|
|
263
|
+
HEADING_5: '##### ',
|
|
264
|
+
HEADING_6: '###### ',
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (headingMap[style]) {
|
|
268
|
+
return `\n${headingMap[style]}${text}\n`
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Apply list prefix
|
|
272
|
+
if (bullet) {
|
|
273
|
+
const listId = bullet.listId || ''
|
|
274
|
+
const nestingLevel = bullet.nestingLevel || 0
|
|
275
|
+
const key = `${listId}:${nestingLevel}`
|
|
276
|
+
const info = listLookup.get(key)
|
|
277
|
+
const indent = ' '.repeat(nestingLevel)
|
|
278
|
+
const prefix = info?.ordered ? '1. ' : '- '
|
|
279
|
+
return `${indent}${prefix}${text}`
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return text
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function formatTextRun(run: docs_v1.Schema$TextRun): string {
|
|
286
|
+
let content = run.content || ''
|
|
287
|
+
const style = run.textStyle
|
|
288
|
+
|
|
289
|
+
if (!style || content === '\n') return content
|
|
290
|
+
|
|
291
|
+
// Don't format whitespace-only content
|
|
292
|
+
const trimmed = content.replace(/\n$/, '')
|
|
293
|
+
if (!trimmed.trim()) return content
|
|
294
|
+
|
|
295
|
+
// Detect code font (Courier variants)
|
|
296
|
+
const fontFamily = style.weightedFontFamily?.fontFamily || ''
|
|
297
|
+
const isCode = /courier/i.test(fontFamily) || /consolas/i.test(fontFamily) || /mono/i.test(fontFamily)
|
|
298
|
+
|
|
299
|
+
// Extract trailing newline to preserve it outside formatting
|
|
300
|
+
const trailingNewline = content.endsWith('\n') ? '\n' : ''
|
|
301
|
+
let formatted = content.replace(/\n$/, '')
|
|
302
|
+
|
|
303
|
+
if (isCode) {
|
|
304
|
+
formatted = `\`${formatted}\``
|
|
305
|
+
} else {
|
|
306
|
+
// Apply formatting — order matters: bold wraps italic wraps strikethrough
|
|
307
|
+
if (style.strikethrough) {
|
|
308
|
+
formatted = `~~${formatted}~~`
|
|
309
|
+
}
|
|
310
|
+
if (style.italic) {
|
|
311
|
+
formatted = `*${formatted}*`
|
|
312
|
+
}
|
|
313
|
+
if (style.bold) {
|
|
314
|
+
formatted = `**${formatted}**`
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Apply link
|
|
319
|
+
if (style.link?.url) {
|
|
320
|
+
formatted = `[${formatted}](${style.link.url})`
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return formatted + trailingNewline
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function convertTable(
|
|
327
|
+
table: docs_v1.Schema$Table,
|
|
328
|
+
listLookup: Map<string, ListInfo>,
|
|
329
|
+
inlineObjects: Record<string, docs_v1.Schema$InlineObject>
|
|
330
|
+
): string[] {
|
|
331
|
+
const rows = table.tableRows || []
|
|
332
|
+
if (rows.length === 0) return []
|
|
333
|
+
|
|
334
|
+
const tableData: string[][] = rows.map(row => {
|
|
335
|
+
const cells = row.tableCells || []
|
|
336
|
+
return cells.map(cell => {
|
|
337
|
+
const content = cell.content || []
|
|
338
|
+
const cellText = content.map(el => {
|
|
339
|
+
if (el.paragraph) {
|
|
340
|
+
return convertParagraph(el.paragraph, listLookup, inlineObjects)
|
|
341
|
+
}
|
|
342
|
+
return ''
|
|
343
|
+
}).join(' ').trim()
|
|
344
|
+
// Escape pipes in cell content
|
|
345
|
+
return cellText.replace(/\|/g, '\\|')
|
|
346
|
+
})
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
if (tableData.length === 0) return []
|
|
350
|
+
|
|
351
|
+
const lines: string[] = []
|
|
352
|
+
|
|
353
|
+
const header = tableData[0]!
|
|
354
|
+
// Header row
|
|
355
|
+
lines.push('| ' + header.join(' | ') + ' |')
|
|
356
|
+
// Separator
|
|
357
|
+
lines.push('| ' + header.map(() => '---').join(' | ') + ' |')
|
|
358
|
+
// Data rows
|
|
359
|
+
for (let i = 1; i < tableData.length; i++) {
|
|
360
|
+
lines.push('| ' + tableData[i]!.join(' | ') + ' |')
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return lines
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Extract plain text from a Google Docs document, stripping all formatting.
|
|
368
|
+
*/
|
|
369
|
+
function extractPlainText(doc: docs_v1.Schema$Document): string {
|
|
370
|
+
const body = doc.body?.content || []
|
|
371
|
+
const parts: string[] = []
|
|
372
|
+
|
|
373
|
+
for (const element of body) {
|
|
374
|
+
if (element.paragraph) {
|
|
375
|
+
const text = (element.paragraph.elements || [])
|
|
376
|
+
.map(el => el.textRun?.content || '')
|
|
377
|
+
.join('')
|
|
378
|
+
parts.push(text)
|
|
379
|
+
} else if (element.table) {
|
|
380
|
+
const rows = element.table.tableRows || []
|
|
381
|
+
for (const row of rows) {
|
|
382
|
+
const cells = row.tableCells || []
|
|
383
|
+
const cellTexts = cells.map(cell => {
|
|
384
|
+
return (cell.content || []).map(el => {
|
|
385
|
+
return (el.paragraph?.elements || [])
|
|
386
|
+
.map(pe => pe.textRun?.content || '')
|
|
387
|
+
.join('')
|
|
388
|
+
}).join('')
|
|
389
|
+
})
|
|
390
|
+
parts.push(cellTexts.join('\t'))
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return parts.join('').trim() + '\n'
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
declare module '../../feature' {
|
|
399
|
+
interface AvailableFeatures {
|
|
400
|
+
googleDocs: typeof GoogleDocs
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export default features.register('googleDocs', GoogleDocs)
|