@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.
Files changed (358) hide show
  1. package/CLAUDE.md +71 -0
  2. package/README.md +78 -0
  3. package/bun.lock +2928 -0
  4. package/bunfig.toml +3 -0
  5. package/commands/audit-docs.ts +740 -0
  6. package/commands/build-scaffolds.ts +154 -0
  7. package/commands/generate-api-docs.ts +114 -0
  8. package/commands/update-introspection.ts +67 -0
  9. package/docs/CLI.md +335 -0
  10. package/docs/README.md +88 -0
  11. package/docs/TABLE-OF-CONTENTS.md +157 -0
  12. package/docs/apis/clients/elevenlabs.md +84 -0
  13. package/docs/apis/clients/graph.md +56 -0
  14. package/docs/apis/clients/openai.md +69 -0
  15. package/docs/apis/clients/rest.md +41 -0
  16. package/docs/apis/clients/websocket.md +107 -0
  17. package/docs/apis/features/agi/assistant.md +471 -0
  18. package/docs/apis/features/agi/assistants-manager.md +154 -0
  19. package/docs/apis/features/agi/claude-code.md +602 -0
  20. package/docs/apis/features/agi/conversation-history.md +352 -0
  21. package/docs/apis/features/agi/conversation.md +333 -0
  22. package/docs/apis/features/agi/docs-reader.md +121 -0
  23. package/docs/apis/features/agi/openai-codex.md +318 -0
  24. package/docs/apis/features/agi/openapi.md +138 -0
  25. package/docs/apis/features/agi/semantic-search.md +387 -0
  26. package/docs/apis/features/agi/skills-library.md +216 -0
  27. package/docs/apis/features/node/container-link.md +133 -0
  28. package/docs/apis/features/node/content-db.md +313 -0
  29. package/docs/apis/features/node/disk-cache.md +379 -0
  30. package/docs/apis/features/node/dns.md +651 -0
  31. package/docs/apis/features/node/docker.md +705 -0
  32. package/docs/apis/features/node/downloader.md +81 -0
  33. package/docs/apis/features/node/esbuild.md +59 -0
  34. package/docs/apis/features/node/file-manager.md +182 -0
  35. package/docs/apis/features/node/fs.md +581 -0
  36. package/docs/apis/features/node/git.md +330 -0
  37. package/docs/apis/features/node/google-auth.md +174 -0
  38. package/docs/apis/features/node/google-calendar.md +187 -0
  39. package/docs/apis/features/node/google-docs.md +151 -0
  40. package/docs/apis/features/node/google-drive.md +225 -0
  41. package/docs/apis/features/node/google-sheets.md +179 -0
  42. package/docs/apis/features/node/grep.md +290 -0
  43. package/docs/apis/features/node/helpers.md +135 -0
  44. package/docs/apis/features/node/ink.md +334 -0
  45. package/docs/apis/features/node/ipc-socket.md +260 -0
  46. package/docs/apis/features/node/json-tree.md +86 -0
  47. package/docs/apis/features/node/launcher-app-command-listener.md +145 -0
  48. package/docs/apis/features/node/networking.md +281 -0
  49. package/docs/apis/features/node/nlp.md +133 -0
  50. package/docs/apis/features/node/opener.md +97 -0
  51. package/docs/apis/features/node/os.md +118 -0
  52. package/docs/apis/features/node/package-finder.md +402 -0
  53. package/docs/apis/features/node/postgres.md +212 -0
  54. package/docs/apis/features/node/proc.md +430 -0
  55. package/docs/apis/features/node/process-manager.md +210 -0
  56. package/docs/apis/features/node/python.md +278 -0
  57. package/docs/apis/features/node/repl.md +88 -0
  58. package/docs/apis/features/node/runpod.md +673 -0
  59. package/docs/apis/features/node/secure-shell.md +169 -0
  60. package/docs/apis/features/node/semantic-search.md +401 -0
  61. package/docs/apis/features/node/sqlite.md +211 -0
  62. package/docs/apis/features/node/telegram.md +254 -0
  63. package/docs/apis/features/node/tts.md +118 -0
  64. package/docs/apis/features/node/ui.md +703 -0
  65. package/docs/apis/features/node/vault.md +64 -0
  66. package/docs/apis/features/node/vm.md +84 -0
  67. package/docs/apis/features/node/window-manager.md +337 -0
  68. package/docs/apis/features/node/yaml-tree.md +85 -0
  69. package/docs/apis/features/node/yaml.md +176 -0
  70. package/docs/apis/features/web/asset-loader.md +47 -0
  71. package/docs/apis/features/web/container-link.md +133 -0
  72. package/docs/apis/features/web/esbuild.md +59 -0
  73. package/docs/apis/features/web/helpers.md +135 -0
  74. package/docs/apis/features/web/network.md +30 -0
  75. package/docs/apis/features/web/speech.md +55 -0
  76. package/docs/apis/features/web/vault.md +64 -0
  77. package/docs/apis/features/web/vm.md +84 -0
  78. package/docs/apis/features/web/voice.md +67 -0
  79. package/docs/apis/servers/express.md +127 -0
  80. package/docs/apis/servers/mcp.md +213 -0
  81. package/docs/apis/servers/websocket.md +99 -0
  82. package/docs/documentation-audit.md +134 -0
  83. package/docs/examples/content-db.md +77 -0
  84. package/docs/examples/disk-cache.md +83 -0
  85. package/docs/examples/docker.md +101 -0
  86. package/docs/examples/downloader.md +70 -0
  87. package/docs/examples/esbuild.md +80 -0
  88. package/docs/examples/file-manager.md +82 -0
  89. package/docs/examples/fs.md +83 -0
  90. package/docs/examples/git.md +85 -0
  91. package/docs/examples/google-auth.md +88 -0
  92. package/docs/examples/google-calendar.md +94 -0
  93. package/docs/examples/google-docs.md +82 -0
  94. package/docs/examples/google-drive.md +96 -0
  95. package/docs/examples/google-sheets.md +95 -0
  96. package/docs/examples/grep.md +85 -0
  97. package/docs/examples/ink-blocks.md +75 -0
  98. package/docs/examples/ink-renderer.md +41 -0
  99. package/docs/examples/ink.md +103 -0
  100. package/docs/examples/ipc-socket.md +103 -0
  101. package/docs/examples/json-tree.md +91 -0
  102. package/docs/examples/launcher-app-command-listener.md +120 -0
  103. package/docs/examples/networking.md +58 -0
  104. package/docs/examples/nlp.md +91 -0
  105. package/docs/examples/opener.md +78 -0
  106. package/docs/examples/os.md +72 -0
  107. package/docs/examples/package-finder.md +89 -0
  108. package/docs/examples/port-exposer.md +89 -0
  109. package/docs/examples/postgres.md +91 -0
  110. package/docs/examples/proc.md +81 -0
  111. package/docs/examples/process-manager.md +79 -0
  112. package/docs/examples/python.md +91 -0
  113. package/docs/examples/repl.md +93 -0
  114. package/docs/examples/runpod.md +119 -0
  115. package/docs/examples/secure-shell.md +92 -0
  116. package/docs/examples/sqlite.md +86 -0
  117. package/docs/examples/telegram.md +77 -0
  118. package/docs/examples/tts.md +86 -0
  119. package/docs/examples/ui.md +80 -0
  120. package/docs/examples/vault.md +70 -0
  121. package/docs/examples/vm.md +86 -0
  122. package/docs/examples/window-manager.md +125 -0
  123. package/docs/examples/yaml-tree.md +93 -0
  124. package/docs/examples/yaml.md +104 -0
  125. package/docs/ideas/class-registration-refactor-possibilities.md +197 -0
  126. package/docs/ideas/container-use-api.md +9 -0
  127. package/docs/ideas/easy-auth-for-express-servers-and-luca-serve.md +0 -0
  128. package/docs/ideas/feature-stacks.md +22 -0
  129. package/docs/ideas/luca-cli-self-sufficiency-demo.md +23 -0
  130. package/docs/ideas/mcp-design.md +9 -0
  131. package/docs/ideas/web-container-debugging-feature.md +13 -0
  132. package/docs/introspection-audit.md +49 -0
  133. package/docs/introspection.md +154 -0
  134. package/docs/mcp/readme.md +162 -0
  135. package/docs/models.ts +38 -0
  136. package/docs/philosophy.md +85 -0
  137. package/docs/principles.md +7 -0
  138. package/docs/prompts/audit-codebase-for-failures-to-use-the-container.md +34 -0
  139. package/docs/prompts/mcp-test-easy-command.md +27 -0
  140. package/docs/reports/assistant-bugs.md +38 -0
  141. package/docs/reports/attach-pattern-usage.md +18 -0
  142. package/docs/reports/code-audit-results.md +391 -0
  143. package/docs/reports/introspection-audit-tasks.md +378 -0
  144. package/docs/reports/luca-mcp-improvements.md +128 -0
  145. package/docs/scaffolds/client.md +140 -0
  146. package/docs/scaffolds/command.md +106 -0
  147. package/docs/scaffolds/endpoint.md +176 -0
  148. package/docs/scaffolds/feature.md +148 -0
  149. package/docs/scaffolds/server.md +187 -0
  150. package/docs/tasks/web-container-helper-discovery.md +71 -0
  151. package/docs/todos.md +1 -0
  152. package/docs/tutorials/01-getting-started.md +106 -0
  153. package/docs/tutorials/02-container.md +210 -0
  154. package/docs/tutorials/03-scripts.md +194 -0
  155. package/docs/tutorials/04-features-overview.md +196 -0
  156. package/docs/tutorials/05-state-and-events.md +171 -0
  157. package/docs/tutorials/06-servers.md +157 -0
  158. package/docs/tutorials/07-endpoints.md +198 -0
  159. package/docs/tutorials/08-commands.md +171 -0
  160. package/docs/tutorials/09-clients.md +162 -0
  161. package/docs/tutorials/10-creating-features.md +198 -0
  162. package/docs/tutorials/11-contentbase.md +191 -0
  163. package/docs/tutorials/12-assistants.md +215 -0
  164. package/docs/tutorials/13-introspection.md +147 -0
  165. package/docs/tutorials/14-type-system.md +174 -0
  166. package/docs/tutorials/15-project-patterns.md +222 -0
  167. package/docs/tutorials/16-google-features.md +534 -0
  168. package/docs/tutorials/17-tui-blocks.md +530 -0
  169. package/docs/tutorials/18-semantic-search.md +334 -0
  170. package/index.ts +1 -0
  171. package/luca.console.ts +9 -0
  172. package/main.py +6 -0
  173. package/package.json +154 -0
  174. package/pyproject.toml +7 -0
  175. package/scripts/animations/chrome-glitch.ts +55 -0
  176. package/scripts/animations/index.ts +16 -0
  177. package/scripts/animations/neon-pulse.ts +64 -0
  178. package/scripts/animations/types.ts +6 -0
  179. package/scripts/build-web.ts +28 -0
  180. package/scripts/examples/ask-luca-expert.ts +42 -0
  181. package/scripts/examples/assistant-questions.ts +12 -0
  182. package/scripts/examples/excalidraw-expert.ts +75 -0
  183. package/scripts/examples/expert-chat.ts +0 -0
  184. package/scripts/examples/file-manager.ts +14 -0
  185. package/scripts/examples/ideas.ts +12 -0
  186. package/scripts/examples/interactive-chat.ts +20 -0
  187. package/scripts/examples/openai-tool-calls.ts +113 -0
  188. package/scripts/examples/opening-a-web-browser.ts +5 -0
  189. package/scripts/examples/telegram-bot.ts +79 -0
  190. package/scripts/examples/telegram-ink-ui.ts +302 -0
  191. package/scripts/examples/using-assistant-with-mcp.ts +560 -0
  192. package/scripts/examples/using-claude-code.ts +10 -0
  193. package/scripts/examples/using-contentdb.ts +35 -0
  194. package/scripts/examples/using-conversations.ts +35 -0
  195. package/scripts/examples/using-disk-cache.ts +10 -0
  196. package/scripts/examples/using-docker-shell.ts +75 -0
  197. package/scripts/examples/using-elevenlabs.ts +25 -0
  198. package/scripts/examples/using-google-calendar.ts +57 -0
  199. package/scripts/examples/using-google-docs.ts +74 -0
  200. package/scripts/examples/using-google-drive.ts +74 -0
  201. package/scripts/examples/using-google-sheets.ts +89 -0
  202. package/scripts/examples/using-nlp.ts +55 -0
  203. package/scripts/examples/using-ollama.ts +10 -0
  204. package/scripts/examples/using-openai-codex.ts +23 -0
  205. package/scripts/examples/using-postgres.ts +55 -0
  206. package/scripts/examples/using-runpod.ts +32 -0
  207. package/scripts/examples/using-tts.ts +40 -0
  208. package/scripts/examples/vm-loading-esm-modules.ts +16 -0
  209. package/scripts/scaffold.ts +391 -0
  210. package/scripts/scratch.ts +15 -0
  211. package/scripts/test-command-listener.ts +123 -0
  212. package/scripts/test-window-manager-lifecycle.ts +86 -0
  213. package/scripts/test-window-manager.ts +43 -0
  214. package/scripts/update-introspection-data.ts +58 -0
  215. package/src/agi/README.md +14 -0
  216. package/src/agi/container.server.ts +114 -0
  217. package/src/agi/endpoints/ask.ts +60 -0
  218. package/src/agi/endpoints/conversations/[id].ts +45 -0
  219. package/src/agi/endpoints/conversations.ts +31 -0
  220. package/src/agi/endpoints/experts.ts +37 -0
  221. package/src/agi/features/assistant.ts +767 -0
  222. package/src/agi/features/assistants-manager.ts +260 -0
  223. package/src/agi/features/claude-code.ts +1111 -0
  224. package/src/agi/features/conversation-history.ts +497 -0
  225. package/src/agi/features/conversation.ts +799 -0
  226. package/src/agi/features/openai-codex.ts +631 -0
  227. package/src/agi/features/openapi.ts +438 -0
  228. package/src/agi/features/skills-library.ts +425 -0
  229. package/src/agi/index.ts +6 -0
  230. package/src/agi/lib/token-counter.ts +122 -0
  231. package/src/browser.ts +25 -0
  232. package/src/bus.ts +100 -0
  233. package/src/cli/cli.ts +70 -0
  234. package/src/client.ts +461 -0
  235. package/src/clients/civitai/index.ts +541 -0
  236. package/src/clients/client-template.ts +41 -0
  237. package/src/clients/comfyui/index.ts +597 -0
  238. package/src/clients/elevenlabs/index.ts +291 -0
  239. package/src/clients/openai/index.ts +451 -0
  240. package/src/clients/supabase/index.ts +366 -0
  241. package/src/command.ts +164 -0
  242. package/src/commands/chat.ts +182 -0
  243. package/src/commands/console.ts +192 -0
  244. package/src/commands/describe.ts +433 -0
  245. package/src/commands/eval.ts +116 -0
  246. package/src/commands/help.ts +214 -0
  247. package/src/commands/index.ts +14 -0
  248. package/src/commands/mcp.ts +64 -0
  249. package/src/commands/prompt.ts +807 -0
  250. package/src/commands/run.ts +257 -0
  251. package/src/commands/sandbox-mcp.ts +439 -0
  252. package/src/commands/scaffold.ts +79 -0
  253. package/src/commands/serve.ts +172 -0
  254. package/src/container.ts +781 -0
  255. package/src/endpoint.ts +340 -0
  256. package/src/feature.ts +75 -0
  257. package/src/hash-object.ts +97 -0
  258. package/src/helper.ts +543 -0
  259. package/src/introspection/generated.agi.ts +23388 -0
  260. package/src/introspection/generated.node.ts +18899 -0
  261. package/src/introspection/generated.web.ts +2021 -0
  262. package/src/introspection/index.ts +256 -0
  263. package/src/introspection/scan.ts +912 -0
  264. package/src/node/container.ts +354 -0
  265. package/src/node/feature.ts +13 -0
  266. package/src/node/features/container-link.ts +558 -0
  267. package/src/node/features/content-db.ts +475 -0
  268. package/src/node/features/disk-cache.ts +382 -0
  269. package/src/node/features/dns.ts +655 -0
  270. package/src/node/features/docker.ts +912 -0
  271. package/src/node/features/downloader.ts +92 -0
  272. package/src/node/features/esbuild.ts +68 -0
  273. package/src/node/features/file-manager.ts +357 -0
  274. package/src/node/features/fs.ts +534 -0
  275. package/src/node/features/git.ts +492 -0
  276. package/src/node/features/google-auth.ts +502 -0
  277. package/src/node/features/google-calendar.ts +300 -0
  278. package/src/node/features/google-docs.ts +404 -0
  279. package/src/node/features/google-drive.ts +339 -0
  280. package/src/node/features/google-sheets.ts +279 -0
  281. package/src/node/features/grep.ts +406 -0
  282. package/src/node/features/helpers.ts +374 -0
  283. package/src/node/features/ink.ts +490 -0
  284. package/src/node/features/ipc-socket.ts +459 -0
  285. package/src/node/features/json-tree.ts +188 -0
  286. package/src/node/features/launcher-app-command-listener.ts +388 -0
  287. package/src/node/features/networking.ts +925 -0
  288. package/src/node/features/nlp.ts +211 -0
  289. package/src/node/features/opener.ts +166 -0
  290. package/src/node/features/os.ts +157 -0
  291. package/src/node/features/package-finder.ts +539 -0
  292. package/src/node/features/port-exposer.ts +342 -0
  293. package/src/node/features/postgres.ts +273 -0
  294. package/src/node/features/proc.ts +502 -0
  295. package/src/node/features/process-manager.ts +542 -0
  296. package/src/node/features/python.ts +444 -0
  297. package/src/node/features/repl.ts +194 -0
  298. package/src/node/features/runpod.ts +802 -0
  299. package/src/node/features/secure-shell.ts +248 -0
  300. package/src/node/features/semantic-search.ts +924 -0
  301. package/src/node/features/sqlite.ts +289 -0
  302. package/src/node/features/telegram.ts +342 -0
  303. package/src/node/features/tts.ts +184 -0
  304. package/src/node/features/ui.ts +857 -0
  305. package/src/node/features/vault.ts +164 -0
  306. package/src/node/features/vm.ts +312 -0
  307. package/src/node/features/window-manager.ts +804 -0
  308. package/src/node/features/yaml-tree.ts +149 -0
  309. package/src/node/features/yaml.ts +132 -0
  310. package/src/node.ts +70 -0
  311. package/src/react/index.ts +175 -0
  312. package/src/registry.ts +199 -0
  313. package/src/scaffolds/generated.ts +1613 -0
  314. package/src/scaffolds/template.ts +37 -0
  315. package/src/schemas/base.ts +255 -0
  316. package/src/server.ts +135 -0
  317. package/src/servers/express.ts +209 -0
  318. package/src/servers/mcp.ts +805 -0
  319. package/src/servers/socket.ts +120 -0
  320. package/src/state.ts +101 -0
  321. package/src/web/clients/socket.ts +82 -0
  322. package/src/web/container.ts +74 -0
  323. package/src/web/extension.ts +30 -0
  324. package/src/web/feature.ts +12 -0
  325. package/src/web/features/asset-loader.ts +64 -0
  326. package/src/web/features/container-link.ts +385 -0
  327. package/src/web/features/esbuild.ts +79 -0
  328. package/src/web/features/helpers.ts +267 -0
  329. package/src/web/features/network.ts +61 -0
  330. package/src/web/features/speech.ts +87 -0
  331. package/src/web/features/vault.ts +189 -0
  332. package/src/web/features/vm.ts +78 -0
  333. package/src/web/features/voice-recognition.ts +129 -0
  334. package/src/web/shims/isomorphic-vm.ts +149 -0
  335. package/test/bus.test.ts +134 -0
  336. package/test/clients-servers.test.ts +216 -0
  337. package/test/container-link.test.ts +274 -0
  338. package/test/features.test.ts +160 -0
  339. package/test/integration.test.ts +787 -0
  340. package/test/node-container.test.ts +121 -0
  341. package/test/rate-limit.test.ts +272 -0
  342. package/test/semantic-search.test.ts +550 -0
  343. package/test/state.test.ts +121 -0
  344. package/test-integration/assistant.test.ts +138 -0
  345. package/test-integration/assistants-manager.test.ts +123 -0
  346. package/test-integration/claude-code.test.ts +98 -0
  347. package/test-integration/conversation-history.test.ts +205 -0
  348. package/test-integration/conversation.test.ts +137 -0
  349. package/test-integration/elevenlabs.test.ts +55 -0
  350. package/test-integration/google-services.test.ts +80 -0
  351. package/test-integration/helpers.ts +89 -0
  352. package/test-integration/openai-codex.test.ts +93 -0
  353. package/test-integration/runpod.test.ts +58 -0
  354. package/test-integration/server-endpoints.test.ts +97 -0
  355. package/test-integration/skills-library.test.ts +157 -0
  356. package/test-integration/telegram.test.ts +46 -0
  357. package/tsconfig.json +58 -0
  358. 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 += `![${alt}](${uri})`
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)