@soederpop/luca 0.0.30 → 0.0.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soederpop/luca",
3
- "version": "0.0.30",
3
+ "version": "0.0.32",
4
4
  "website": "https://luca.soederpop.com",
5
5
  "description": "lightweight universal conversational architecture AKA Le Ultimate Component Architecture AKA Last Universal Common Ancestor, part AI part Human",
6
6
  "author": "jon soeder aka the people's champ <jon@soederpop.com>",
@@ -29,6 +29,7 @@ export const AssistantEventsSchema = FeatureEventsSchema.extend({
29
29
  toolResult: z.tuple([z.string().describe('Tool name'), z.any().describe('Result value')]).describe('Emitted when a tool returns a result'),
30
30
  toolError: z.tuple([z.string().describe('Tool name'), z.any().describe('Error')]).describe('Emitted when a tool call fails'),
31
31
  hookFired: z.tuple([z.string().describe('Hook/event name')]).describe('Emitted when a hook function is called'),
32
+ reloaded: z.tuple([]).describe('Emitted after tools, hooks, and system prompt are reloaded from disk'),
32
33
  systemPromptExtensionsChanged: z.tuple([]).describe('Emitted when system prompt extensions are added or removed'),
33
34
  })
34
35
 
@@ -461,6 +462,8 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
461
462
  */
