@soederpop/luca 0.0.32 → 0.0.35

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 (92) hide show
  1. package/README.md +241 -36
  2. package/bun.lock +24 -6
  3. package/commands/build-python-bridge.ts +43 -0
  4. package/docs/README.md +1 -1
  5. package/docs/TABLE-OF-CONTENTS.md +0 -1
  6. package/docs/apis/clients/rest.md +7 -7
  7. package/docs/apis/clients/websocket.md +23 -10
  8. package/docs/apis/features/agi/assistant.md +155 -8
  9. package/docs/apis/features/agi/assistants-manager.md +90 -22
  10. package/docs/apis/features/agi/auto-assistant.md +377 -0
  11. package/docs/apis/features/agi/browser-use.md +802 -0
  12. package/docs/apis/features/agi/claude-code.md +6 -1
  13. package/docs/apis/features/agi/conversation-history.md +7 -6
  14. package/docs/apis/features/agi/conversation.md +111 -38
  15. package/docs/apis/features/agi/docs-reader.md +35 -57
  16. package/docs/apis/features/agi/file-tools.md +163 -0
  17. package/docs/apis/features/agi/openapi.md +2 -2
  18. package/docs/apis/features/agi/skills-library.md +227 -0
  19. package/docs/apis/features/node/content-db.md +125 -4
  20. package/docs/apis/features/node/disk-cache.md +11 -11
  21. package/docs/apis/features/node/downloader.md +1 -1
  22. package/docs/apis/features/node/file-manager.md +15 -15
  23. package/docs/apis/features/node/fs.md +78 -21
  24. package/docs/apis/features/node/git.md +50 -10
  25. package/docs/apis/features/node/google-calendar.md +3 -0
  26. package/docs/apis/features/node/google-docs.md +10 -1
  27. package/docs/apis/features/node/google-drive.md +3 -0
  28. package/docs/apis/features/node/google-mail.md +214 -0
  29. package/docs/apis/features/node/google-sheets.md +3 -0
  30. package/docs/apis/features/node/ink.md +10 -10
  31. package/docs/apis/features/node/ipc-socket.md +83 -93
  32. package/docs/apis/features/node/networking.md +5 -5
  33. package/docs/apis/features/node/os.md +7 -7
  34. package/docs/apis/features/node/package-finder.md +14 -14
  35. package/docs/apis/features/node/proc.md +2 -1
  36. package/docs/apis/features/node/process-manager.md +70 -3
  37. package/docs/apis/features/node/python.md +265 -9
  38. package/docs/apis/features/node/redis.md +380 -0
  39. package/docs/apis/features/node/ui.md +13 -13
  40. package/docs/apis/servers/express.md +35 -7
  41. package/docs/apis/servers/mcp.md +3 -3
  42. package/docs/apis/servers/websocket.md +51 -8
  43. package/docs/bootstrap/CLAUDE.md +1 -1
  44. package/docs/bootstrap/SKILL.md +93 -7
  45. package/docs/examples/feature-as-tool-provider.md +143 -0
  46. package/docs/examples/python.md +42 -1
  47. package/docs/introspection.md +15 -5
  48. package/docs/tutorials/00-bootstrap.md +3 -3
  49. package/docs/tutorials/02-container.md +2 -2
  50. package/docs/tutorials/10-creating-features.md +5 -0
  51. package/docs/tutorials/13-introspection.md +12 -2
  52. package/docs/tutorials/19-python-sessions.md +401 -0
  53. package/package.json +8 -5
  54. package/scripts/examples/using-assistant-with-mcp.ts +2 -7
  55. package/scripts/test-linux-binary.sh +80 -0
  56. package/src/agi/container.server.ts +8 -0
  57. package/src/agi/features/assistant.ts +18 -0
  58. package/src/agi/features/autonomous-assistant.ts +435 -0
  59. package/src/agi/features/conversation.ts +58 -6
  60. package/src/agi/features/file-tools.ts +286 -0
  61. package/src/agi/features/luca-coder.ts +643 -0
  62. package/src/bootstrap/generated.ts +705 -107
  63. package/src/cli/build-info.ts +2 -2
  64. package/src/cli/cli.ts +22 -13
  65. package/src/commands/bootstrap.ts +49 -6
  66. package/src/commands/code.ts +369 -0
  67. package/src/commands/describe.ts +7 -2
  68. package/src/commands/index.ts +1 -0
  69. package/src/commands/sandbox-mcp.ts +7 -7
  70. package/src/commands/save-api-docs.ts +1 -1
  71. package/src/container-describer.ts +4 -4
  72. package/src/container.ts +10 -19
  73. package/src/helper.ts +24 -33
  74. package/src/introspection/generated.agi.ts +3026 -849
  75. package/src/introspection/generated.node.ts +1690 -1012
  76. package/src/introspection/generated.web.ts +15 -57
  77. package/src/node/container.ts +5 -5
  78. package/src/node/features/figlet-fonts.ts +597 -0
  79. package/src/node/features/fs.ts +3 -9
  80. package/src/node/features/helpers.ts +20 -0
  81. package/src/node/features/python.ts +429 -16
  82. package/src/node/features/redis.ts +446 -0
  83. package/src/node/features/ui.ts +4 -11
  84. package/src/python/bridge.py +220 -0
  85. package/src/python/generated.ts +227 -0
  86. package/src/scaffolds/generated.ts +1 -1
  87. package/test/python-session.test.ts +105 -0
  88. package/assistants/lucaExpert/CORE.md +0 -37
  89. package/assistants/lucaExpert/hooks.ts +0 -9
  90. package/assistants/lucaExpert/tools.ts +0 -177
  91. package/docs/examples/port-exposer.md +0 -89
  92. package/src/node/features/port-exposer.ts +0 -351
