@soederpop/luca 0.0.6 → 0.0.8

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 (208) hide show
  1. package/CLAUDE.md +10 -1
  2. package/RUNME.md +56 -0
  3. package/bun.lock +1 -1
  4. package/commands/build-bootstrap.ts +78 -0
  5. package/commands/build-scaffolds.ts +24 -2
  6. package/commands/try-all-challenges.ts +543 -0
  7. package/commands/try-challenge.ts +100 -0
  8. package/docs/README.md +52 -80
  9. package/docs/TABLE-OF-CONTENTS.md +82 -51
  10. package/docs/apis/clients/elevenlabs.md +232 -8
  11. package/docs/apis/clients/graph.md +59 -8
  12. package/docs/apis/clients/openai.md +362 -2
  13. package/docs/apis/clients/rest.md +122 -2
  14. package/docs/apis/clients/websocket.md +71 -17
  15. package/docs/apis/features/agi/assistant.md +9 -3
  16. package/docs/apis/features/agi/assistants-manager.md +2 -2
  17. package/docs/apis/features/agi/claude-code.md +153 -14
  18. package/docs/apis/features/agi/conversation-history.md +15 -3
  19. package/docs/apis/features/agi/conversation.md +133 -20
  20. package/docs/apis/features/agi/openai-codex.md +90 -12
  21. package/docs/apis/features/agi/skills-library.md +23 -5
  22. package/docs/apis/features/node/container-link.md +59 -0
  23. package/docs/apis/features/node/content-db.md +1 -1
  24. package/docs/apis/features/node/disk-cache.md +1 -1
  25. package/docs/apis/features/node/dns.md +1 -0
  26. package/docs/apis/features/node/docker.md +2 -1
  27. package/docs/apis/features/node/esbuild.md +4 -3
  28. package/docs/apis/features/node/file-manager.md +13 -4
  29. package/docs/apis/features/node/fs.md +726 -171
  30. package/docs/apis/features/node/git.md +1 -0
  31. package/docs/apis/features/node/google-auth.md +23 -4
  32. package/docs/apis/features/node/google-calendar.md +14 -2
  33. package/docs/apis/features/node/google-docs.md +15 -2
  34. package/docs/apis/features/node/google-drive.md +21 -3
  35. package/docs/apis/features/node/google-sheets.md +14 -2
  36. package/docs/apis/features/node/grep.md +2 -0
  37. package/docs/apis/features/node/helpers.md +29 -0
  38. package/docs/apis/features/node/ink.md +2 -2
  39. package/docs/apis/features/node/networking.md +39 -4
  40. package/docs/apis/features/node/os.md +28 -0
  41. package/docs/apis/features/node/postgres.md +26 -4
  42. package/docs/apis/features/node/proc.md +37 -28
  43. package/docs/apis/features/node/process-manager.md +33 -5
  44. package/docs/apis/features/node/repl.md +1 -1
  45. package/docs/apis/features/node/runpod.md +1 -0
  46. package/docs/apis/features/node/secure-shell.md +7 -0
  47. package/docs/apis/features/node/semantic-search.md +12 -5
  48. package/docs/apis/features/node/sqlite.md +26 -4
  49. package/docs/apis/features/node/telegram.md +30 -5
  50. package/docs/apis/features/node/tts.md +17 -2
  51. package/docs/apis/features/node/ui.md +1 -1
  52. package/docs/apis/features/node/vault.md +4 -9
  53. package/docs/apis/features/node/vm.md +3 -12
  54. package/docs/apis/features/node/window-manager.md +128 -20
  55. package/docs/apis/features/web/asset-loader.md +13 -1
  56. package/docs/apis/features/web/container-link.md +59 -0
  57. package/docs/apis/features/web/esbuild.md +4 -3
  58. package/docs/apis/features/web/helpers.md +29 -0
  59. package/docs/apis/features/web/network.md +16 -2
  60. package/docs/apis/features/web/speech.md +16 -2
  61. package/docs/apis/features/web/vault.md +4 -9
  62. package/docs/apis/features/web/vm.md +3 -12
  63. package/docs/apis/features/web/voice.md +18 -1
  64. package/docs/apis/servers/express.md +18 -2
  65. package/docs/apis/servers/mcp.md +29 -4
  66. package/docs/apis/servers/websocket.md +34 -6
  67. package/docs/bootstrap/CLAUDE.md +100 -0
  68. package/docs/bootstrap/SKILL.md +222 -0
  69. package/docs/bootstrap/templates/about-command.ts +41 -0
  70. package/docs/bootstrap/templates/docs-models.ts +22 -0
  71. package/docs/bootstrap/templates/docs-readme.md +43 -0
  72. package/docs/bootstrap/templates/example-feature.ts +53 -0
  73. package/docs/bootstrap/templates/health-endpoint.ts +15 -0
  74. package/docs/bootstrap/templates/luca-cli.ts +25 -0
  75. package/docs/bootstrap/templates/runme.md +54 -0
  76. package/docs/challenges/caching-proxy.md +16 -0
  77. package/docs/challenges/content-db-round-trip.md +14 -0
  78. package/docs/challenges/custom-command.md +9 -0
  79. package/docs/challenges/file-watcher-pipeline.md +11 -0
  80. package/docs/challenges/grep-audit-report.md +15 -0
  81. package/docs/challenges/multi-feature-dashboard.md +14 -0
  82. package/docs/challenges/process-orchestrator.md +17 -0
  83. package/docs/challenges/rest-api-server-with-client.md +12 -0
  84. package/docs/challenges/script-runner-with-vm.md +11 -0
  85. package/docs/challenges/simple-rest-api.md +15 -0
  86. package/docs/challenges/websocket-serve-and-client.md +11 -0
  87. package/docs/challenges/yaml-config-system.md +14 -0
  88. package/docs/command-system-overhaul.md +94 -0
  89. package/docs/examples/assistant/CORE.md +18 -0
  90. package/docs/examples/assistant/hooks.ts +3 -0
  91. package/docs/examples/assistant/tools.ts +10 -0
  92. package/docs/examples/window-manager-layouts.md +180 -0
  93. package/docs/in-memory-fs.md +4 -0
  94. package/docs/models.ts +13 -10
  95. package/docs/philosophy.md +4 -3
  96. package/docs/reports/console-hmr-design.md +170 -0
  97. package/docs/reports/helper-semantic-search.md +72 -0
  98. package/docs/scaffolds/client.md +29 -20
  99. package/docs/scaffolds/command.md +64 -50
  100. package/docs/scaffolds/endpoint.md +31 -36
  101. package/docs/scaffolds/feature.md +28 -18
  102. package/docs/scaffolds/selector.md +91 -0
  103. package/docs/scaffolds/server.md +18 -9
  104. package/docs/selectors.md +115 -0
  105. package/docs/sessions/custom-command/attempt-log-2.md +195 -0
  106. package/docs/sessions/file-watcher-pipeline/attempt-log-1.md +728 -0
  107. package/docs/sessions/file-watcher-pipeline/attempt-log-2.md +555 -0
  108. package/docs/sessions/grep-audit-report/attempt-log-1.md +289 -0
  109. package/docs/sessions/multi-feature-dashboard/attempt-log-2.md +679 -0
  110. package/docs/sessions/rest-api-server-with-client/attempt-log-1.md +1 -0
  111. package/docs/sessions/rest-api-server-with-client/attempt-log-3.md +920 -0
  112. package/docs/sessions/simple-rest-api/attempt-log-1.md +593 -0
  113. package/docs/sessions/websocket-serve-and-client/attempt-log-2.md +995 -0
  114. package/docs/tutorials/00-bootstrap.md +148 -0
  115. package/docs/tutorials/07-endpoints.md +7 -7
  116. package/docs/tutorials/08-commands.md +153 -72
  117. package/luca.cli.ts +3 -0
  118. package/package.json +6 -5
  119. package/public/index.html +1430 -0
  120. package/scripts/examples/using-ollama.ts +2 -1
  121. package/scripts/update-introspection-data.ts +2 -2
  122. package/src/agi/endpoints/experts.ts +1 -1
  123. package/src/agi/features/assistant.ts +7 -0
  124. package/src/agi/features/assistants-manager.ts +5 -5
  125. package/src/agi/features/claude-code.ts +263 -3
  126. package/src/agi/features/conversation-history.ts +7 -1
  127. package/src/agi/features/conversation.ts +26 -3
  128. package/src/agi/features/openai-codex.ts +26 -2
  129. package/src/agi/features/openapi.ts +6 -1
  130. package/src/agi/features/skills-library.ts +9 -1
  131. package/src/bootstrap/generated.ts +595 -0
  132. package/src/cli/cli.ts +64 -21
  133. package/src/client.ts +23 -357
  134. package/src/clients/civitai/index.ts +1 -1
  135. package/src/clients/client-template.ts +1 -1
  136. package/src/clients/comfyui/index.ts +13 -2
  137. package/src/clients/elevenlabs/index.ts +2 -1
  138. package/src/clients/graph.ts +87 -0
  139. package/src/clients/openai/index.ts +10 -1
  140. package/src/clients/rest.ts +207 -0
  141. package/src/clients/websocket.ts +176 -0
  142. package/src/command.ts +281 -34
  143. package/src/commands/bootstrap.ts +185 -0
  144. package/src/commands/chat.ts +5 -4
  145. package/src/commands/describe.ts +341 -4
  146. package/src/commands/help.ts +35 -9
  147. package/src/commands/index.ts +3 -0
  148. package/src/commands/introspect.ts +92 -2
  149. package/src/commands/prompt.ts +5 -6
  150. package/src/commands/run.ts +75 -10
  151. package/src/commands/save-api-docs.ts +49 -0
  152. package/src/commands/scaffold.ts +169 -23
  153. package/src/commands/select.ts +94 -0
  154. package/src/commands/serve.ts +10 -1
  155. package/src/container.ts +15 -0
  156. package/src/endpoint.ts +19 -0
  157. package/src/graft.ts +181 -0
  158. package/src/introspection/generated.agi.ts +12458 -8968
  159. package/src/introspection/generated.node.ts +10573 -7145
  160. package/src/introspection/generated.web.ts +1 -1
  161. package/src/introspection/index.ts +26 -0
  162. package/src/node/container.ts +6 -7
  163. package/src/node/features/content-db.ts +49 -2
  164. package/src/node/features/disk-cache.ts +16 -9
  165. package/src/node/features/dns.ts +16 -3
  166. package/src/node/features/docker.ts +16 -4
  167. package/src/node/features/esbuild.ts +22 -2
  168. package/src/node/features/file-manager.ts +184 -29
  169. package/src/node/features/fs.ts +704 -248
  170. package/src/node/features/git.ts +21 -8
  171. package/src/node/features/grep.ts +23 -3
  172. package/src/node/features/helpers.ts +372 -43
  173. package/src/node/features/networking.ts +39 -4
  174. package/src/node/features/opener.ts +28 -15
  175. package/src/node/features/os.ts +76 -0
  176. package/src/node/features/port-exposer.ts +11 -1
  177. package/src/node/features/postgres.ts +17 -1
  178. package/src/node/features/proc.ts +4 -1
  179. package/src/node/features/python.ts +63 -14
  180. package/src/node/features/repl.ts +11 -7
  181. package/src/node/features/runpod.ts +16 -3
  182. package/src/node/features/secure-shell.ts +27 -2
  183. package/src/node/features/semantic-search.ts +12 -1
  184. package/src/node/features/ui.ts +5 -69
  185. package/src/node/features/vm.ts +17 -0
  186. package/src/node/features/window-manager.ts +68 -20
  187. package/src/node.ts +5 -0
  188. package/src/scaffolds/generated.ts +492 -290
  189. package/src/scaffolds/template.ts +9 -0
  190. package/src/schemas/base.ts +46 -5
  191. package/src/selector.ts +282 -0
  192. package/src/server.ts +11 -0
  193. package/src/servers/express.ts +27 -12
  194. package/src/servers/socket.ts +45 -11
  195. package/src/web/clients/socket.ts +4 -1
  196. package/src/web/container.ts +2 -1
  197. package/src/web/features/network.ts +7 -1
  198. package/src/web/features/voice-recognition.ts +16 -1
  199. package/test/clients-servers.test.ts +2 -1
  200. package/test/command.test.ts +267 -0
  201. package/test/vm-context.test.ts +146 -0
  202. package/test-integration/assistants-manager.test.ts +10 -20
  203. package/docs/apis/features/node/launcher-app-command-listener.md +0 -145
  204. package/docs/examples/launcher-app-command-listener.md +0 -120
  205. package/docs/tasks/web-container-helper-discovery.md +0 -71
  206. package/docs/todos.md +0 -1
  207. package/scripts/test-command-listener.ts +0 -123
  208. package/src/node/features/launcher-app-command-listener.ts +0 -389