462
463
  addTool(name: string, handler: (...args: any[]) => any, schema?: z.ZodType): this {
463
464
  if (!name) throw new Error('addTool handler must be a named function')
465
+ if (!this._runtimeToolNames) this._runtimeToolNames = new Set()
466
+ this._runtimeToolNames.add(name)
464
467
 
465
468
  const current = { ...this.tools }
466
469
 
@@ -504,10 +507,12 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
504
507
 
505
508
  if (typeof nameOrHandler === 'string') {
506
509
  delete current[nameOrHandler]
510
+ this._runtimeToolNames?.delete(nameOrHandler)
507
511
  } else {
508
512
  for (const [name, tool] of Object.entries(current)) {
509
513
  if (tool.handler === nameOrHandler) {
510
514
  delete current[name]
515
+ this._runtimeToolNames?.delete(name)
511
516
  break
512
517
  }
513
518
  }
@@ -936,17 +941,69 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
936
941
  */
937
942
  /** Hook names that are called directly during lifecycle, not bound as event listeners. */
938
943
  private static lifecycleHooks = new Set(['formatSystemPrompt'])
944
+ /** Stored references to bound hook listeners so they can be unbound on reload. Lazily initialized because afterInitialize runs before field initializers. */
945
+ private _boundHookListeners!: Array<{ event: string; listener: (...args: any[]) => void }>
946
+ /** Tool names added at runtime via addTool()/use(), so reload() can preserve them. */
947
+ private _runtimeToolNames!: Set<string>
939
948
 
940
949
  private bindHooksToEvents() {
950
+ if (!this._boundHookListeners) this._boundHookListeners = []
941
951
  const assistant = this
942
952
  const hooks = (this.state.get('hooks') || {}) as Record<string, (...args: any[]) => any>
943
953
  for (const [eventName, hookFn] of Object.entries(hooks)) {
944
954
  if (Assistant.lifecycleHooks.has(eventName)) continue
945
- this.on(eventName as any, (...args: any[]) => {
955
+ const listener = (...args: any[]) => {
946
956
  this.emit('hookFired', eventName)
947
957
  hookFn(assistant, ...args)
948
- })
958
+ }
959
+ this._boundHookListeners.push({ event: eventName, listener })
960
+ this.on(eventName as any, listener)
961
+ }
962
+ }
963
+
964
+ private unbindHooksFromEvents() {
965
+ if (!this._boundHookListeners) return
966
+ for (const { event, listener } of this._boundHookListeners) {
967
+ this.off(event as any, listener)
949
968
  }
969
+ this._boundHookListeners = []
970
+ }
971
+
972
+ /**
973
+ * Reload tools, hooks, and system prompt from disk. Useful during development
974
+ * or when tool/hook files have been modified and you want the assistant to
975
+ * pick up changes without restarting.
976
+ *
977
+ * @returns this, for chaining
978
+ */
979
+ reload(): this {
980
+ // Unbind existing hook listeners
981
+ this.unbindHooksFromEvents()
982
+
983
+ // Snapshot runtime-added tools before reloading from disk
984
+ const runtimeTools: Record<string, ConversationTool> = {}
985
+ if (this._runtimeToolNames?.size) {
986
+ const current = this.tools
987
+ for (const name of this._runtimeToolNames) {
988
+ if (current[name]) runtimeTools[name] = current[name]
989
+ }
990
+ }
991
+
992
+ // Reload system prompt from disk
993
+ this.state.set('systemPrompt', this.loadSystemPrompt())
994
+
995
+ // Reload tools from disk (merges with option tools), then restore runtime tools
996
+ const diskTools = this.loadTools()
997
+ this.state.set('tools', { ...diskTools, ...runtimeTools })
998
+ this.emit('toolsChanged')
999
+
1000
+ // Reload hooks from disk and rebind
1001
+ this.state.set('hooks', this.loadHooks())
1002
+ this.bindHooksToEvents()
1003
+
1004
+ this.emit('reloaded')
1005
+
1006
+ return this
950
1007
  }
951
1008
 
952
1009
  /**
@@ -1,5 +1,5 @@
1
1
  // Auto-generated bootstrap content
2
- // Generated at: 2026-03-24T06:38:37.146Z
2
+ // Generated at: 2026-03-24T09:09:11.574Z
3
3
  // Source: docs/bootstrap/*.md, docs/bootstrap/templates/*, docs/examples/*.md, docs/tutorials/*.md
4
4
  //
5
5
  // Do not edit manually. Run: luca build-bootstrap
@@ -1,4 +1,4 @@
1
1
  // Generated at compile time — do not edit manually
2
- export const BUILD_SHA = 'd0653ac'
2
+ export const BUILD_SHA = 'ff23ed7'
3
3
  export const BUILD_BRANCH = 'main'
4
- export const BUILD_DATE = '2026-03-24T06:38:37Z'
4
+ export const BUILD_DATE = '2026-03-24T09:08:07Z'
package/src/command.ts CHANGED
@@ -109,7 +109,26 @@ export class Command<
109
109
  const named = this._normalizeInput(raw, dispatchSource)
110
110
 
111
111
  // Validate against argsSchema
112
- const parsed = Cls.argsSchema.parse(named)
112
+ let parsed: any
113
+ try {
114
+ parsed = Cls.argsSchema.parse(named)
115
+ } catch (err: any) {
116
+ if (err?.name === 'ZodError' && dispatchSource === 'cli') {
117
+ const ui = (this.container as any).feature('ui')
118
+ const cmdName = Cls.shortcut?.replace('commands.', '') || 'unknown'
119
+ const issues = err.issues || []
120
+
121
+ ui.print.red(`\n Error: Invalid options for "${cmdName}"\n`)
122
+ for (const issue of issues) {
123
+ const path = issue.path?.length ? issue.path.join('.') : 'input'
124
+ ui.print(` ${ui.colors.yellow('→')} ${ui.colors.bold(path)}: ${issue.message}`)
125
+ }
126
+ ui.print('')
127
+ ui.print.dim(` Run ${ui.colors.cyan(`luca ${cmdName} --help`)} for usage info.\n`)
128
+ process.exit(1)
129
+ }
130
+ throw err
131
+ }
113
132
 
114
133
  // For headless dispatch, capture stdout/stderr
115
134
  if (dispatchSource !== 'cli') {
@@ -19,6 +19,7 @@ export const argsSchema = CommandOptionsSchema.extend({
19
19
  force: z.boolean().default(false).describe('Kill any process currently using the target port'),
20
20
  anyPort: z.boolean().default(false).describe('Find an available port starting above 3000'),
21
21
  open: z.boolean().default(true).describe('Open the server URL in Google Chrome'),
22
+ watch: z.boolean().default(false).describe('Watch endpoint files and hot-reload handlers on change'),
22
23
  })
23
24
 
24
25
  export default async function serve(options: z.infer<typeof argsSchema>, context: ContainerContext) {
@@ -157,6 +158,32 @@ export default async function serve(options: z.infer<typeof argsSchema>, context
157
158
  }
158
159
  }
159
160
 
161
+ if (options.watch && endpointsDir) {
162
+ const fm = container.feature('fileManager')
163
+ await fm.watch({ paths: [endpointsDir] })
164
+
165
+ fm.on('file:change', async ({ type, path: filePath }: { type: string; path: string }) => {
166
+ if (!filePath.endsWith('.ts')) return
167
+
168
+ if (type === 'change') {
169
+ try {
170
+ const ep = await expressServer.reloadEndpoint(filePath)
171
+ if (ep) {
172
+ console.log(`[watch] Reloaded ${ep.methods.map((m: string) => m.toUpperCase()).join(',')} ${ep.path}`)
173
+ }
174
+ } catch (err: any) {
175
+ console.error(`[watch] Failed to reload ${filePath}: ${err.message}`)
176
+ }
177
+ } else if (type === 'add') {
178
+ console.log(`[watch] New file detected: ${filePath} (restart to mount)`)
179
+ } else if (type === 'delete') {
180
+ console.log(`[watch] File deleted: ${filePath} (restart to unmount)`)
181
+ }
182
+ })
183
+
184
+ console.log(`\n[watch] Watching ${endpointsDir} for changes`)
185
+ }
186
+
160
187
  console.log()
161
188
  }
162
189
 
package/src/endpoint.ts CHANGED
@@ -197,6 +197,12 @@ export class Endpoint<
197
197
 
198
198
  async reload(): Promise<this> {
199
199
  this._module = null
200
+ if (this.options.filePath) {
201
+ const helpers = this.container.feature('helpers') as any
202
+ const mod = await helpers.loadModuleExports(this.options.filePath, { cacheBust: true })
203
+ const endpointModule: EndpointModule = mod.default || mod
204
+ return this.load(endpointModule)
205
+ }
200
206
  return this.load()
201
207
  }
202
208
 
package/src/helper.ts CHANGED
@@ -704,16 +704,42 @@ function presentIntrospectionAsTypeScript(introspection: HelperIntrospection, se
704
704
  }
705
705
  }
706
706
 
707
- // State
707
+ // State — render as the observable State<T> object, not just currentState
708
708
  if (shouldRender('state') && introspection.state && Object.keys(introspection.state).length > 0) {
709
709
  if (members.length > 0) members.push('')
710
710
  const stateMembers = Object.entries(introspection.state)
711
711
  .map(([name, info]) => {
712
- const comment = info.description ? ` /** ${info.description} */\n` : ''
713
- return `${comment} ${name}: ${normalizeTypeString(info.type || 'any')};`
712
+ const comment = info.description ? ` /** ${info.description} */\n` : ''
713
+ return `${comment} ${name}: ${normalizeTypeString(info.type || 'any')};`
714
714
  })
715
715
  .join('\n')
716
- members.push(` state: {\n${stateMembers}\n };`)
716
+ const stateShape = `{\n${stateMembers}\n }`
717
+ members.push(` state: {`)
718
+ members.push(` /** The current version number, incremented on each change */`)
719
+ members.push(` readonly version: number;`)
720
+ members.push(` /** Get the value of a state key */`)
721
+ members.push(` get<K extends keyof T>(key: K): T[K] | undefined;`)
722
+ members.push(` /** Set a state key to a new value, notifying observers */`)
723
+ members.push(` set<K extends keyof T>(key: K, value: T[K]): this;`)
724
+ members.push(` /** Delete a state key, notifying observers */`)
725
+ members.push(` delete<K extends keyof T>(key: K): this;`)
726
+ members.push(` /** Check if a state key exists */`)
727
+ members.push(` has<K extends keyof T>(key: K): boolean;`)
728
+ members.push(` /** Get all state keys */`)
729
+ members.push(` keys(): string[];`)
730
+ members.push(` /** Get the current state snapshot */`)
731
+ members.push(` readonly current: ${stateShape};`)
732
+ members.push(` /** Get all entries as [key, value] pairs */`)
733
+ members.push(` entries(): [string, any][];`)
734
+ members.push(` /** Get all state values */`)
735
+ members.push(` values(): any[];`)
736
+ members.push(` /** Register an observer callback for state changes. Returns an unsubscribe function. */`)
737
+ members.push(` observe(callback: (changeType: 'add' | 'update' | 'delete', key: string, value?: any) => void): () => void;`)
738
+ members.push(` /** Merge partial state, notifying observers for each changed key */`)
739
+ members.push(` setState(value: Partial<${stateShape}> | ((current: ${stateShape}, state: this) => Partial<${stateShape}>)): void;`)
740
+ members.push(` /** Clear all state, notifying observers */`)
741
+ members.push(` clear(): void;`)
742
+ members.push(` };`)
717
743
  }
718
744
 
719
745
  // Options
@@ -804,5 +830,16 @@ function isGenericObjectType(type: string): boolean {
804
830
  function normalizeTypeString(type: string): string {
805
831
  if (!type) return 'any'
806
832
  // The AST scanner sometimes wraps types in quotes
807
- return type.replace(/^["']|["']$/g, '')
833
+ type = type.replace(/^["']|["']$/g, '')
834
+ // Convert internal ReturnType<typeof this.container.feature<'name'>> to a clean import reference
835
+ // e.g. ReturnType<typeof this.container.feature<'proc'>> → import('luca').Proc
836
+ type = type.replace(
837
+ /ReturnType<typeof this\.container\.(feature|client|server)<'([^']+)'>>/g,
838
+ (_match, _kind, name) => {
839
+ // Convert shortcut name to PascalCase class name
840
+ const className = name.replace(/(^|[-_])(\w)/g, (_: string, _sep: string, ch: string) => ch.toUpperCase())
841
+ return `import('@soederpop/luca').${className}`
842
+ }
843
+ )
844
+ return type
808
845
  }
@@ -1,7 +1,7 @@
1
1
  import { setBuildTimeData, setContainerBuildTimeData } from './index.js';
2
2
 
3
3
  // Auto-generated introspection registry data
4
- // Generated at: 2026-03-24T06:38:35.595Z
4
+ // Generated at: 2026-03-24T09:09:09.989Z
5
5
 
6
6
  setBuildTimeData('features.googleDocs', {
7
7
  "id": "features.googleDocs",
@@ -10426,6 +10426,16 @@ setBuildTimeData('features.helpers', {
10426
10426
  "absPath": {
10427
10427
  "type": "string",
10428
10428
  "description": "Absolute path to the module file"
10429
+ },
10430
+ "options": {
10431
+ "type": "{ cacheBust?: boolean }",
10432
+ "description": "Optional settings",
10433
+ "properties": {
10434
+ "cacheBust": {
10435
+ "type": "any",
10436
+ "description": "When true, appends a timestamp query to bypass the native import cache (useful for hot reload)"
10437
+ }
10438
+ }
10429
10439
  }
10430
10440
  },
10431
10441
  "required": [
@@ -12749,6 +12759,19 @@ setBuildTimeData('servers.express', {
12749
12759
  ],
12750
12760
  "returns": "Promise<this>"
12751
12761
  },
12762
+ "reloadEndpoint": {
12763
+ "description": "Reload a mounted endpoint by its file path. Re-reads the module through the helpers VM loader so the next request picks up the new handlers.",
12764
+ "parameters": {
12765
+ "filePath": {
12766
+ "type": "string",
12767
+ "description": "Absolute path to the endpoint file"
12768
+ }
12769
+ },
12770
+ "required": [
12771
+ "filePath"
12772
+ ],
12773
+ "returns": "Promise<Endpoint | null>"
12774
+ },
12752
12775
  "useEndpointModules": {
12753
12776
  "description": "",
12754
12777
  "parameters": {
@@ -14941,6 +14964,12 @@ setBuildTimeData('features.assistant', {
14941
14964
  "required": [],
14942
14965
  "returns": "Promise<number>"
14943
14966
  },
14967
+ "reload": {
14968
+ "description": "Reload tools, hooks, and system prompt from disk. Useful during development or when tool/hook files have been modified and you want the assistant to pick up changes without restarting.",
14969
+ "parameters": {},
14970
+ "required": [],
14971
+ "returns": "this"
14972
+ },
14944
14973
  "start": {
14945
14974
  "description": "Start the assistant by creating the conversation and wiring up events. The system prompt, tools, and hooks are already loaded synchronously during initialization.",
14946
14975
  "parameters": {},
@@ -15128,6 +15157,11 @@ setBuildTimeData('features.assistant', {
15128
15157
  "description": "Event emitted by Assistant",
15129
15158
  "arguments": {}
15130
15159
  },
15160
+ "reloaded": {
15161
+ "name": "reloaded",
15162
+ "description": "Event emitted by Assistant",
15163
+ "arguments": {}
15164
+ },
15131
15165
  "turnStart": {
15132
15166
  "name": "turnStart",
15133
15167
  "description": "Event emitted by Assistant",
@@ -27880,6 +27914,16 @@ export const introspectionData = [
27880
27914
  "absPath": {
27881
27915
  "type": "string",
27882
27916
  "description": "Absolute path to the module file"
27917
+ },
27918
+ "options": {
27919
+ "type": "{ cacheBust?: boolean }",
27920
+ "description": "Optional settings",
27921
+ "properties": {
27922
+ "cacheBust": {
27923
+ "type": "any",
27924
+ "description": "When true, appends a timestamp query to bypass the native import cache (useful for hot reload)"
27925
+ }
27926
+ }
27883
27927
  }
27884
27928
  },
27885
27929
  "required": [
@@ -30192,6 +30236,19 @@ export const introspectionData = [
30192
30236
  ],
30193
30237
  "returns": "Promise<this>"
30194
30238
  },
30239
+ "reloadEndpoint": {
30240
+ "description": "Reload a mounted endpoint by its file path. Re-reads the module through the helpers VM loader so the next request picks up the new handlers.",
30241
+ "parameters": {
30242
+ "filePath": {
30243
+ "type": "string",
30244
+ "description": "Absolute path to the endpoint file"
30245
+ }
30246
+ },
30247
+ "required": [
30248
+ "filePath"
30249
+ ],
30250
+ "returns": "Promise<Endpoint | null>"
30251
+ },
30195
30252
  "useEndpointModules": {
30196
30253
  "description": "",
30197
30254
  "parameters": {
@@ -32376,6 +32433,12 @@ export const introspectionData = [
32376
32433
  "required": [],
32377
32434
  "returns": "Promise<number>"
32378
32435
  },
32436
+ "reload": {
32437
+ "description": "Reload tools, hooks, and system prompt from disk. Useful during development or when tool/hook files have been modified and you want the assistant to pick up changes without restarting.",
32438
+ "parameters": {},
32439
+ "required": [],
32440
+ "returns": "this"
32441
+ },
32379
32442
  "start": {
32380
32443
  "description": "Start the assistant by creating the conversation and wiring up events. The system prompt, tools, and hooks are already loaded synchronously during initialization.",
32381
32444
  "parameters": {},
@@ -32563,6 +32626,11 @@ export const introspectionData = [
32563
32626
  "description": "Event emitted by Assistant",
32564
32627
  "arguments": {}
32565
32628
  },
32629
+ "reloaded": {
32630
+ "name": "reloaded",
32631
+ "description": "Event emitted by Assistant",
32632
+ "arguments": {}
32633
+ },
32566
32634
  "turnStart": {
32567
32635
  "name": "turnStart",
32568
32636
  "description": "Event emitted by Assistant",
@@ -1,7 +1,7 @@
1
1
  import { setBuildTimeData, setContainerBuildTimeData } from './index.js';
2
2
 
3
3
  // Auto-generated introspection registry data
4
- // Generated at: 2026-03-24T06:38:35.496Z
4
+ // Generated at: 2026-03-24T09:09:09.885Z
5
5
 
6
6
  setBuildTimeData('features.googleDocs', {
7
7
  "id": "features.googleDocs",
@@ -10426,6 +10426,16 @@ setBuildTimeData('features.helpers', {
10426
10426
  "absPath": {
10427
10427
  "type": "string",
10428
10428
  "description": "Absolute path to the module file"
10429
+ },
10430
+ "options": {
10431
+ "type": "{ cacheBust?: boolean }",
10432
+ "description": "Optional settings",
10433
+ "properties": {
10434
+ "cacheBust": {
10435
+ "type": "any",
10436
+ "description": "When true, appends a timestamp query to bypass the native import cache (useful for hot reload)"
10437
+ }
10438
+ }
10429
10439
  }
10430
10440
  },
10431
10441
  "required": [
@@ -12749,6 +12759,19 @@ setBuildTimeData('servers.express', {
12749
12759
  ],
12750
12760
  "returns": "Promise<this>"
12751
12761
  },
12762
+ "reloadEndpoint": {
12763
+ "description": "Reload a mounted endpoint by its file path. Re-reads the module through the helpers VM loader so the next request picks up the new handlers.",
12764
+ "parameters": {
12765
+ "filePath": {
12766
+ "type": "string",
12767
+ "description": "Absolute path to the endpoint file"
12768
+ }
12769
+ },
12770
+ "required": [
12771
+ "filePath"
12772
+ ],
12773
+ "returns": "Promise<Endpoint | null>"
12774
+ },
12752
12775
  "useEndpointModules": {
12753
12776
  "description": "",
12754
12777
  "parameters": {
@@ -23768,6 +23791,16 @@ export const introspectionData = [
23768
23791
  "absPath": {
23769
23792
  "type": "string",
23770
23793
  "description": "Absolute path to the module file"
23794
+ },
23795
+ "options": {
23796
+ "type": "{ cacheBust?: boolean }",
23797
+ "description": "Optional settings",
23798
+ "properties": {
23799
+ "cacheBust": {
23800
+ "type": "any",
23801
+ "description": "When true, appends a timestamp query to bypass the native import cache (useful for hot reload)"
23802
+ }
23803
+ }
23771
23804
  }
23772
23805
  },
23773
23806
  "required": [
@@ -26080,6 +26113,19 @@ export const introspectionData = [
26080
26113
  ],
26081
26114
  "returns": "Promise<this>"
26082
26115
  },
26116
+ "reloadEndpoint": {
26117
+ "description": "Reload a mounted endpoint by its file path. Re-reads the module through the helpers VM loader so the next request picks up the new handlers.",
26118
+ "parameters": {
26119
+ "filePath": {
26120
+ "type": "string",
26121
+ "description": "Absolute path to the endpoint file"
26122
+ }
26123
+ },
26124
+ "required": [
26125
+ "filePath"
26126
+ ],
26127
+ "returns": "Promise<Endpoint | null>"
26128
+ },
26083
26129
  "useEndpointModules": {
26084
26130
  "description": "",
26085
26131
  "parameters": {
@@ -1,7 +1,7 @@
1
1
  import { setBuildTimeData, setContainerBuildTimeData } from './index.js';
2
2
 
3
3
  // Auto-generated introspection registry data
4
- // Generated at: 2026-03-24T06:38:35.508Z
4
+ // Generated at: 2026-03-24T09:09:09.897Z
5
5
 
6
6
  setBuildTimeData('features.containerLink', {
7
7
  "id": "features.containerLink",
@@ -408,11 +408,14 @@ export class Helpers extends Feature<HelpersState, HelpersOptions> {
408
408
  * Uses the same `useNativeImport` check as discovery to decide the loading strategy.
409
409
  *
410
410
  * @param absPath - Absolute path to the module file
411
+ * @param options - Optional settings
412
+ * @param options.cacheBust - When true, appends a timestamp query to bypass the native import cache (useful for hot reload)
411
413
  * @returns The module's exports
412
414
  */
413
- async loadModuleExports(absPath: string): Promise<Record<string, any>> {
415
+ async loadModuleExports(absPath: string, options?: { cacheBust?: boolean }): Promise<Record<string, any>> {
414
416
  if (this.useNativeImport) {
415
- const mod = await import(absPath)
417
+ const importPath = options?.cacheBust ? `${absPath}?t=${Date.now()}` : absPath
418
+ const mod = await import(importPath)
416
419
  return mod
417
420
  }
418
421
 
@@ -1,5 +1,5 @@
1
1
  // Auto-generated scaffold and MCP readme content
2
- // Generated at: 2026-03-24T06:38:36.374Z
2
+ // Generated at: 2026-03-24T09:09:10.786Z
3
3
  // Source: docs/scaffolds/*.md, docs/examples/assistant/, and docs/mcp/readme.md
4
4
  //
5
5
  // Do not edit manually. Run: luca build-scaffolds
@@ -211,6 +211,24 @@ export class ExpressServer<T extends ServerState = ServerState, K extends Expres
211
211
  return this
212
212
  }
213
213
 
214
+ /**
215
+ * Reload a mounted endpoint by its file path. Re-reads the module through
216
+ * the helpers VM loader so the next request picks up the new handlers.
217
+ *
218
+ * @param filePath - Absolute path to the endpoint file
219
+ * @returns The reloaded Endpoint, or null if no mounted endpoint matches
220
+ */
221
+ async reloadEndpoint(filePath: string): Promise<Endpoint | null> {
222
+ const endpoint = this._mountedEndpoints.find(ep => (ep.options as any).filePath === filePath)
223
+ if (!endpoint) return null
224
+
225
+ const helpers = this.container.feature('helpers') as any
226
+ const mod = await helpers.loadModuleExports(filePath, { cacheBust: true })
227
+ const endpointModule: EndpointModule = mod.default || mod
228
+ await endpoint.load(endpointModule)
229
+ return endpoint
230
+ }
231
+
214
232
  async useEndpointModules(modules: EndpointModule[]): Promise<this> {
215
233
  for (const mod of modules) {
216
234
  try {