@@ -3,7 +3,12 @@ import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '.
3
3
  import { Feature } from '../feature.js'
4
4
  import { Feature as UniversalFeature } from '../../feature.js'
5
5
  import { Client, clients } from '../../client.js'
6
+ import { RestClient } from '../../clients/rest.js'
7
+ import { GraphClient } from '../../clients/graph.js'
8
+ import { WebSocketClient } from '../../clients/websocket.js'
6
9
  import { Server, servers } from '../../server.js'
10
+ import { ExpressServer } from '../../servers/express.js'
11
+ import { WebsocketServer } from '../../servers/socket.js'
7
12
  import { Command, commands } from '../../command.js'
8
13
  import { graftModule, isNativeHelperClass } from '../../graft.js'
9
14
  import { endpoints } from '../../endpoint.js'
@@ -176,6 +181,11 @@ export class Helpers extends Feature<HelpersState, HelpersOptions> {
176
181
 
177
182
  // Helper subclasses
178
183
  Selector,
184
+ RestClient,
185
+ GraphClient,
186
+ WebSocketClient,
187
+ ExpressServer,
188
+ WebsocketServer,
179
189
 
180
190
  // The singleton container
181
191
  default: this.container,
@@ -210,6 +220,16 @@ export class Helpers extends Feature<HelpersState, HelpersOptions> {
210
220
  vm.defineModule('@soederpop/luca', lucaExports)
211
221
  vm.defineModule('@soederpop/luca/schemas', schemasModule)
212
222
  vm.defineModule('@soederpop/luca/node', lucaExports)
223
+
224
+ // Deep import paths AIs and developers might reach for
225
+ vm.defineModule('@soederpop/luca/client', { Client, ClientsRegistry: clients.constructor, default: Client })
226
+ vm.defineModule('@soederpop/luca/server', { Server, ServersRegistry: servers.constructor, default: Server })
227
+ vm.defineModule('@soederpop/luca/clients/rest', { RestClient, default: RestClient })
228
+ vm.defineModule('@soederpop/luca/clients/graph', { GraphClient, default: GraphClient })
229
+ vm.defineModule('@soederpop/luca/clients/websocket', { WebSocketClient, default: WebSocketClient })
230
+ vm.defineModule('@soederpop/luca/servers/express', { ExpressServer, default: ExpressServer })
231
+ vm.defineModule('@soederpop/luca/servers/socket', { WebsocketServer, default: WebsocketServer })
232
+
213
233
  vm.defineModule('zod', { z, default: { z } })
214
234
  }
215
235
 
@@ -3,6 +3,9 @@ import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '.
3
3
  import { Feature } from "../feature.js";
4
4
  import { existsSync } from 'fs';
5
5
  import { join, resolve } from 'path';
6
+ import { tmpdir } from 'os';
7
+ import { bridgeScript } from '../../python/generated.js';
8
+ import type { ChildProcess } from 'child_process';
6
9
 
7
10
  export const PythonStateSchema = FeatureStateSchema.extend({
8
11
  /** Path to the detected Python executable */
@@ -15,6 +18,10 @@ export const PythonStateSchema = FeatureStateSchema.extend({
15
18
  isReady: z.boolean().default(false).describe('Whether the Python environment is ready for execution'),
16
19
  /** Path to the last executed Python script */
17
20
  lastExecutedScript: z.string().nullable().default(null).describe('Path to the last executed Python script'),
21
+ /** Whether a persistent Python session is currently active */
22
+ sessionActive: z.boolean().default(false).describe('Whether a persistent Python session is currently active'),
23
+ /** Unique ID of the current persistent session */
24
+ sessionId: z.string().nullable().default(null).describe('Unique ID of the current persistent session'),
18
25
  })
19
26
 
20
27
  export const PythonOptionsSchema = FeatureOptionsSchema.extend({
@@ -69,32 +76,56 @@ export const PythonEventsSchema = FeatureEventsSchema.extend({
69
76
  }).describe('Execution result'),
70
77
  }).describe('File execution details')]).describe('When a Python file finishes executing'),
71
78
  localsParseError: z.tuple([z.any().describe('The parse error')]).describe('When captured locals fail to parse as JSON'),
79
+ sessionStarted: z.tuple([z.object({
80
+ sessionId: z.string().describe('Unique session identifier'),
81
+ }).describe('Session start details')]).describe('When a persistent Python session starts'),
82
+ sessionStopped: z.tuple([z.object({
83
+ sessionId: z.string().describe('Session identifier that stopped'),
84
+ }).describe('Session stop details')]).describe('When a persistent Python session stops'),
85
+ sessionError: z.tuple([z.object({
86
+ error: z.string().describe('Error message'),
87
+ sessionId: z.string().nullable().describe('Session identifier, if available'),
88
+ }).describe('Session error details')]).describe('When a session-level error occurs'),
72
89
  }).describe('Python events')