@@ -37,6 +37,19 @@ export class Opener extends Feature {
37
37
  static override optionsSchema = FeatureOptionsSchema
38
38
  static { Feature.register(this, 'opener') }
39
39
 
40
+ private _binCache: Record<string, string> = {}
41
+
42
+ /** Resolve a binary path via `which`, caching the result. */
43
+ private resolveBin(name: string): string {
44
+ if (this._binCache[name]) return this._binCache[name]
45
+ try {
46
+ this._binCache[name] = this.container.proc.exec(`which ${name}`).trim()
47
+ } catch {
48
+ this._binCache[name] = name
49
+ }
50
+ return this._binCache[name]
51
+ }
52
+
40
53
  /**
41
54
  * Opens a path or URL with the appropriate application.
42
55
  *
@@ -62,18 +75,18 @@ export class Opener extends Feature {
62
75
  const proc = this.container.proc
63
76
 
64
77
  if (platform === 'darwin') {
65
- await proc.spawnAndCapture('open', ['-a', 'Google Chrome', url])
78
+ await proc.spawnAndCapture(this.resolveBin('open'), ['-a', 'Google Chrome', url])
66
79
  } else if (platform === 'win32') {
67
- await proc.spawnAndCapture('cmd', ['/c', 'start', 'chrome', url])
80
+ await proc.spawnAndCapture(this.resolveBin('cmd'), ['/c', 'start', 'chrome', url])
68
81
  } else {
69
82
  // Linux - try google-chrome, then chromium, then fall back to xdg-open
70
83
  try {
71
- await proc.spawnAndCapture('google-chrome', [url])
84
+ await proc.spawnAndCapture(this.resolveBin('google-chrome'), [url])
72
85
  } catch {
73
86
  try {
74
- await proc.spawnAndCapture('chromium', [url])
87
+ await proc.spawnAndCapture(this.resolveBin('chromium'), [url])
75
88
  } catch {
76
- await proc.spawnAndCapture('xdg-open', [url])
89
+ await proc.spawnAndCapture(this.resolveBin('xdg-open'), [url])
77
90
  }
78
91
  }
79
92
  }
@@ -93,11 +106,11 @@ export class Opener extends Feature {
93
106
  const proc = this.container.proc
94
107
 
95
108
  if (platform === 'darwin') {
96
- await proc.spawnAndCapture('open', ['-a', name])
109
+ await proc.spawnAndCapture(this.resolveBin('open'), ['-a', name])
97
110
  } else if (platform === 'win32') {
98
- await proc.spawnAndCapture('cmd', ['/c', 'start', '', name])
111
+ await proc.spawnAndCapture(this.resolveBin('cmd'), ['/c', 'start', '', name])
99
112
  } else {
100
- await proc.spawnAndCapture(name.toLowerCase(), [])
113
+ await proc.spawnAndCapture(this.resolveBin(name.toLowerCase()), [])
101
114
  }
102
115
  }
103
116
 
@@ -113,10 +126,10 @@ export class Opener extends Feature {
113
126
  const proc = this.container.proc
114
127
 
115
128
  try {
116
- await proc.spawnAndCapture('code', [path])
129
+ await proc.spawnAndCapture(this.resolveBin('code'), [path])
117
130
  } catch {
118
131
  if (this.container.os.platform === 'darwin') {
119
- await proc.spawnAndCapture('open', ['-a', 'Visual Studio Code', path])
132
+ await proc.spawnAndCapture(this.resolveBin('open'), ['-a', 'Visual Studio Code', path])
120
133
  } else {
121
134
  throw new Error('VS Code CLI (code) not found. Install it from VS Code: Command Palette > "Shell Command: Install code command in PATH"')
122
135
  }
@@ -135,10 +148,10 @@ export class Opener extends Feature {
135
148
  const proc = this.container.proc
136
149
 
137
150
  try {
138
- await proc.spawnAndCapture('cursor', [path])
151
+ await proc.spawnAndCapture(this.resolveBin('cursor'), [path])
139
152
  } catch {
140
153
  if (this.container.os.platform === 'darwin') {
141
- await proc.spawnAndCapture('open', ['-a', 'Cursor', path])
154
+ await proc.spawnAndCapture(this.resolveBin('open'), ['-a', 'Cursor', path])
142
155
  } else {
143
156
  throw new Error('Cursor CLI (cursor) not found. Install it from Cursor: Command Palette > "Shell Command: Install cursor command in PATH"')
144
157
  }
@@ -149,11 +162,11 @@ export class Opener extends Feature {
149
162
  const proc = this.container.proc
150
163
 
151
164
  if (platform === 'darwin') {
152
- await proc.spawnAndCapture('open', [target])
165
+ await proc.spawnAndCapture(this.resolveBin('open'), [target])
153
166
  } else if (platform === 'win32') {
154
- await proc.spawnAndCapture('cmd', ['/c', 'start', '', target])
167
+ await proc.spawnAndCapture(this.resolveBin('cmd'), ['/c', 'start', '', target])
155
168
  } else {
156
- await proc.spawnAndCapture('xdg-open', [target])
169
+ await proc.spawnAndCapture(this.resolveBin('xdg-open'), [target])
157
170
  }
158
171
  }
159
172
  }
@@ -2,6 +2,16 @@ import { Feature } from '../feature.js'
2
2
  import { FeatureStateSchema, FeatureOptionsSchema } from '../../schemas/base.js'
3
3
  import os from 'os'
4
4
 
5
+ /** Information about a connected display. */
6
+ export interface DisplayInfo {
7
+ name: string
8
+ resolution: { width: number; height: number }
9
+ retina: boolean
10
+ main: boolean
11
+ refreshRate?: number
12
+ connectionType?: string
13
+ }
14
+
5
15
  /**
6
16
  * The OS feature provides access to operating system utilities and information.
7
17
  *
@@ -153,6 +163,72 @@ export class OS extends Feature {
153
163
  get macAddresses() : string[] {
154
164
  return Object.values(this.networkInterfaces).flat().filter(v => typeof v !== 'undefined' && v.internal === false && v.family === 'IPv4').map(v => v?.mac!).filter(Boolean)
155
165
  }
166
+
167
+ /**
168
+ * Gets information about all connected displays.
169
+ *
170
+ * Platform-specific: currently implemented for macOS (darwin).
171
+ * Linux and Windows will throw with a clear "not yet implemented" message.
172
+ *
173
+ * @returns {DisplayInfo[]} Array of display information objects
174
+ *
175
+ * @example
176
+ * ```typescript
177
+ * const displays = os.getDisplayInfo()
178
+ * displays.forEach(d => {
179
+ * console.log(`${d.name}: ${d.resolution.width}x${d.resolution.height}${d.retina ? ' (Retina)' : ''}`)
180
+ * })
181
+ * ```
182
+ */
183
+ getDisplayInfo(): DisplayInfo[] {
184
+ const platform = this.platform as NodeJS.Platform
185
+ const handler = this._displayHandlers[platform]
186
+
187
+ if (!handler) {
188
+ throw new Error(`getDisplayInfo() is not yet implemented for platform: ${platform}`)
189
+ }
190
+
191
+ return handler()
192
+ }
193
+
194
+ private _displayHandlers: Partial<Record<NodeJS.Platform, () => DisplayInfo[]>> = {
195
+ darwin: (): DisplayInfo[] => {
196
+ const proc = this.container.feature('proc')
197
+ const raw = proc.exec('system_profiler SPDisplaysDataType -json')
198
+ const data = JSON.parse(raw)
199
+ const displays: DisplayInfo[] = []
200
+
201
+ for (const gpu of data.SPDisplaysDataType ?? []) {
202
+ for (const d of gpu.spdisplays_ndrvs ?? []) {
203
+ const resStr: string = d._spdisplays_resolution ?? ''
204
+ const resMatch = resStr.match(/(\d+)\s*x\s*(\d+)/)
205
+ const hzMatch = resStr.match(/@\s*([\d.]+)\s*Hz/i)
206
+
207
+ displays.push({
208
+ name: d._name ?? 'Unknown',
209
+ resolution: {
210
+ width: resMatch ? parseInt(resMatch[1], 10) : 0,
211
+ height: resMatch ? parseInt(resMatch[2], 10) : 0,
212
+ },
213
+ retina: /retina/i.test(d._spdisplays_resolution ?? '') || /retina/i.test(d.spdisplays_display_type ?? ''),
214
+ main: d.spdisplays_main === 'spdisplays_yes' || /yes/i.test(d.spdisplays_main ?? ''),
215
+ refreshRate: hzMatch ? parseFloat(hzMatch[1]) : undefined,
216
+ connectionType: d.spdisplays_connection_type ?? undefined,
217
+ })
218
+ }
219
+ }
220
+
221
+ return displays
222
+ },
223
+
224
+ linux: (): DisplayInfo[] => {
225
+ throw new Error('getDisplayInfo() is not yet implemented for Linux')
226
+ },
227
+
228
+ win32: (): DisplayInfo[] => {
229
+ throw new Error('getDisplayInfo() is not yet implemented for Windows')
230
+ },
231
+ }
156
232
  }
