@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,425 @@
1
+ import { z } from 'zod'
2
+ import path from 'path'
3
+ import os from 'os'
4
+ import fs from 'fs/promises'
5
+ import yaml from 'js-yaml'
6
+ import { kebabCase } from 'lodash-es'
7
+ import { FeatureStateSchema, FeatureOptionsSchema } from '../../schemas/base.js'
8
+ import type { Container } from '@soederpop/luca/container'
9
+ import { type AvailableFeatures, features, Feature } from '@soederpop/luca/feature'
10
+ import { Collection, defineModel } from 'contentbase'
11
+ import type { ConversationTool } from './conversation'
12
+
13
+ declare module '@soederpop/luca/feature' {
14
+ interface AvailableFeatures {
15
+ skillsLibrary: typeof SkillsLibrary
16
+ }
17
+ }
18
+
19
+ export interface SkillEntry {
20
+ /** Skill name from frontmatter */
21
+ name: string
22
+ /** Skill description from frontmatter */
23
+ description: string
24
+ /** Markdown body (instructions) */
25
+ body: string
26
+ /** Raw content including frontmatter */
27
+ raw: string
28
+ /** Which collection this came from */
29
+ source: 'project' | 'user'
30
+ /** Directory/path id within its collection */
31
+ pathId: string
32
+ /** All frontmatter metadata */
33
+ meta: Record<string, unknown>
34
+ }
35
+
36
+ const SkillMetaSchema = z.object({
37
+ name: z.string().describe('Unique name identifier for the skill'),
38
+ description: z.string().describe('What the skill does and when to use it'),
39
+ version: z.string().optional().describe('Skill version'),
40
+ tags: z.array(z.string()).optional().describe('Tags for categorization'),
41
+ author: z.string().optional().describe('Skill author'),
42
+ license: z.string().optional().describe('Skill license'),
43
+ })
44
+
45
+ const SkillModel = defineModel('Skill', {
46
+ meta: SkillMetaSchema as any,
47
+ match: (doc: { id: string; meta: Record<string, unknown> }) =>
48
+ doc.id.endsWith('/SKILL') || doc.id === 'SKILL',
49
+ })
50
+
51
+ export const SkillsLibraryStateSchema = FeatureStateSchema.extend({
52
+ loaded: z.boolean().describe('Whether both collections have been loaded'),
53
+ projectSkillCount: z.number().describe('Number of skills in the project collection'),
54
+ userSkillCount: z.number().describe('Number of skills in the user-level collection'),
55
+ totalSkillCount: z.number().describe('Total number of skills across both collections'),
56
+ })
57
+
58
+ export const SkillsLibraryOptionsSchema = FeatureOptionsSchema.extend({
59
+ /** Path to project-level skills directory. Defaults to .claude/skills relative to container cwd. */
60
+ projectSkillsPath: z.string().optional().describe('Path to project-level skills directory'),
61
+ /** Path to user-level skills directory. Defaults to ~/.luca/skills. */
62
+ userSkillsPath: z.string().optional().describe('Path to user-level global skills directory'),
63
+ })
64
+
65
+ export type SkillsLibraryState = z.infer<typeof SkillsLibraryStateSchema>
66
+ export type SkillsLibraryOptions = z.infer<typeof SkillsLibraryOptionsSchema>
67
+
68
+ /**
69
+ * Manages two contentbase collections of skills following the Claude Code SKILL.md format.
70
+ * Project-level skills live in .claude/skills/ and user-level skills live in ~/.luca/skills/.
71
+ * Skills can be discovered, searched, created, updated, and removed at runtime.
72
+ *
73
+ * @extends Feature
74
+ *
75
+ * @example
76
+ * ```typescript
77
+ * const skills = container.feature('skillsLibrary')
78
+ * await skills.load()
79
+ *
80
+ * // List and search
81
+ * const allSkills = skills.list()
82
+ * const matches = skills.search('code review')
83
+ *
84
+ * // Create a new skill
85
+ * await skills.create({
86
+ * name: 'summarize',
87
+ * description: 'Summarize a document',
88
+ * body: '## Instructions\nRead the document and produce a concise summary.'
89
+ * })
90
+ * ```
91
+ */
92
+ export class SkillsLibrary extends Feature<SkillsLibraryState, SkillsLibraryOptions> {
93
+ static override stateSchema = SkillsLibraryStateSchema
94
+ static override optionsSchema = SkillsLibraryOptionsSchema
95
+ static override shortcut = 'features.skillsLibrary' as const
96
+
97
+ private _projectCollection?: Collection
98
+ private _userCollection?: Collection
99
+
100
+ static attach(container: Container<AvailableFeatures, any>) {
101
+ features.register('skillsLibrary', SkillsLibrary)
102
+ return container
103
+ }
104
+
105
+ /** @returns Default state with loaded=false and zero skill counts across both collections. */
106
+ override get initialState(): SkillsLibraryState {
107
+ return {
108
+ ...super.initialState,
109
+ loaded: false,
110
+ projectSkillCount: 0,
111
+ userSkillCount: 0,
112
+ totalSkillCount: 0,
113
+ }
114
+ }
115
+
116
+ /** Returns the project-level contentbase Collection, lazily initialized. */
117
+ get projectCollection(): Collection {
118
+ if (this._projectCollection) return this._projectCollection
119
+ const rootPath =
120
+ this.options.projectSkillsPath ||
121
+ (this.container as any).paths.resolve('.claude', 'skills')
122
+ this._projectCollection = new Collection({ rootPath, extensions: ['md'] })
123
+ this._projectCollection.register(SkillModel)
124
+ return this._projectCollection
125
+ }
126
+
127
+ /** Returns the user-level contentbase Collection, lazily initialized. */
128
+ get userCollection(): Collection {
129
+ if (this._userCollection) return this._userCollection
130
+ const rootPath =
131
+ this.options.userSkillsPath || path.resolve(os.homedir(), '.luca', 'skills')
132
+ this._userCollection = new Collection({ rootPath, extensions: ['md'] })
133
+ this._userCollection.register(SkillModel)
134
+ return this._userCollection
135
+ }
136
+
137
+ /** Whether the skills library has been loaded. */
138
+ get isLoaded(): boolean {
139
+ return !!this.state.get('loaded')
140
+ }
141
+
142
+ /** Array of all skill names across both collections. */
143
+ get skillNames(): string[] {
144
+ return this.list().map((s) => s.name)
145
+ }
146
+
147
+ /**
148
+ * Loads both project and user skill collections from disk.
149
+ * Gracefully handles missing directories.
150
+ *
151
+ * @returns {Promise<SkillsLibrary>} This instance
152
+ */
153
+ async load(): Promise<SkillsLibrary> {
154
+ if (this.isLoaded) return this
155
+
156
+ try {
157
+ await this.projectCollection.load()
158
+ } catch {
159
+ // Directory doesn't exist yet - zero project skills
160
+ }
161
+
162
+ try {
163
+ await this.userCollection.load()
164
+ } catch {
165
+ // Directory doesn't exist yet - zero user skills
166
+ }
167
+
168
+ this.updateCounts()
169
+ this.state.set('loaded', true)
170
+ this.emit('loaded')
171
+ return this
172
+ }
173
+
174
+ /**
175
+ * Lists all skills from both collections. Project skills come first.
176
+ *
177
+ * @returns {SkillEntry[]} All available skills
178
+ */
179
+ list(): SkillEntry[] {
180
+ const projectSkills = this.listFromCollection(this.projectCollection, 'project')
181
+ const userSkills = this.listFromCollection(this.userCollection, 'user')
182
+ return [...projectSkills, ...userSkills]
183
+ }
184
+
185
+ /**
186
+ * Finds a skill by name. Project skills take precedence over user skills.
187
+ *
188
+ * @param {string} name - The skill name to find (case-insensitive)
189
+ * @returns {SkillEntry | undefined} The skill entry, or undefined if not found
190
+ */
191
+ find(name: string): SkillEntry | undefined {
192
+ const lower = name.toLowerCase()
193
+ return this.list().find((s) => s.name.toLowerCase() === lower)
194
+ }
195
+
196
+ /**
197
+ * Searches skills by substring match against name and description.
198
+ *
199
+ * @param {string} query - The search query
200
+ * @returns {SkillEntry[]} Matching skills
201
+ */
202
+ search(query: string): SkillEntry[] {
203
+ const q = query.toLowerCase()
204
+ return this.list().filter(
205
+ (s) =>
206
+ s.name.toLowerCase().includes(q) || s.description.toLowerCase().includes(q)
207
+ )
208
+ }
209
+
210
+ /**
211
+ * Gets a skill by name. Alias for find().
212
+ *
213
+ * @param {string} name - The skill name
214
+ * @returns {SkillEntry | undefined} The skill entry
215
+ */
216
+ getSkill(name: string): SkillEntry | undefined {
217
+ return this.find(name)
218
+ }
219
+
220
+ /**
221
+ * Creates a new SKILL.md file in the specified collection.
222
+ * Maintains the directory-per-skill structure (skill-name/SKILL.md).
223
+ *
224
+ * @param {object} skill - The skill to create
225
+ * @param {'project' | 'user'} target - Which collection to write to (default: 'project')
226
+ * @returns {Promise<SkillEntry>} The created skill entry
227
+ */
228
+ async create(
229
+ skill: {
230
+ name: string
231
+ description: string
232
+ body: string
233
+ meta?: Record<string, unknown>
234
+ },
235
+ target: 'project' | 'user' = 'project'
236
+ ): Promise<SkillEntry> {
237
+ const collection =
238
+ target === 'project' ? this.projectCollection : this.userCollection
239
+
240
+ const frontmatter = (yaml.dump({
241
+ name: skill.name,
242
+ description: skill.description,
243
+ ...skill.meta,
244
+ }) as string).trim()
245
+
246
+ const content = `---\n${frontmatter}\n---\n\n${skill.body}`
247
+ const dirName = kebabCase(skill.name)
248
+ const pathId = `${dirName}/SKILL`
249
+
250
+ await fs.mkdir((collection as any).rootPath, { recursive: true })
251
+ await collection.saveItem(pathId, { content, extension: '.md' })
252
+ await collection.load({ refresh: true })
253
+ this.updateCounts()
254
+
255
+ const entry: SkillEntry = {
256
+ name: skill.name,
257
+ description: skill.description,
258
+ body: skill.body,
259
+ raw: content,
260
+ source: target,
261
+ pathId,
262
+ meta: { name: skill.name, description: skill.description, ...skill.meta },
263
+ }
264
+
265
+ this.emit('skillCreated', entry)
266
+ return entry
267
+ }
268
+
269
+ /**
270
+ * Updates an existing skill's content or metadata.
271
+ *
272
+ * @param {string} name - The skill name to update
273
+ * @param {object} updates - Fields to update
274
+ * @returns {Promise<SkillEntry>} The updated skill entry
275
+ */
276
+ async update(
277
+ name: string,
278
+ updates: {
279
+ description?: string
280
+ body?: string
281
+ meta?: Record<string, unknown>
282
+ }
283
+ ): Promise<SkillEntry> {
284
+ const existing = this.find(name)
285
+ if (!existing) throw new Error(`Skill "${name}" not found`)
286
+
287
+ const collection =
288
+ existing.source === 'project' ? this.projectCollection : this.userCollection
289
+
290
+ const newMeta = { ...existing.meta, ...updates.meta }
291
+ if (updates.description) newMeta.description = updates.description
292
+
293
+ const frontmatter = (yaml.dump(newMeta) as string).trim()
294
+ const body = updates.body ?? existing.body
295
+ const content = `---\n${frontmatter}\n---\n\n${body}`
296
+
297
+ await collection.saveItem(existing.pathId, { content, extension: '.md' })
298
+ await collection.load({ refresh: true })
299
+ this.updateCounts()
300
+
301
+ const entry: SkillEntry = {
302
+ name: existing.name,
303
+ description: updates.description ?? existing.description,
304
+ body,
305
+ raw: content,
306
+ source: existing.source,
307
+ pathId: existing.pathId,
308
+ meta: newMeta,
309
+ }
310
+
311
+ this.emit('skillUpdated', entry)
312
+ return entry
313
+ }
314
+
315
+ /**
316
+ * Removes a skill by name, deleting its SKILL.md and cleaning up the directory.
317
+ *
318
+ * @param {string} name - The skill name to remove
319
+ * @returns {Promise<boolean>} Whether the skill was found and removed
320
+ */
321
+ async remove(name: string): Promise<boolean> {
322
+ const existing = this.find(name)
323
+ if (!existing) return false
324
+
325
+ const collection =
326
+ existing.source === 'project' ? this.projectCollection : this.userCollection
327
+
328
+ await collection.deleteItem(existing.pathId)
329
+
330
+ const skillDir = path.resolve(
331
+ (collection as any).rootPath,
332
+ existing.pathId.split('/')[0]!
333
+ )
334
+ try {
335
+ await fs.rm(skillDir, { recursive: true })
336
+ } catch {
337
+ // directory might have other files or already be gone
338
+ }
339
+
340
+ await collection.load({ refresh: true })
341
+ this.updateCounts()
342
+ this.emit('skillRemoved', existing.name)
343
+ return true
344
+ }
345
+
346
+ /**
347
+ * Converts all skills into ConversationTool format for use with Conversation.
348
+ * Each skill becomes a tool that returns its instruction body when invoked.
349
+ *
350
+ * @returns {Record<string, ConversationTool>} Tools keyed by sanitized skill name
351
+ */
352
+ toConversationTools(): Record<string, ConversationTool> {
353
+ const tools: Record<string, ConversationTool> = {}
354
+
355
+ for (const skill of this.list()) {
356
+ const toolName = `skill_${skill.name.replace(/[^a-zA-Z0-9_]/g, '_')}`
357
+ tools[toolName] = {
358
+ handler: async () => skill.body,
359
+ description: skill.description,
360
+ parameters: {
361
+ type: 'object',
362
+ properties: {},
363
+ },
364
+ }
365
+ }
366
+
367
+ return tools
368
+ }
369
+
370
+ /**
371
+ * Generates a markdown block listing all available skills with names and descriptions.
372
+ * Suitable for injecting into a system prompt.
373
+ *
374
+ * @returns {string} Markdown listing, or empty string if no skills
375
+ */
376
+ toSystemPromptBlock(): string {
377
+ const skills = this.list()
378
+ if (skills.length === 0) return ''
379
+
380
+ const lines = skills.map((s) => `- **${s.name}**: ${s.description}`)
381
+ return `## Available Skills\n\n${lines.join('\n')}`
382
+ }
383
+
384
+ // --- Private ---
385
+
386
+ private listFromCollection(
387
+ collection: Collection,
388
+ source: 'project' | 'user'
389
+ ): SkillEntry[] {
390
+ if (!(collection as any).loaded) return []
391
+
392
+ const entries: SkillEntry[] = []
393
+ for (const pathId of collection.available) {
394
+ if (!pathId.endsWith('/SKILL') && pathId !== 'SKILL') continue
395
+
396
+ const item = collection.items.get(pathId)!
397
+ entries.push({
398
+ name: (item.meta.name as string) || pathId.split('/')[0] || pathId,
399
+ description: (item.meta.description as string) || '',
400
+ body: item.content,
401
+ raw: item.raw,
402
+ source,
403
+ pathId,
404
+ meta: item.meta,
405
+ })
406
+ }
407
+
408
+ return entries
409
+ }
410
+
411
+ private updateCounts(): void {
412
+ const projectCount = this.listFromCollection(
413
+ this.projectCollection,
414
+ 'project'
415
+ ).length
416
+ const userCount = this.listFromCollection(this.userCollection, 'user').length
417
+ this.state.setState({
418
+ projectSkillCount: projectCount,
419
+ userSkillCount: userCount,
420
+ totalSkillCount: projectCount + userCount,
421
+ })
422
+ }
423
+ }
424
+
425
+ export default features.register('skillsLibrary', SkillsLibrary)
@@ -0,0 +1,6 @@
1
+ import container from './container.server'
2
+ import type { AGIContainer } from './container.server'
3
+
4
+ export * from './container.server'
5
+
6
+ export default container as AGIContainer
@@ -0,0 +1,122 @@
1
+ import { encodingForModel, getEncoding } from 'js-tiktoken'
2
+ import type { Tiktoken } from 'js-tiktoken'
3
+
4
+ /**
5
+ * Known model context window sizes. Prefix-matched for dated variants
6
+ * (e.g. "gpt-4o-2024-08-06" matches "gpt-4o").
7
+ */
8
+ const MODEL_CONTEXT_WINDOWS: Record<string, number> = {
9
+ 'gpt-4.1': 1_000_000,
10
+ 'gpt-4.1-mini': 1_000_000,
11
+ 'gpt-4.1-nano': 1_000_000,
12
+ 'gpt-4o': 128_000,
13
+ 'gpt-4o-mini': 128_000,
14
+ 'gpt-4-turbo': 128_000,
15
+ 'gpt-4': 8_192,
16
+ 'gpt-3.5-turbo': 16_385,
17
+ 'o1': 200_000,
18
+ 'o1-mini': 128_000,
19
+ 'o1-pro': 200_000,
20
+ 'o3': 200_000,
21
+ 'o3-mini': 200_000,
22
+ 'o4-mini': 200_000,
23
+ 'gpt-5': 1_000_000,
24
+ }
25
+
26
+ const DEFAULT_CONTEXT_WINDOW = 128_000
27
+
28
+ const encoderCache = new Map<string, Tiktoken>()
29
+
30
+ /** Look up the context window size for a model name (exact then prefix match). */
31
+ export function getContextWindow(model: string): number {
32
+ if (MODEL_CONTEXT_WINDOWS[model]) return MODEL_CONTEXT_WINDOWS[model]
33
+
34
+ // Prefix match — longest prefix wins (e.g. "gpt-4o-mini" before "gpt-4o")
35
+ let best = ''
36
+ for (const key of Object.keys(MODEL_CONTEXT_WINDOWS)) {
37
+ if (model.startsWith(key) && key.length > best.length) {
38
+ best = key
39
+ }
40
+ }
41
+
42
+ return best ? MODEL_CONTEXT_WINDOWS[best] : DEFAULT_CONTEXT_WINDOW
43
+ }
44
+
45
+ /** Get a cached tiktoken encoder for a model (falls back to o200k_base). */
46
+ export function getEncoder(model: string): Tiktoken {
47
+ if (encoderCache.has(model)) return encoderCache.get(model)!
48
+
49
+ let enc: Tiktoken
50
+ try {
51
+ enc = encodingForModel(model as any)
52
+ } catch {
53
+ enc = getEncoding('o200k_base')
54
+ }
55
+
56
+ encoderCache.set(model, enc)
57
+ return enc
58
+ }
59
+
60
+ /** Count tokens in a plain string. */
61
+ export function countTokens(text: string, model: string): number {
62
+ return getEncoder(model).encode(text).length
63
+ }
64
+
65
+ /**
66
+ * Estimate the total input token count for a messages array.
67
+ * Follows the OpenAI token counting recipe with per-message overhead.
68
+ */
69
+ export function countMessageTokens(messages: any[], model: string): number {
70
+ const enc = getEncoder(model)
71
+ const TOKENS_PER_MESSAGE = 3 // <|start|>role\ncontent<|end|>
72
+ const REPLY_PRIMING = 3
73
+
74
+ let total = 0
75
+
76
+ for (const msg of messages) {
77
+ total += TOKENS_PER_MESSAGE
78
+
79
+ // Role
80
+ if (msg.role) {
81
+ total += enc.encode(msg.role).length
82
+ }
83
+
84
+ // Content
85
+ if (typeof msg.content === 'string') {
86
+ total += enc.encode(msg.content).length
87
+ } else if (Array.isArray(msg.content)) {
88
+ for (const part of msg.content) {
89
+ if (part.type === 'text' && part.text) {
90
+ total += enc.encode(part.text).length
91
+ } else if (part.type === 'image_url') {
92
+ // Rough image token estimates
93
+ const detail = part.image_url?.detail || 'auto'
94
+ total += detail === 'low' ? 85 : 170
95
+ } else if (part.type === 'input_audio' || part.type === 'input_file') {
96
+ total += 50 // rough placeholder for non-text parts
97
+ }
98
+ }
99
+ }
100
+
101
+ // Tool calls on assistant messages
102
+ if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
103
+ for (const tc of msg.tool_calls) {
104
+ total += 3 // tool call overhead
105
+ if (tc.function?.name) {
106
+ total += enc.encode(tc.function.name).length
107
+ }
108
+ if (tc.function?.arguments) {
109
+ total += enc.encode(tc.function.arguments).length
110
+ }
111
+ }
112
+ }
113
+
114
+ // Name field (used on some message types)
115
+ if (msg.name) {
116
+ total += enc.encode(msg.name).length + 1
117
+ }
118
+ }
119
+
120
+ total += REPLY_PRIMING
121
+ return total
122
+ }
package/src/browser.ts ADDED
@@ -0,0 +1,25 @@
1
+ export * from './web/container.js'
2
+ import { WebContainer } from './web/container.js'
3
+ import './introspection/generated.web.js'
4
+
5
+ const container = new WebContainer({})
6
+
7
+ if (typeof window !== 'undefined') {
8
+ ;(window as any).luca = container
9
+ }
10
+
11
+ export default container
12
+
13
+ /**
14
+ * Returns the singleton container instance.
15
+ * LLMs love to hallucinate this function — so we provide it, but warn.
16
+ * If you need a separate container, use `container.subcontainer()`.
17
+ */
18
+ export function createContainer() {
19
+ console.warn(
20
+ '[luca] createContainer() is unnecessary — import the default export instead.\n' +
21
+ ' `import container from "@soederpop/luca"`\n' +
22
+ ' For a separate instance, use container.subcontainer().'
23
+ )
24
+ return container
25
+ }
package/src/bus.ts ADDED
@@ -0,0 +1,100 @@
1
+ export type EventMap = Record<string, any[]>;
2
+
3
+ type Listener<Args extends any[] = any[]> = (...args: Args) => void;
4
+
5
+ export interface EventStats {
6
+ event: string;
7
+ fireCount: number;
8
+ lastFiredAt: number | null;
9
+ timestamps: number[];
10
+ firesPerMinute: number;
11
+ }
12
+
13
+ export class Bus<T extends EventMap = EventMap> {
14
+ private events: Map<string, Listener[]>;
15
+ private stats: Map<string, { fireCount: number; lastFiredAt: number | null; timestamps: number[] }>;
16
+
17
+ constructor() {
18
+ this.events = new Map();
19
+ this.stats = new Map();
20
+ }
21
+
22
+ private recordEmit(event: string): void {
23
+ const now = Date.now();
24
+ const existing = this.stats.get(event) || { fireCount: 0, lastFiredAt: null, timestamps: [] };
25
+ existing.fireCount++;
26
+ existing.lastFiredAt = now;
27
+ existing.timestamps.push(now);
28
+ this.stats.set(event, existing);
29
+ }
30
+
31
+ private computeFiresPerMinute(timestamps: number[]): number {
32
+ if (timestamps.length < 2) return 0;
33
+ const now = Date.now();
34
+ const oneMinuteAgo = now - 60_000;
35
+ const recent = timestamps.filter(t => t >= oneMinuteAgo);
36
+ return recent.length;
37
+ }
38
+
39
+ getEventStats<E extends string & keyof T>(event: E): EventStats {
40
+ const raw = this.stats.get(event) || { fireCount: 0, lastFiredAt: null, timestamps: [] };
41
+ return {
42
+ event,
43
+ fireCount: raw.fireCount,
44
+ lastFiredAt: raw.lastFiredAt,
45
+ timestamps: raw.timestamps,
46
+ firesPerMinute: this.computeFiresPerMinute(raw.timestamps),
47
+ };
48
+ }
49
+
50
+ get history(): EventStats[] {
51
+ return Array.from(this.stats.keys()).map(event => this.getEventStats(event as any));
52
+ }
53
+
54
+ get firedEvents(): string[] {
55
+ return Array.from(this.stats.keys());
56
+ }
57
+
58
+ async waitFor<E extends string & keyof T>(event: E): Promise<T[E]> {
59
+ return new Promise(resolve => {
60
+ this.once(event, resolve as any);
61
+ });
62
+ }
63
+
64
+ emit<E extends string & keyof T>(event: E, ...args: T[E]): void {
65
+ this.recordEmit(event);
66
+ const listeners = this.events.get(event);
67
+ if (!listeners) return;
68
+
69
+ listeners.forEach(listener => listener(...args));
70
+ }
71
+
72
+ on<E extends string & keyof T>(event: E, listener: (...args: T[E]) => void): void {
73
+ const listeners = this.events.get(event) || [];
74
+ listeners.push(listener as Listener);
75
+ this.events.set(event, listeners);
76
+ }
77
+
78
+ once<E extends string & keyof T>(event: E, listener: (...args: T[E]) => void): void {
79
+ const onceListener: Listener = (...args: any[]) => {
80
+ (listener as Listener)(...args);
81
+ this.off(event, onceListener as any);
82
+ };
83
+ this.on(event, onceListener as any);
84
+ }
85
+
86
+ off<E extends string & keyof T>(event: E, listener?: (...args: T[E]) => void): void {
87
+ const listeners = this.events.get(event);
88
+ if (!listeners) return;
89
+
90
+ if (!listener) {
91
+ this.events.delete(event);
92
+ return;
93
+ }
94
+
95
+ const index = listeners.indexOf(listener as Listener);
96
+ if (index !== -1) {
97
+ listeners.splice(index, 1);
98
+ }
99
+ }
100
+ }