73
90
 
91
+ /** Result from a persistent session run() call. */
92
+ export interface RunResult {
93
+ ok: boolean
94
+ result: any
95
+ stdout: string
96
+ error?: string
97
+ traceback?: string
98
+ }
99
+
74
100
  /**
75
101
  * The Python VM feature provides Python virtual machine capabilities for executing Python code.
76
- *
102
+ *
77
103
  * This feature automatically detects Python environments (uv, conda, venv, system) and provides
78
104
  * methods to install dependencies and execute Python scripts. It can manage project-specific
79
105
  * Python environments and maintain context between executions.
80
- *
106
+ *
107
+ * Supports two modes:
108
+ * - **Stateless** (default): `execute()` and `executeFile()` spawn a fresh process per call
109
+ * - **Persistent session**: `startSession()` spawns a long-lived bridge process that maintains
110
+ * state across `run()` calls, enabling real codebase interaction with imports and session variables
111
+ *
81
112
  * @example
82
113
  * ```typescript
83
- * const python = container.feature('python', {
114
+ * const python = container.feature('python', {
84
115
  * dir: "/path/to/python/project",
85
- * contextScript: "/path/to/setup-context.py"
86
116
  * })
87
- *
88
- * // Auto-install dependencies
89
- * await python.installDependencies()
90
- *
91
- * // Execute Python code
117
+ *
118
+ * // Stateless execution
92
119
  * const result = await python.execute('print("Hello from Python!")')
93
- *
94
- * // Execute with custom variables
95
- * const result2 = await python.execute('print(f"Hello {name}!")', { name: 'World' })
120
+ *
121
+ * // Persistent session
122
+ * await python.startSession()
123
+ * await python.run('import myapp.models')
124
+ * await python.run('users = myapp.models.User.objects.all()')
125
+ * const result = await python.run('print(len(users))')
126
+ * await python.stopSession()
96
127
  * ```
97
- *
128
+ *
98
129
  * @extends Feature
99
130
  */