157
233
 
158
234
  export default OS
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod'
2
- import { FeatureStateSchema, FeatureOptionsSchema } from '../../schemas/base.js'
2
+ import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
3
3
  import * as ngrok from '@ngrok/ngrok'
4
4
  import { Feature } from '../../feature.js'
5
5
 
@@ -49,6 +49,15 @@ export const PortExposerOptionsSchema = FeatureOptionsSchema.extend({
49
49
  })
50
50
  export type PortExposerOptions = z.infer<typeof PortExposerOptionsSchema>
51
51
 
52
+ export const PortExposerEventsSchema = FeatureEventsSchema.extend({
53
+ exposed: z.tuple([z.object({
54
+ publicUrl: z.string().optional().describe('The public ngrok URL'),
55
+ localPort: z.number().describe('The local port being exposed'),
56
+ }).describe('Exposure details')]).describe('When a local port is successfully exposed via ngrok'),
57
+ closed: z.tuple([]).describe('When the ngrok tunnel is closed'),
58
+ error: z.tuple([z.any().describe('The error object')]).describe('When an ngrok operation fails'),
59
+ }).describe('Port Exposer events')
60
+
52
61
  /**
53
62
  * Port Exposer Feature
54
63
  *
@@ -81,6 +90,7 @@ export class PortExposer extends Feature<PortExposerState, PortExposerOptions> {
81
90
  static override shortcut = 'portExposer' as const
82
91
  static override stateSchema = PortExposerStateSchema
83
92
  static override optionsSchema = PortExposerOptionsSchema
93
+ static override eventsSchema = PortExposerEventsSchema
84
94
  static { Feature.register(this, 'portExposer') }
85
95
 
86
96
  private ngrokListener?: ngrok.Listener
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod'
2
2
  import { SQL } from 'bun'
3
3
  import { Feature } from '../feature.js'
4
- import { FeatureStateSchema, FeatureOptionsSchema } from '../../schemas/base.js'
4
+ import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
5
5
  import type { ContainerContext } from '../../container.js'
6
6
 
7
7
  type SqlValue = string | number | boolean | bigint | Uint8Array | Buffer | null
@@ -21,6 +21,21 @@ export const PostgresOptionsSchema = FeatureOptionsSchema.extend({
21
21
  export type PostgresState = z.infer<typeof PostgresStateSchema>
22
22
  export type PostgresOptions = z.infer<typeof PostgresOptionsSchema>
23
23
 
24
+ export const PostgresEventsSchema = FeatureEventsSchema.extend({
25
+ query: z.tuple([
26
+ z.string().describe('The SQL query text'),
27
+ z.array(z.any()).describe('Bound parameter values'),
28
+ z.number().describe('Number of rows returned'),
29
+ ]).describe('When a SELECT-like query is executed'),
30
+ execute: z.tuple([
31
+ z.string().describe('The SQL statement text'),
32
+ z.array(z.any()).describe('Bound parameter values'),
33
+ z.number().describe('Number of rows affected'),
34
+ ]).describe('When a write/update/delete statement is executed'),
35
+ error: z.tuple([z.any().describe('The error object')]).describe('When a postgres operation fails'),
36
+ closed: z.tuple([]).describe('When the postgres connection is closed'),
37
+ }).describe('Postgres events')
38
+
24
39
  /**
25
40
  * Postgres feature for safe SQL execution through Bun's native SQL client.
26
41
  *
@@ -46,6 +61,7 @@ export class Postgres extends Feature<PostgresState, PostgresOptions> {
46
61
  static override shortcut = 'features.postgres' as const
47
62
  static override stateSchema = PostgresStateSchema
48
63
  static override optionsSchema = PostgresOptionsSchema
64
+ static override eventsSchema = PostgresEventsSchema
49
65
  static { Feature.register(this, 'postgres') }
50
66
 
51
67
  private _client: SQL
@@ -22,6 +22,8 @@ interface SpawnOptions {
22
22
  onOutput?: (data: string) => void;
23
23
  /** Callback invoked when the process exits */
