@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,339 @@
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 drive_v3 } from 'googleapis'
5
+ import type { GoogleAuth } from './google-auth.js'
6
+
7
+ export type DriveFile = {
8
+ id: string
9
+ name: string
10
+ mimeType: string
11
+ size?: string
12
+ createdTime?: string
13
+ modifiedTime?: string
14
+ parents?: string[]
15
+ webViewLink?: string
16
+ iconLink?: string
17
+ owners?: Array<{ displayName?: string | null; emailAddress?: string | null }>
18
+ }
19
+
20
+ export type DriveFileList = {
21
+ files: DriveFile[]
22
+ nextPageToken?: string
23
+ }
24
+
25
+ export type DriveBrowseResult = {
26
+ folder: DriveFile
27
+ files: DriveFile[]
28
+ folders: DriveFile[]
29
+ nextPageToken?: string
30
+ }
31
+
32
+ export type ListFilesOptions = {
33
+ pageSize?: number
34
+ pageToken?: string
35
+ orderBy?: string
36
+ fields?: string
37
+ corpora?: 'user' | 'drive' | 'allDrives'
38
+ }
39
+
40
+ export type SearchOptions = ListFilesOptions & {
41
+ mimeType?: string
42
+ inFolder?: string
43
+ }
44
+
45
+ export type SharedDrive = {
46
+ id: string
47
+ name: string
48
+ colorRgb?: string
49
+ }
50
+
51
+ export const GoogleDriveStateSchema = FeatureStateSchema.extend({
52
+ lastQuery: z.string().optional()
53
+ .describe('Last search query or folder ID browsed'),
54
+ lastResultCount: z.number().optional()
55
+ .describe('Number of results from last list/search operation'),
56
+ lastError: z.string().optional()
57
+ .describe('Last Drive API error message'),
58
+ })
59
+ export type GoogleDriveState = z.infer<typeof GoogleDriveStateSchema>
60
+
61
+ export const GoogleDriveOptionsSchema = FeatureOptionsSchema.extend({
62
+ defaultCorpora: z.enum(['user', 'drive', 'allDrives']).optional()
63
+ .describe('Default corpus for file queries (default: user)'),
64
+ pageSize: z.number().optional()
65
+ .describe('Default number of results per page (default: 100)'),
66
+ })
67
+ export type GoogleDriveOptions = z.infer<typeof GoogleDriveOptionsSchema>
68
+
69
+ export const GoogleDriveEventsSchema = FeatureEventsSchema.extend({
70
+ filesFetched: z.tuple([z.number().describe('Number of files returned')])
71
+ .describe('Files were fetched from Drive'),
72
+ fileDownloaded: z.tuple([z.string().describe('File ID')])
73
+ .describe('A file was downloaded'),
74
+ error: z.tuple([z.any().describe('The error')]).describe('Drive API error occurred'),
75
+ })
76
+
77
+ const DEFAULT_FIELDS = 'files(id,name,mimeType,size,createdTime,modifiedTime,parents,webViewLink,iconLink,owners),nextPageToken'
78
+
79
+ /**
80
+ * Google Drive feature for listing, searching, browsing, and downloading files.
81
+ *
82
+ * Depends on the googleAuth feature for authentication. Creates a Drive v3 API
83
+ * client lazily and passes the auth client from googleAuth.
84
+ *
85
+ * @example
86
+ * ```typescript
87
+ * const drive = container.feature('googleDrive')
88
+ *
89
+ * // List recent files
90
+ * const { files } = await drive.listFiles()
91
+ *
92
+ * // Search for documents
93
+ * const { files: docs } = await drive.search('quarterly report', { mimeType: 'application/pdf' })
94
+ *
95
+ * // Browse a folder
96
+ * const contents = await drive.browse('folder-id-here')
97
+ *
98
+ * // Download a file to disk
99
+ * await drive.downloadTo('file-id', './downloads/report.pdf')
100
+ * ```
101
+ */
102
+ export class GoogleDrive extends Feature<GoogleDriveState, GoogleDriveOptions> {
103
+ static override shortcut = 'features.googleDrive' as const
104
+ static override stateSchema = GoogleDriveStateSchema
105
+ static override optionsSchema = GoogleDriveOptionsSchema
106
+ static override eventsSchema = GoogleDriveEventsSchema
107
+
108
+ private _drive?: drive_v3.Drive
109
+
110
+ override get initialState(): GoogleDriveState {
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
+ /** Get or create the Drive v3 API client. */
120
+ private async getDrive(): Promise<drive_v3.Drive> {
121
+ if (this._drive) return this._drive
122
+ const auth = await this.auth.getAuthClient()
123
+ this._drive = google.drive({ version: 'v3', auth: auth as any })
124
+ return this._drive
125
+ }
126
+
127
+ /**
128
+ * List files in the user's Drive with an optional query filter.
129
+ *
130
+ * @param query - Drive search query (e.g. "name contains 'report'", "mimeType='application/pdf'")
131
+ * @param options - Pagination and filtering options
132
+ * @returns Files array and optional nextPageToken
133
+ */
134
+ async listFiles(query?: string, options: ListFilesOptions = {}): Promise<DriveFileList> {
135
+ try {
136
+ const drive = await this.getDrive()
137
+ const res = await drive.files.list({
138
+ q: query || undefined,
139
+ pageSize: options.pageSize || this.options.pageSize || 100,
140
+ pageToken: options.pageToken || undefined,
141
+ orderBy: options.orderBy || 'modifiedTime desc',
142
+ fields: options.fields || DEFAULT_FIELDS,
143
+ corpora: options.corpora || this.options.defaultCorpora || 'user',
144
+ includeItemsFromAllDrives: (options.corpora || this.options.defaultCorpora) === 'allDrives',
145
+ supportsAllDrives: true,
146
+ })
147
+
148
+ const files = (res.data.files || []).map(normalizeDriveFile)
149
+ this.setState({ lastQuery: query, lastResultCount: files.length })
150
+ this.emit('filesFetched', files.length)
151
+ return { files, nextPageToken: res.data.nextPageToken || undefined }
152
+ } catch (err: any) {
153
+ this.setState({ lastError: err.message })
154
+ this.emit('error', err)
155
+ throw err
156
+ }
157
+ }
158
+
159
+ /**
160
+ * List files within a specific folder.
161
+ *
162
+ * @param folderId - The Drive folder ID
163
+ * @param options - Pagination and filtering options
164
+ */
165
+ async listFolder(folderId: string, options: ListFilesOptions = {}): Promise<DriveFileList> {
166
+ const query = `'${folderId}' in parents and trashed = false`
167
+ return this.listFiles(query, options)
168
+ }
169
+
170
+ /**
171
+ * Browse a folder's contents, separating files from subfolders.
172
+ *
173
+ * @param folderId - Folder ID to browse (defaults to 'root')
174
+ * @returns Folder metadata, child files, and child folders
175
+ */
176
+ async browse(folderId: string = 'root'): Promise<DriveBrowseResult> {
177
+ const drive = await this.getDrive()
178
+
179
+ // Get folder metadata
180
+ const folderRes = await drive.files.get({
181
+ fileId: folderId,
182
+ fields: 'id,name,mimeType,createdTime,modifiedTime,parents,webViewLink',
183
+ supportsAllDrives: true,
184
+ })
185
+ const folder = normalizeDriveFile(folderRes.data)
186
+
187
+ // List folder contents
188
+ const { files } = await this.listFolder(folderId, { pageSize: 1000 })
189
+ const folders = files.filter(f => f.mimeType === 'application/vnd.google-apps.folder')
190
+ const nonFolders = files.filter(f => f.mimeType !== 'application/vnd.google-apps.folder')
191
+
192
+ return { folder, files: nonFolders, folders }
193
+ }
194
+
195
+ /**
196
+ * Search files by name, content, or MIME type.
197
+ *
198
+ * @param term - Search term to look for in file names and content
199
+ * @param options - Additional search options like mimeType filter or folder restriction
200
+ */
201
+ async search(term: string, options: SearchOptions = {}): Promise<DriveFileList> {
202
+ const parts: string[] = [`fullText contains '${term.replace(/'/g, "\\'")}'`]
203
+
204
+ if (options.mimeType) {
205
+ parts.push(`mimeType = '${options.mimeType}'`)
206
+ }
207
+ if (options.inFolder) {
208
+ parts.push(`'${options.inFolder}' in parents`)
209
+ }
210
+ parts.push('trashed = false')
211
+
212
+ return this.listFiles(parts.join(' and '), options)
213
+ }
214
+
215
+ /**
216
+ * Get file metadata by file ID.
217
+ *
218
+ * @param fileId - The Drive file ID
219
+ * @param fields - Specific fields to request (defaults to common fields)
220
+ */
221
+ async getFile(fileId: string, fields?: string): Promise<DriveFile> {
222
+ try {
223
+ const drive = await this.getDrive()
224
+ const res = await drive.files.get({
225
+ fileId,
226
+ fields: fields || 'id,name,mimeType,size,createdTime,modifiedTime,parents,webViewLink,iconLink,owners',
227
+ supportsAllDrives: true,
228
+ })
229
+ return normalizeDriveFile(res.data)
230
+ } catch (err: any) {
231
+ this.setState({ lastError: err.message })
232
+ this.emit('error', err)
233
+ throw err
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Download a file's content as a Buffer.
239
+ * Uses alt=media for binary download of non-Google files.
240
+ *
241
+ * @param fileId - The Drive file ID
242
+ */
243
+ async download(fileId: string): Promise<Buffer> {
244
+ try {
245
+ const drive = await this.getDrive()
246
+ const res = await drive.files.get(
247
+ { fileId, alt: 'media', supportsAllDrives: true },
248
+ { responseType: 'arraybuffer' }
249
+ )
250
+ this.emit('fileDownloaded', fileId)
251
+ return Buffer.from(res.data as ArrayBuffer)
252
+ } catch (err: any) {
253
+ this.setState({ lastError: err.message })
254
+ this.emit('error', err)
255
+ throw err
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Download a file and save it to a local path.
261
+ *
262
+ * @param fileId - The Drive file ID
263
+ * @param localPath - Local file path (resolved relative to container cwd)
264
+ * @returns Absolute path of the saved file
265
+ */
266
+ async downloadTo(fileId: string, localPath: string): Promise<string> {
267
+ const buffer = await this.download(fileId)
268
+ const outPath = this.container.paths.resolve(localPath)
269
+ await this.container.fs.writeFileAsync(outPath, buffer)
270
+ return outPath
271
+ }
272
+
273
+ /**
274
+ * Export a Google Workspace file (Docs, Sheets, Slides) to a given MIME type.
275
+ * Uses the Files.export endpoint.
276
+ *
277
+ * @param fileId - The Drive file ID of a Google Workspace document
278
+ * @param mimeType - Target MIME type (e.g. 'text/plain', 'application/pdf', 'text/csv')
279
+ */
280
+ async exportFile(fileId: string, mimeType: string): Promise<Buffer> {
281
+ try {
282
+ const drive = await this.getDrive()
283
+ const res = await drive.files.export(
284
+ { fileId, mimeType },
285
+ { responseType: 'arraybuffer' }
286
+ )
287
+ this.emit('fileDownloaded', fileId)
288
+ return Buffer.from(res.data as ArrayBuffer)
289
+ } catch (err: any) {
290
+ this.setState({ lastError: err.message })
291
+ this.emit('error', err)
292
+ throw err
293
+ }
294
+ }
295
+
296
+ /**
297
+ * List all shared drives the user has access to.
298
+ *
299
+ * @returns Array of shared drive objects
300
+ */
301
+ async listDrives(): Promise<SharedDrive[]> {
302
+ try {
303
+ const drive = await this.getDrive()
304
+ const res = await drive.drives.list({ pageSize: 100 })
305
+ return (res.data.drives || []).map(d => ({
306
+ id: d.id || '',
307
+ name: d.name || '',
308
+ colorRgb: d.colorRgb || undefined,
309
+ }))
310
+ } catch (err: any) {
311
+ this.setState({ lastError: err.message })
312
+ this.emit('error', err)
313
+ throw err
314
+ }
315
+ }
316
+ }
317
+
318
+ function normalizeDriveFile(f: drive_v3.Schema$File): DriveFile {
319
+ return {
320
+ id: f.id || '',
321
+ name: f.name || '',
322
+ mimeType: f.mimeType || '',
323
+ size: f.size || undefined,
324
+ createdTime: f.createdTime || undefined,
325
+ modifiedTime: f.modifiedTime || undefined,
326
+ parents: f.parents || undefined,
327
+ webViewLink: f.webViewLink || undefined,
328
+ iconLink: f.iconLink || undefined,
329
+ owners: f.owners?.map(o => ({ displayName: o.displayName, emailAddress: o.emailAddress })) || undefined,
330
+ }
331
+ }
332
+
333
+ declare module '../../feature' {
334
+ interface AvailableFeatures {
335
+ googleDrive: typeof GoogleDrive
336
+ }
337
+ }
338
+
339
+ export default features.register('googleDrive', GoogleDrive)
@@ -0,0 +1,279 @@
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 sheets_v4 } from 'googleapis'
5
+ import type { GoogleAuth } from './google-auth.js'
6
+
7
+ export type SpreadsheetMeta = {
8
+ spreadsheetId: string
9
+ title: string
10
+ locale: string
11
+ sheets: SheetInfo[]
12
+ }
13
+
14
+ export type SheetInfo = {
15
+ sheetId: number
16
+ title: string
17
+ index: number
18
+ rowCount: number
19
+ columnCount: number
20
+ }
21
+
22
+ export const GoogleSheetsStateSchema = FeatureStateSchema.extend({
23
+ lastSpreadsheetId: z.string().optional()
24
+ .describe('Last spreadsheet ID accessed'),
25
+ lastSheetName: z.string().optional()
26
+ .describe('Last sheet/tab name accessed'),
27
+ lastRowCount: z.number().optional()
28
+ .describe('Number of rows returned in last read'),
29
+ lastError: z.string().optional()
30
+ .describe('Last Sheets API error message'),
31
+ })
32
+ export type GoogleSheetsState = z.infer<typeof GoogleSheetsStateSchema>
33
+
34
+ export const GoogleSheetsOptionsSchema = FeatureOptionsSchema.extend({
35
+ defaultSpreadsheetId: z.string().optional()
36
+ .describe('Default spreadsheet ID for operations'),
37
+ })
38
+ export type GoogleSheetsOptions = z.infer<typeof GoogleSheetsOptionsSchema>
39
+
40
+ export const GoogleSheetsEventsSchema = FeatureEventsSchema.extend({
41
+ dataFetched: z.tuple([z.number().describe('Number of rows')])
42
+ .describe('Sheet data was fetched'),
43
+ error: z.tuple([z.any().describe('The error')]).describe('Sheets API error occurred'),
44
+ })
45
+
46
+ /**
47
+ * Google Sheets feature for reading spreadsheet data as JSON, CSV, or raw arrays.
48
+ *
49
+ * Depends on the googleAuth feature for authentication. Creates a Sheets v4 API
50
+ * client lazily and provides convenient methods for reading tabular data.
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * const sheets = container.feature('googleSheets', {
55
+ * defaultSpreadsheetId: '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms'
56
+ * })
57
+ *
58
+ * // Read as JSON objects (first row = headers)
59
+ * const data = await sheets.getAsJson('Sheet1')
60
+ * // => [{ name: 'Alice', age: '30' }, { name: 'Bob', age: '25' }]
61
+ *
62
+ * // Read as CSV string
63
+ * const csv = await sheets.getAsCsv('Revenue')
64
+ *
65
+ * // Read a specific range
66
+ * const values = await sheets.getRange('Sheet1!A1:D10')
67
+ *
68
+ * // Save to file
69
+ * await sheets.saveAsJson('./data/export.json')
70
+ * ```
71
+ */
72
+ export class GoogleSheets extends Feature<GoogleSheetsState, GoogleSheetsOptions> {
73
+ static override shortcut = 'features.googleSheets' as const
74
+ static override stateSchema = GoogleSheetsStateSchema
75
+ static override optionsSchema = GoogleSheetsOptionsSchema
76
+ static override eventsSchema = GoogleSheetsEventsSchema
77
+
78
+ private _sheets?: sheets_v4.Sheets
79
+
80
+ override get initialState(): GoogleSheetsState {
81
+ return { ...super.initialState }
82
+ }
83
+
84
+ /** Access the google-auth feature lazily. */
85
+ get auth(): GoogleAuth {
86
+ return this.container.feature('googleAuth') as unknown as GoogleAuth
87
+ }
88
+
89
+ /** Get or create the Sheets v4 API client. */
90
+ private async getSheets(): Promise<sheets_v4.Sheets> {
91
+ if (this._sheets) return this._sheets
92
+ const auth = await this.auth.getAuthClient()
93
+ this._sheets = google.sheets({ version: 'v4', auth: auth as any })
94
+ return this._sheets
95
+ }
96
+
97
+ /** Resolve spreadsheet ID from argument or default option. */
98
+ private resolveId(spreadsheetId?: string): string {
99
+ const id = spreadsheetId || this.options.defaultSpreadsheetId
100
+ if (!id) throw new Error('Spreadsheet ID required. Pass it as argument or set options.defaultSpreadsheetId.')
101
+ return id
102
+ }
103
+
104
+ /**
105
+ * Get spreadsheet metadata including title, locale, and sheet list.
106
+ *
107
+ * @param spreadsheetId - The spreadsheet ID (defaults to options.defaultSpreadsheetId)
108
+ */
109
+ async getSpreadsheet(spreadsheetId?: string): Promise<SpreadsheetMeta> {
110
+ const id = this.resolveId(spreadsheetId)
111
+ try {
112
+ const sheets = await this.getSheets()
113
+ const res = await sheets.spreadsheets.get({ spreadsheetId: id })
114
+ const data = res.data
115
+
116
+ this.setState({ lastSpreadsheetId: id })
117
+ return {
118
+ spreadsheetId: data.spreadsheetId || id,
119
+ title: data.properties?.title || '',
120
+ locale: data.properties?.locale || 'en_US',
121
+ sheets: (data.sheets || []).map(s => ({
122
+ sheetId: s.properties?.sheetId || 0,
123
+ title: s.properties?.title || '',
124
+ index: s.properties?.index || 0,
125
+ rowCount: s.properties?.gridProperties?.rowCount || 0,
126
+ columnCount: s.properties?.gridProperties?.columnCount || 0,
127
+ })),
128
+ }
129
+ } catch (err: any) {
130
+ this.setState({ lastError: err.message })
131
+ this.emit('error', err)
132
+ throw err
133
+ }
134
+ }
135
+
136
+ /**
137
+ * List all sheets (tabs) in a spreadsheet.
138
+ *
139
+ * @param spreadsheetId - The spreadsheet ID
140
+ */
141
+ async listSheets(spreadsheetId?: string): Promise<SheetInfo[]> {
142
+ const meta = await this.getSpreadsheet(spreadsheetId)
143
+ return meta.sheets
144
+ }
145
+
146
+ /**
147
+ * Read a range of values from a sheet.
148
+ *
149
+ * @param range - A1 notation range (e.g. "Sheet1!A1:D10" or "Sheet1" for entire sheet)
150
+ * @param spreadsheetId - The spreadsheet ID
151
+ * @returns Raw values as a 2D string array
152
+ */
153
+ async getRange(range: string, spreadsheetId?: string): Promise<string[][]> {
154
+ const id = this.resolveId(spreadsheetId)
155
+ try {
156
+ const sheets = await this.getSheets()
157
+ const res = await sheets.spreadsheets.values.get({
158
+ spreadsheetId: id,
159
+ range,
160
+ valueRenderOption: 'FORMATTED_VALUE',
161
+ })
162
+
163
+ const values = (res.data.values || []) as string[][]
164
+ this.setState({
165
+ lastSpreadsheetId: id,
166
+ lastSheetName: range.split('!')[0],
167
+ lastRowCount: values.length,
168
+ })
169
+ this.emit('dataFetched', values.length)
170
+ return values
171
+ } catch (err: any) {
172
+ this.setState({ lastError: err.message })
173
+ this.emit('error', err)
174
+ throw err
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Read a sheet as an array of JSON objects.
180
+ * The first row is treated as headers; subsequent rows become objects keyed by those headers.
181
+ *
182
+ * @param sheetName - Name of the sheet tab (if omitted, reads the first sheet)
183
+ * @param spreadsheetId - The spreadsheet ID
184
+ */
185
+ async getAsJson<T extends Record<string, any> = Record<string, string>>(
186
+ sheetName?: string,
187
+ spreadsheetId?: string
188
+ ): Promise<T[]> {
189
+ const id = this.resolveId(spreadsheetId)
190
+
191
+ // If no sheet name, get the first sheet's name
192
+ let range: string
193
+ if (sheetName) {
194
+ range = sheetName
195
+ } else {
196
+ const meta = await this.getSpreadsheet(id)
197
+ range = meta.sheets[0]?.title || 'Sheet1'
198
+ }
199
+
200
+ const values = await this.getRange(range, id)
201
+ if (values.length < 2) return []
202
+
203
+ const headers = values[0]!
204
+ return values.slice(1).map(row => {
205
+ const obj: Record<string, string> = {}
206
+ headers.forEach((header, i) => {
207
+ obj[header] = row[i] || ''
208
+ })
209
+ return obj as T
210
+ })
211
+ }
212
+
213
+ /**
214
+ * Read a sheet and return it as a CSV string.
215
+ *
216
+ * @param sheetName - Name of the sheet tab (if omitted, reads the first sheet)
217
+ * @param spreadsheetId - The spreadsheet ID
218
+ */
219
+ async getAsCsv(sheetName?: string, spreadsheetId?: string): Promise<string> {
220
+ const id = this.resolveId(spreadsheetId)
221
+
222
+ let range: string
223
+ if (sheetName) {
224
+ range = sheetName
225
+ } else {
226
+ const meta = await this.getSpreadsheet(id)
227
+ range = meta.sheets[0]?.title || 'Sheet1'
228
+ }
229
+
230
+ const values = await this.getRange(range, id)
231
+ return values.map(row =>
232
+ row.map(cell => {
233
+ const str = String(cell)
234
+ if (str.includes(',') || str.includes('"') || str.includes('\n')) {
235
+ return `"${str.replace(/"/g, '""')}"`
236
+ }
237
+ return str
238
+ }).join(',')
239
+ ).join('\n')
240
+ }
241
+
242
+ /**
243
+ * Download sheet data as JSON and save to a local file.
244
+ *
245
+ * @param localPath - Local file path (resolved relative to container cwd)
246
+ * @param sheetName - Sheet tab name (defaults to first sheet)
247
+ * @param spreadsheetId - The spreadsheet ID
248
+ * @returns Absolute path of the saved file
249
+ */
250
+ async saveAsJson(localPath: string, sheetName?: string, spreadsheetId?: string): Promise<string> {
251
+ const data = await this.getAsJson(sheetName, spreadsheetId)
252
+ const outPath = this.container.paths.resolve(localPath)
253
+ await this.container.fs.writeFileAsync(outPath, JSON.stringify(data, null, 2))
254
+ return outPath
255
+ }
256
+
257
+ /**
258
+ * Download sheet data as CSV and save to a local file.
259
+ *
260
+ * @param localPath - Local file path (resolved relative to container cwd)
261
+ * @param sheetName - Sheet tab name (defaults to first sheet)
262
+ * @param spreadsheetId - The spreadsheet ID
263
+ * @returns Absolute path of the saved file
264
+ */
265
+ async saveAsCsv(localPath: string, sheetName?: string, spreadsheetId?: string): Promise<string> {
266
+ const csv = await this.getAsCsv(sheetName, spreadsheetId)
267
+ const outPath = this.container.paths.resolve(localPath)
268
+ await this.container.fs.writeFileAsync(outPath, csv)
269
+ return outPath
270
+ }
271
+ }
272
+
273
+ declare module '../../feature' {
274
+ interface AvailableFeatures {
275
+ googleSheets: typeof GoogleSheets
276
+ }
277
+ }
278
+
279
+ export default features.register('googleSheets', GoogleSheets)