100
131
  export class Python<
@@ -107,6 +138,11 @@ export class Python<
107
138
  static override eventsSchema = PythonEventsSchema
108
139
  static { Feature.register(this, 'python') }
109
140
 
141
+ private _bridgeProcess: ChildProcess | null = null
142
+ private _bridgeScriptPath: string | null = null
143
+ private _pendingRequests = new Map<string, { resolve: (v: any) => void, reject: (e: any) => void }>()
144
+ private _stdoutBuffer = ''
145
+
110
146
  override get initialState(): T {
111
147
  return {
112
148
  ...super.initialState,
@@ -114,7 +150,9 @@ export class Python<
114
150
  projectDir: null,
115
151
  environmentType: null,
116
152
  isReady: false,
117
- lastExecutedScript: null
153
+ lastExecutedScript: null,
154
+ sessionActive: false,
155
+ sessionId: null,
118
156
  } as T
119
157
  }
120
158
 
@@ -365,8 +403,8 @@ export class Python<
365
403
 
366
404
  const { projectDir, pythonPath } = this
367
405
 
368
- // Create temporary script
369
- const tempDir = join(projectDir, '.luca-python-temp')
406
+ // Create temporary script in system temp dir (not inside the project)
407
+ const tempDir = `${tmpdir()}/luca-python-temp`
370
408
  await fs.ensureFolder(tempDir)
371
409
  const scriptPath = join(tempDir, `script-${Date.now()}.py`)
372
410
 
@@ -482,6 +520,381 @@ export class Python<
482
520
 
483
521
  return { version, path, packages }
484
522
  }