24
24
  onExit?: (code: number) => void;
25
+ /** Callback invoked when the process starts */
26
+ onStart?: (childProcess: ChildProcess) => void;
25
27
  }
26
28
 
27
29
  interface RawSpawnOptions {
@@ -69,6 +71,7 @@ export class ChildProcess extends Feature {
69
71
  static override shortcut = "features.proc" as const
70
72
  static override stateSchema = FeatureStateSchema
71
73
  static override optionsSchema = FeatureOptionsSchema
74
+ // @ts-ignore TODO: fix this
72
75
  static { Feature.register(this, 'proc') }
73
76
 
74
77
  /**
@@ -184,7 +187,7 @@ export class ChildProcess extends Feature {
184
187
  const childProcess = proc.childProcess!;
185
188
 
186
189
  if (typeof options?.onStart === 'function') {
187
- options.onStart(childProcess)
190
+ options.onStart(childProcess as any)
188
191
  }
189
192
 
190
193
  if (childProcess.stdout && childProcess.stderr) {
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod'
2
- import { FeatureStateSchema, FeatureOptionsSchema } from '../../schemas/base.js'
2
+ import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
3
3
  import { Feature } from "../feature.js";
4
4
  import { existsSync } from 'fs';
5
5
  import { join, resolve } from 'path';
@@ -31,6 +31,46 @@ export const PythonOptionsSchema = FeatureOptionsSchema.extend({
31
31
  export type PythonState = z.infer<typeof PythonStateSchema>
32
32
  export type PythonOptions = z.infer<typeof PythonOptionsSchema>
33
33
 
34
+ export const PythonEventsSchema = FeatureEventsSchema.extend({
35
+ ready: z.tuple([]).describe('When the Python environment is ready for execution'),
36
+ environmentDetected: z.tuple([z.object({
37
+ pythonPath: z.string().nullable().describe('Path to the detected Python executable'),
38
+ environmentType: z.enum(['uv', 'conda', 'venv', 'system']).nullable().describe('Detected environment type'),
39
+ }).describe('Environment detection result')]).describe('When the Python environment type is detected'),
40
+ installingDependencies: z.tuple([z.object({
41
+ command: z.string().describe('The install command being run'),
42
+ }).describe('Install details')]).describe('When dependency installation begins'),
43
+ dependenciesInstalled: z.tuple([z.object({
44
+ stdout: z.string().describe('Standard output from install'),
45
+ stderr: z.string().describe('Standard error from install'),
46
+ exitCode: z.number().describe('Process exit code'),
47
+ }).describe('Install result')]).describe('When dependencies are successfully installed'),
48
+ dependencyInstallFailed: z.tuple([z.object({
49
+ stdout: z.string().describe('Standard output from install'),
50
+ stderr: z.string().describe('Standard error from install'),
51
+ exitCode: z.number().describe('Process exit code'),
52
+ }).describe('Install result')]).describe('When dependency installation fails'),
53
+ codeExecuted: z.tuple([z.object({
54
+ code: z.string().describe('The Python code that was executed'),
55
+ variables: z.record(z.any()).describe('Variables passed to the execution'),
56
+ result: z.object({
57
+ stdout: z.string().describe('Standard output'),
58
+ stderr: z.string().describe('Standard error'),
59
+ exitCode: z.number().describe('Process exit code'),
60
+ }).describe('Execution result'),
61
+ }).describe('Code execution details')]).describe('When Python code finishes executing'),
62
+ fileExecuted: z.tuple([z.object({
63
+ filePath: z.string().describe('Path to the executed Python file'),
64
+ variables: z.record(z.any()).describe('Variables passed as arguments'),
65
+ result: z.object({
66
+ stdout: z.string().describe('Standard output'),
67
+ stderr: z.string().describe('Standard error'),
68
+ exitCode: z.number().describe('Process exit code'),
69
+ }).describe('Execution result'),
70
+ }).describe('File execution details')]).describe('When a Python file finishes executing'),
71
+ localsParseError: z.tuple([z.any().describe('The parse error')]).describe('When captured locals fail to parse as JSON'),
72
+ }).describe('Python events')
73
+
34
74
  /**
35
75
  * The Python VM feature provides Python virtual machine capabilities for executing Python code.
36
76
  *
@@ -64,6 +104,7 @@ export class Python<
64
104
  static override shortcut = "features.python" as const
65
105
  static override stateSchema = PythonStateSchema
66
106
  static override optionsSchema = PythonOptionsSchema
107
+ static override eventsSchema = PythonEventsSchema
67
108
  static { Feature.register(this, 'python') }
68
109
 
69
110
  override get initialState(): T {
@@ -137,6 +178,13 @@ export class Python<
137
178
  let pythonPath: string | null = null
138
179
  let environmentType: PythonState['environmentType'] = null
139
180
 
181
+ const proc = this.container.feature('proc')
182
+
183
+ /** Resolve a binary to its full path via `which`, falling back to the bare name. */
184
+ const resolveBin = (name: string): string => {
185
+ try { return proc.exec(`which ${name}`).trim() } catch { return name }
186
+ }
187
+
140
188
  // Use explicitly provided Python path
141
189
  if (this.options.pythonPath) {
142
190
  pythonPath = this.options.pythonPath
@@ -145,10 +193,10 @@ export class Python<
145
193
  // Check for uv
146
194
  else if (existsSync(join(projectDir, 'uv.lock')) || existsSync(join(projectDir, 'pyproject.toml'))) {
147
195
  try {
148
- const proc = this.container.feature('proc')
149
- const result = await proc.execAndCapture('uv run python --version')
196
+ const uvBin = resolveBin('uv')
197
+ const result = await proc.execAndCapture(`${uvBin} run python --version`)
150
198
  if (result.exitCode === 0) {
151
- pythonPath = 'uv run python'
199
+ pythonPath = `${uvBin} run python`
152
200
  environmentType = 'uv'
153
201
  }
154
202
  } catch (error) {
@@ -158,10 +206,10 @@ export class Python<
158
206
  // Check for conda
159
207
  else if (existsSync(join(projectDir, 'environment.yml')) || existsSync(join(projectDir, 'conda.yml'))) {
160
208
  try {
161
- const proc = this.container.feature('proc')
162
- const result = await proc.execAndCapture('conda run python --version')
209
+ const condaBin = resolveBin('conda')
210
+ const result = await proc.execAndCapture(`${condaBin} run python --version`)
163
211
  if (result.exitCode === 0) {
164
- pythonPath = 'conda run python'
212
+ pythonPath = `${condaBin} run python`
165
213
  environmentType = 'conda'
166
214
  }
167
215
  } catch (error) {
@@ -171,10 +219,10 @@ export class Python<
171
219
  // Check for venv
172
220
  else if (existsSync(join(projectDir, 'venv')) || existsSync(join(projectDir, '.venv'))) {
173
221
  const venvPath = existsSync(join(projectDir, 'venv')) ? 'venv' : '.venv'
174
- const venvPython = process.platform === 'win32'
222
+ const venvPython = process.platform === 'win32'
175
223
  ? join(projectDir, venvPath, 'Scripts', 'python.exe')
176
224
  : join(projectDir, venvPath, 'bin', 'python')
177
-
225
+
178
226
  if (existsSync(venvPython)) {
179
227
  pythonPath = venvPython
180
228
  environmentType = 'venv'
@@ -184,15 +232,16 @@ export class Python<
184
232
  // Fall back to system Python
185
233
  if (!pythonPath) {
186
234
  try {
187
- const proc = this.container.feature('proc')
188
- const result = await proc.execAndCapture('python3 --version')
235
+ const python3Bin = resolveBin('python3')
236
+ const result = await proc.execAndCapture(`${python3Bin} --version`)
189
237
  if (result.exitCode === 0) {
190
- pythonPath = 'python3'
238
+ pythonPath = python3Bin
191
239
  environmentType = 'system'
192
240
  } else {
193
- const result2 = await proc.execAndCapture('python --version')
241
+ const pythonBin = resolveBin('python')
242
+ const result2 = await proc.execAndCapture(`${pythonBin} --version`)
194
243
  if (result2.exitCode === 0) {
195
- pythonPath = 'python'
244
+ pythonPath = pythonBin
196
245
  environmentType = 'system'
197
246
  }
198
247
  }
@@ -61,7 +61,7 @@ export class Repl<
61
61
  * Type `.exit` or `exit` to quit. Supports top-level await.
62
62
  *
63
63
  * @param options - Configuration for the REPL session
64
- * @param options.historyPath - Custom path for the history file (defaults to node_modules/.cache/.repl_history)
64
+ * @param options.historyPath - Custom path for the history file (defaults to ~/.cache/luca/repl-{cwdHash}.history)
65
65
  * @param options.context - Additional variables to inject into the VM context
66
66
  * @returns The Repl instance
67
67
  *
@@ -81,17 +81,20 @@ export class Repl<
81
81
 
82
82
  const { prompt = "> " } = this.options;
83
83
 
84
- // Set up history file
84
+ // Set up history file — per-project history keyed by cwd hash
85
85
  const userHistoryPath = options.historyPath || this.options.historyPath
86
- this._historyPath = typeof userHistoryPath === 'string'
87
- ? this.container.paths.resolve(userHistoryPath)
88
- : this.container.paths.resolve('node_modules', '.cache', '.repl_history')
86
+ if (typeof userHistoryPath === 'string') {
87
+ this._historyPath = this.container.paths.resolve(userHistoryPath)
88
+ } else {
89
+ const cwdHash = this.container.utils.hashObject(this.container.cwd)
90
+ this._historyPath = this.container.paths.resolve(process.env.HOME!, '.cache', 'luca', `repl-${cwdHash}.history`)
91
+ }
89
92
 
90
93
  this.container.fs.ensureFolder(this.container.paths.dirname(this._historyPath))
91
94
 
92
95
  // Load existing history
93
96
  try {
94
- const content = fs.readFileSync(this._historyPath, 'utf-8')
97
+ const content = fs.readFile(this._historyPath, 'utf-8')
95
98
  this._history = content.split('\n').filter(Boolean).reverse()
96
99
  } catch {}
97
100
 
@@ -99,6 +102,7 @@ export class Repl<
99
102
  this._vmContext = vm.createContext({
100
103
  ...this.container.context,
101
104
  ...options.context,
105
+ setTimeout, setInterval, process, clearInterval, clearTimeout, Buffer, URL, URLSearchParams,
102
106
  // @ts-ignore
103
107
  client: (...args: any[]) => this.container.client(...args),
104
108
  })
@@ -192,4 +196,4 @@ export class Repl<
192
196
  }
193
197
  }
194
198
 
195
- export default Repl
199
+ export default Repl
@@ -37,11 +37,24 @@ export class Runpod extends Feature<RunpodState, RunpodOptions> {
37
37
  static override optionsSchema = RunpodOptionsSchema
38
38
  static { Feature.register(this, 'runpod') }
39
39
 
40
+ private _resolvedRunpodctlPath: string | null = null
41
+
40
42
  /** The proc feature used for executing CLI commands like runpodctl. */
41
43
  get proc() {
42
44
  return this.container.feature('proc')
43
45
  }
44
46
 
47
+ /** Resolve the runpodctl binary path via `which`, caching the result. */
48
+ get runpodctlPath(): string {
49
+ if (this._resolvedRunpodctlPath) return this._resolvedRunpodctlPath
50
+ try {
51
+ this._resolvedRunpodctlPath = this.proc.exec('which runpodctl').trim()
52
+ } catch {
53
+ this._resolvedRunpodctlPath = 'runpodctl'
54
+ }
55
+ return this._resolvedRunpodctlPath
56
+ }
57
+
45
58
  /** RunPod API key from options or the RUNPOD_API_KEY environment variable. */
46
59
  get apiKey() {
47
60
  return this.options.apiKey || process.env.RUNPOD_API_KEY || ''
@@ -510,7 +523,7 @@ export class Runpod extends Feature<RunpodState, RunpodOptions> {
510
523
  * ```
511
524
  */
512
525
  async listPods(detailed = false): Promise<PodInfo[]> {
513
- const { stdout: output } = await this.proc.spawnAndCapture('runpodctl', ['get', 'pod', '-a'])
526
+ const { stdout: output } = await this.proc.spawnAndCapture(this.runpodctlPath, ['get', 'pod', '-a'])
514
527
  const pods = output
515
528
  .trim()
516
529
  .split("\n")
@@ -550,7 +563,7 @@ export class Runpod extends Feature<RunpodState, RunpodOptions> {
550
563
  * ```
551
564
  */
552
565
  async getPodInfo(podId: string): Promise<PodInfo> {
553
- const { stdout: output } = await this.proc.spawnAndCapture('runpodctl', ['get', 'pod', podId, '-a'])
566
+ const { stdout: output } = await this.proc.spawnAndCapture(this.runpodctlPath, ['get', 'pod', podId, '-a'])
554
567
 
555
568
  return output
556
569
  .trim()
@@ -588,7 +601,7 @@ export class Runpod extends Feature<RunpodState, RunpodOptions> {
588
601
  * ```
589
602
  */
590
603
  async listSecureGPUs() {
591
- const { stdout: output } = await this.proc.spawnAndCapture('runpodctl', ['get', 'cloud', '--secure'])
604
+ const { stdout: output } = await this.proc.spawnAndCapture(this.runpodctlPath, ['get', 'cloud', '--secure'])
592
605
 
593
606
  return output
594
607
  .split("\n")
@@ -58,6 +58,9 @@ export class SecureShell extends Feature<SecureShellState, SecureShellOptions> {
58
58
  }
59
59
  }
60
60
 
61
+ private _resolvedSshPath: string | null = null
62
+ private _resolvedScpPath: string | null = null
63
+
61
64
  /**
62
65
  * Get the proc feature for executing shell commands
63
66
  */
@@ -65,6 +68,28 @@ export class SecureShell extends Feature<SecureShellState, SecureShellOptions> {
65
68
  return this.container.feature('proc')
66
69
  }
67
70
 
71
+ /** Resolved path to the ssh binary */
72
+ get sshPath(): string {
73
+ if (this._resolvedSshPath) return this._resolvedSshPath
74
+ try {
75
+ this._resolvedSshPath = this.proc.exec('which ssh').trim()
76
+ } catch {
77
+ this._resolvedSshPath = 'ssh'
78
+ }
79
+ return this._resolvedSshPath
80
+ }
81
+
82
+ /** Resolved path to the scp binary */
83
+ get scpPath(): string {
84
+ if (this._resolvedScpPath) return this._resolvedScpPath
85
+ try {
86
+ this._resolvedScpPath = this.proc.exec('which scp').trim()
87
+ } catch {
88
+ this._resolvedScpPath = 'scp'
89
+ }
90
+ return this._resolvedScpPath
91
+ }
92
+
68
93
  /**
69
94
  * Validate that required options are provided
70
95
  */
@@ -86,7 +111,7 @@ export class SecureShell extends Feature<SecureShellState, SecureShellOptions> {
86
111
  private buildSSHConnectionString(): string {
87
112
  this.validateOptions()
88
113
  const { host, port = 22, username, key } = this.options
89
- let sshCmd = `ssh -p ${port}`
114
+ let sshCmd = `${this.sshPath} -p ${port}`
90
115
 
91
116
  if (key) {
92
117
  sshCmd += ` -i "${key}"`
@@ -106,7 +131,7 @@ export class SecureShell extends Feature<SecureShellState, SecureShellOptions> {
106
131
  private buildSCPConnectionString(): string {
107
132
  this.validateOptions()
108
133
  const { host, port = 22, username, key } = this.options
109
- let scpCmd = `scp -P ${port}`
134
+ let scpCmd = `${this.scpPath} -P ${port}`
110
135
 
111
136
  if (key) {
112
137
  scpCmd += ` -i "${key}"`
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod'
2
- import { FeatureStateSchema, FeatureOptionsSchema } from '../../schemas/base.js'
2
+ import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
3
3
  import { type AvailableFeatures } from '@soederpop/luca/feature'
4
4
  import { Feature } from '../feature.js'
5
5
  import { Database } from 'bun:sqlite'
@@ -34,6 +34,16 @@ export const SemanticSearchStateSchema = FeatureStateSchema.extend({
34
34
  export type SemanticSearchOptions = z.infer<typeof SemanticSearchOptionsSchema>
35
35
  export type SemanticSearchState = z.infer<typeof SemanticSearchStateSchema>
36
36
 
37
+ export const SemanticSearchEventsSchema = FeatureEventsSchema.extend({
38
+ modelLoaded: z.tuple([]).describe('When the local embedding model is loaded into memory'),
39
+ dbReady: z.tuple([]).describe('When the SQLite database is initialized and ready'),
40
+ indexed: z.tuple([z.object({
41
+ documents: z.number().describe('Number of documents indexed'),
42
+ chunks: z.number().describe('Number of chunks created'),
43
+ }).describe('Indexing result')]).describe('When documents are indexed with embeddings'),
44
+ modelDisposed: z.tuple([]).describe('When the local embedding model is disposed from memory'),
45
+ }).describe('Semantic Search events')
46
+
37
47
  // ── Types ───────────────────────────────────────────────────────────
38
48
 
39
49
  export interface Chunk {
@@ -273,6 +283,7 @@ function chunkByDocument(doc: DocumentInput): Chunk[] {
273
283
  export class SemanticSearch extends Feature<SemanticSearchState, SemanticSearchOptions> {
274
284
  static override stateSchema = SemanticSearchStateSchema
275
285
  static override optionsSchema = SemanticSearchOptionsSchema
286
+ static override eventsSchema = SemanticSearchEventsSchema
276
287
  static override shortcut = 'features.semanticSearch' as const
277
288
  static { Feature.register(this, 'semanticSearch') }
278
289