523
+
524
+ // ---------------------------------------------------------------------------
525
+ // Persistent session methods
526
+ // ---------------------------------------------------------------------------
527
+
528
+ /**
529
+ * Splits the (possibly multi-word) pythonPath into a command and args array
530
+ * suitable for proc.spawn(). For example, `uv run python` becomes
531
+ * `{ command: 'uv', args: ['run', 'python', ...extraArgs] }`.
532
+ */
533
+ private _parsePythonCommand(extraArgs: string[]): { command: string, args: string[] } {
534
+ const parts = this.pythonPath.split(/\s+/)
535
+ return { command: parts[0], args: [...parts.slice(1), ...extraArgs] }
536
+ }
537
+
538
+ /**
539
+ * Writes the bundled bridge.py to a temp directory and returns its path.
540
+ * Reuses the same path across calls within a process.
541
+ */
542
+ private async _ensureBridgeScript(): Promise<string> {
543
+ if (this._bridgeScriptPath) return this._bridgeScriptPath
544
+
545
+ const fs = this.container.feature('fs')
546
+ const bridgeDir = `${tmpdir()}/luca-python-bridge`
547
+ await fs.ensureFolder(bridgeDir)
548
+ const scriptPath = `${bridgeDir}/bridge.py`
549
+ await fs.writeFileAsync(scriptPath, bridgeScript)
550
+ this._bridgeScriptPath = scriptPath
551
+ return scriptPath
552
+ }
553
+
554
+ /**
555
+ * Sends a JSON-line request to the bridge process and returns a promise
556
+ * that resolves when the matching response (by id) arrives.
557
+ *
558
+ * @param type - The request type (exec, eval, import, call, get_locals, reset)
559
+ * @param payload - Additional fields to include in the request
560
+ * @param timeout - Timeout in ms (default 30000)
561
+ */
562
+ private _sendRequest(type: string, payload: Record<string, any> = {}, timeout = 30000): Promise<any> {
563
+ if (!this._bridgeProcess || !this._bridgeProcess.stdin) {
564
+ return Promise.reject(new Error('No active Python session. Call startSession() first.'))
565
+ }
566
+
567
+ const id = this.container.utils.uuid()
568
+ const request = JSON.stringify({ id, type, ...payload }) + '\n'
569
+
570
+ return new Promise((resolve, reject) => {
571
+ const timer = setTimeout(() => {
572
+ this._pendingRequests.delete(id)
573
+ reject(new Error(`Python bridge request timed out after ${timeout}ms (type: ${type})`))
574
+ }, timeout)
575
+
576
+ this._pendingRequests.set(id, {
577
+ resolve: (value: any) => {
578
+ clearTimeout(timer)
579
+ resolve(value)
580
+ },
581
+ reject: (err: any) => {
582
+ clearTimeout(timer)
583
+ reject(err)
584
+ },
585
+ })
586
+
587
+ this._bridgeProcess!.stdin!.write(request)
588
+ })
589
+ }
590
+
591
+ /**
592
+ * Handles incoming stdout data from the bridge process. Buffers partial
593
+ * lines and parses complete JSON-line responses, resolving their matching
594
+ * pending requests.
595
+ */
596
+ private _onBridgeData(chunk: Buffer | string): void {
597
+ this._stdoutBuffer += chunk.toString()
598
+
599
+ const lines = this._stdoutBuffer.split('\n')
600
+ // Keep the last (possibly incomplete) segment in the buffer
601
+ this._stdoutBuffer = lines.pop() || ''
602
+
603
+ for (const line of lines) {
604
+ if (!line.trim()) continue
605
+ try {
606
+ const response = JSON.parse(line)
607
+ const id = response.id
608
+ if (id && this._pendingRequests.has(id)) {
609
+ const pending = this._pendingRequests.get(id)!
610
+ this._pendingRequests.delete(id)
611
+ pending.resolve(response)
612
+ }
613
+ // Non-id responses (like the initial "ready") are handled by startSession
614
+ } catch {
615
+ // Not JSON — could be stray output, ignore
616
+ }
617
+ }
618
+ }
619
+
620
+ /**
621
+ * Starts a persistent Python session by spawning the bridge process.
622
+ *
623
+ * The bridge sets up sys.path for the project directory, then enters a
624
+ * JSON-line REPL loop. State (variables, imports) persists across run() calls
625
+ * until stopSession() or resetSession() is called.
626
+ *
627
+ * @example
628
+ * ```typescript
629
+ * const python = container.feature('python', { dir: '/path/to/project' })
630
+ * await python.enable()
631
+ * await python.startSession()
632
+ * await python.run('x = 42')
633
+ * const result = await python.run('print(x)')
634
+ * console.log(result.stdout) // '42\n'
635
+ * await python.stopSession()
636
+ * ```
637
+ */
638
+ async startSession(): Promise<void> {
639
+ if (this.state.get('sessionActive')) {
640
+ throw new Error('A Python session is already active. Call stopSession() first.')
641
+ }
642
+
643
+ const proc = this.container.feature('proc')
644
+ const bridgePath = await this._ensureBridgeScript()
645
+ const { command, args } = this._parsePythonCommand(['-u', bridgePath])
646
+
647
+ const child = proc.spawn(command, args, {
648
+ cwd: this.projectDir,
649
+ stdout: 'pipe',
650
+ stderr: 'pipe',
651
+ })
652
+
653
+ this._bridgeProcess = child
654
+ this._stdoutBuffer = ''
655
+
656
+ // Wait for the ready signal from the bridge
657
+ const readyPromise = new Promise<void>((resolve, reject) => {
658
+ const timer = setTimeout(() => {
659
+ reject(new Error('Python bridge failed to start within 15 seconds'))
660
+ }, 15000)
661
+
662
+ const onData = (chunk: Buffer | string) => {
663
+ this._stdoutBuffer += chunk.toString()
664
+ const lines = this._stdoutBuffer.split('\n')
665
+ this._stdoutBuffer = lines.pop() || ''
666
+
667
+ for (const line of lines) {
668
+ if (!line.trim()) continue
669
+ try {
670
+ const msg = JSON.parse(line)
671
+ if (msg.type === 'ready' && msg.ok) {
672
+ clearTimeout(timer)
673
+ // Switch to the normal data handler
674
+ child.stdout!.removeListener('data', onData)
675
+ child.stdout!.on('data', this._onBridgeData.bind(this))
676
+ resolve()
677
+ return
678
+ }
679
+ } catch {
680
+ // ignore non-JSON during init
681
+ }
682
+ }
683
+ }
684
+
685
+ child.stdout!.on('data', onData)
686
+
687
+ child.on('error', (err: Error) => {
688
+ clearTimeout(timer)
689
+ reject(new Error(`Python bridge process error: ${err.message}`))
690
+ })
691
+
692
+ child.on('exit', (code: number | null) => {
693
+ clearTimeout(timer)
694
+ reject(new Error(`Python bridge exited during startup with code ${code}`))
695
+ })
696
+ })
697
+
698
+ // Send init handshake with project directory
699
+ child.stdin!.write(JSON.stringify({ project_dir: this.projectDir }) + '\n')
700
+
701
+ await readyPromise
702
+
703
+ // Register crash handler (after successful startup)
704
+ child.removeAllListeners('exit')
705
+ child.on('exit', (code: number | null) => {
706
+ const sessionId = this.state.get('sessionId')
707
+ this.state.set('sessionActive', false)
708
+ this._bridgeProcess = null
709
+
710
+ // Reject all pending requests
711
+ for (const [id, pending] of this._pendingRequests) {
712
+ pending.reject(new Error(`Python bridge exited unexpectedly with code ${code}`))
713
+ }
714
+ this._pendingRequests.clear()
715
+
716
+ this.emit('sessionError', { error: `Bridge exited with code ${code}`, sessionId })
717
+ })
718
+
719
+ // Capture stderr for diagnostics (don't interfere with protocol)
720
+ child.stderr!.on('data', (chunk: Buffer | string) => {
721
+ const text = chunk.toString().trim()
722
+ if (text) {
723
+ this.emit('sessionError', { error: text, sessionId: this.state.get('sessionId') })
724
+ }
725
+ })
726
+
727
+ const sessionId = this.container.utils.uuid()
728
+ this.state.set('sessionActive', true)
729
+ this.state.set('sessionId', sessionId)
730
+ this.emit('sessionStarted', { sessionId })
731
+ }
732
+
733
+ /**
734
+ * Stops the persistent Python session and cleans up the bridge process.
735
+ *
736
+ * @example
737
+ * ```typescript
738
+ * await python.stopSession()
739
+ * ```
740
+ */
741
+ async stopSession(): Promise<void> {
742
+ const sessionId = this.state.get('sessionId')
743
+
744
+ if (this._bridgeProcess) {
745
+ this._bridgeProcess.removeAllListeners('exit')
746
+ this._bridgeProcess.kill('SIGTERM')
747
+ this._bridgeProcess = null
748
+ }
749
+
750
+ // Reject any pending requests
751
+ for (const [id, pending] of this._pendingRequests) {
752
+ pending.reject(new Error('Python session stopped'))
753
+ }
754
+ this._pendingRequests.clear()
755
+ this._stdoutBuffer = ''
756
+
757
+ this.state.set('sessionActive', false)
758
+ this.state.set('sessionId', null)
759
+
760
+ if (sessionId) {
761
+ this.emit('sessionStopped', { sessionId })
762
+ }
763
+ }
764
+
765
+ /**
766
+ * Executes Python code in the persistent session. Variables and imports
767
+ * survive across calls. This is the session equivalent of execute().
768
+ *
769
+ * @param code - Python code to execute
770
+ * @param variables - Variables to inject into the namespace before execution
771
+ * @returns The execution result including captured stdout and any error info
772
+ *
773
+ * @example
774
+ * ```typescript
775
+ * await python.startSession()
776
+ *
777
+ * // State persists across calls
778
+ * await python.run('x = 42')
779
+ * const result = await python.run('print(x * 2)')
780
+ * console.log(result.stdout) // '84\n'
781
+ *
782
+ * // Inject variables from JS
783
+ * const result2 = await python.run('print(f"Hello {name}!")', { name: 'World' })
784
+ * console.log(result2.stdout) // 'Hello World!\n'
785
+ * ```
786
+ */
787
+ async run(code: string, variables: Record<string, any> = {}): Promise<RunResult> {
788
+ const response = await this._sendRequest('exec', { code, variables })
789
+ return {
790
+ ok: response.ok,
791
+ result: response.result ?? null,
792
+ stdout: response.stdout ?? '',
793
+ error: response.error,
794
+ traceback: response.traceback,
795
+ }
796
+ }
797
+
798
+ /**
799
+ * Evaluates a Python expression in the persistent session and returns its value.
800
+ *
801
+ * @param expression - Python expression to evaluate
802
+ * @returns The evaluated result (JSON-serializable, or repr() string for complex types)
803
+ *
804
+ * @example
805
+ * ```typescript
806
+ * await python.run('x = 42')
807
+ * const result = await python.eval('x * 2')
808
+ * console.log(result) // 84
809
+ * ```
810
+ */
811
+ async eval(expression: string): Promise<any> {
812
+ const response = await this._sendRequest('eval', { expression })
813
+ if (!response.ok) {
814
+ throw new Error(response.error || 'eval failed')
815
+ }
816
+ return response.result
817
+ }
818
+
819
+ /**
820
+ * Imports a Python module into the persistent session namespace.
821
+ *
822
+ * @param moduleName - Dotted module path (e.g. 'myapp.models')
823
+ * @param alias - Optional alias for the import (defaults to the last segment)
824
+ *
825
+ * @example
826
+ * ```typescript
827
+ * await python.importModule('json')
828
+ * await python.importModule('myapp.models', 'models')
829
+ * const result = await python.eval('models.User')
830
+ * ```
831
+ */
832
+ async importModule(moduleName: string, alias?: string): Promise<void> {
833
+ const response = await this._sendRequest('import', { module: moduleName, alias })
834
+ if (!response.ok) {
835
+ throw new Error(response.error || `Failed to import ${moduleName}`)
836
+ }
837
+ }
838
+
839
+ /**
840
+ * Calls a function by dotted path in the persistent session namespace.
841
+ *
842
+ * @param funcPath - Dotted path to the function (e.g. 'json.dumps' or 'my_func')
843
+ * @param args - Positional arguments
844
+ * @param kwargs - Keyword arguments
845
+ * @returns The function's return value
846
+ *
847
+ * @example
848
+ * ```typescript
849
+ * await python.importModule('json')
850
+ * const result = await python.call('json.dumps', [{ a: 1 }], { indent: 2 })
851
+ * ```
852
+ */
853
+ async call(funcPath: string, args: any[] = [], kwargs: Record<string, any> = {}): Promise<any> {
854
+ const response = await this._sendRequest('call', { function: funcPath, args, kwargs })
855
+ if (!response.ok) {
856
+ throw new Error(response.error || `Failed to call ${funcPath}`)
857
+ }
858
+ return response.result
859
+ }
860
+
861
+ /**
862
+ * Returns all non-dunder variables from the persistent session namespace.
863
+ *
864
+ * @returns A record of variable names to their JSON-serializable values
865
+ *
866
+ * @example
867
+ * ```typescript
868
+ * await python.run('x = 42\ny = "hello"')
869
+ * const locals = await python.getLocals()
870
+ * console.log(locals) // { x: 42, y: 'hello' }
871
+ * ```
872
+ */
873
+ async getLocals(): Promise<Record<string, any>> {
874
+ const response = await this._sendRequest('get_locals')
875
+ if (!response.ok) {
876
+ throw new Error(response.error || 'Failed to get locals')
877
+ }
878
+ return response.result
879
+ }
880
+
881
+ /**
882
+ * Clears all variables and imports from the persistent session namespace.
883
+ * The session remains active — you can continue calling run() after reset.
884
+ *
885
+ * @example
886
+ * ```typescript
887
+ * await python.run('x = 42')
888
+ * await python.resetSession()
889
+ * // x is now undefined
890
+ * ```
891
+ */
892
+ async resetSession(): Promise<void> {
893
+ const response = await this._sendRequest('reset')
894
+ if (!response.ok) {
895
+ throw new Error(response.error || 'Failed to reset session')
896
+ }
897
+ }
485
898
  }
486
899
 
487
900
  export default Python