@simpleplatform/sdk 1.0.0

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/dist/ai.d.ts ADDED
@@ -0,0 +1,239 @@
1
+ /**
2
+ * @file Simple Platform AI SDK
3
+ *
4
+ * This module provides a high-level, developer-friendly interface for interacting
5
+ * with the platform's powerful, asynchronous AI engine. It abstracts away the
6
+ * underlying complexity of calling the trusted `ai-orchestrator` primitive,
7
+ * handling payload construction and response transformation automatically.
8
+ *
9
+ * This makes AI a first-class, reusable primitive, available to any developer
10
+ * in any logic module they build on the Simple platform.
11
+ */
12
+ import type { Context, DocumentHandle } from './types';
13
+ /**
14
+ * Base properties common to all JSON Schema definitions.
15
+ */
16
+ interface JSONSchemaBase {
17
+ description?: string;
18
+ }
19
+ /**
20
+ * JSON Schema definition for a string type.
21
+ */
22
+ export interface JSONSchemaString extends JSONSchemaBase {
23
+ format?: 'date' | 'date-time' | 'email' | 'uri';
24
+ maxLength?: number;
25
+ minLength?: number;
26
+ pattern?: string;
27
+ type: 'string';
28
+ }
29
+ /**
30
+ * JSON Schema definition for a number or integer type.
31
+ */
32
+ export interface JSONSchemaNumber extends JSONSchemaBase {
33
+ exclusiveMaximum?: number;
34
+ exclusiveMinimum?: number;
35
+ maximum?: number;
36
+ minimum?: number;
37
+ multipleOf?: number;
38
+ type: 'integer' | 'number';
39
+ }
40
+ /**
41
+ * JSON Schema definition for a boolean type.
42
+ */
43
+ export interface JSONSchemaBoolean extends JSONSchemaBase {
44
+ type: 'boolean';
45
+ }
46
+ /**
47
+ * JSON Schema definition for an object type.
48
+ */
49
+ export interface JSONSchemaObject extends JSONSchemaBase {
50
+ properties: Record<string, JSONSchema>;
51
+ required?: string[];
52
+ type: 'object';
53
+ }
54
+ /**
55
+ * JSON Schema definition for an array type.
56
+ */
57
+ export interface JSONSchemaArray extends JSONSchemaBase {
58
+ items: JSONSchema;
59
+ maxItems?: number;
60
+ minItems?: number;
61
+ type: 'array';
62
+ }
63
+ /**
64
+ * A discriminated union representing a complete and strongly-typed JSON Schema.
65
+ * This provides developers with precise autocompletion and type-checking.
66
+ */
67
+ export type JSONSchema = JSONSchemaArray | JSONSchemaBoolean | JSONSchemaNumber | JSONSchemaObject | JSONSchemaString;
68
+ /**
69
+ * A set of common configuration options shared across all AI operations.
70
+ * This adheres to the DRY principle, ensuring a consistent API surface.
71
+ */
72
+ export interface AICommonOptions {
73
+ /**
74
+ * The kind of model to use for this execution.
75
+ * Example: "medium", "large".
76
+ * If omitted, the platform chooses a sensible default model.
77
+ */
78
+ model?: 'large' | 'lite' | 'medium' | 'xl';
79
+ /** A natural language prompt to guide the AI's process. */
80
+ prompt: string;
81
+ /**
82
+ * If true, forces the AI to provide a detailed, multi-step reasoning
83
+ * process in the generated output. Defaults to false.
84
+ */
85
+ reasoning?: boolean;
86
+ /**
87
+ * The maximum number of tokens to spend on the reasoning process.
88
+ * If omitted, the platform chooses a sensible default.
89
+ * Ignored if `reasoning` is false.
90
+ */
91
+ reasoningBudget?: number;
92
+ /**
93
+ * If true, forces the AI to re-process the input, ignoring
94
+ * any previously cached results in the AI Memcache. Defaults to false.
95
+ */
96
+ regenerate?: boolean;
97
+ /**
98
+ * (Optional) A system prompt to define the AI's role, personality, or
99
+ * high-level instructions for the entire task.
100
+ */
101
+ systemPrompt?: string;
102
+ /**
103
+ * (Optional) Controls the creativity of the model. A value from 0.0 (most
104
+ * deterministic) to 1.0 (most creative). Defaults to the model's standard.
105
+ */
106
+ temperature?: number;
107
+ /**
108
+ * (Optional) The maximum time in milliseconds to wait for the AI operation.
109
+ * If this timeout is exceeded, the promise will reject. Defaults to 30,000ms.
110
+ */
111
+ timeout?: number;
112
+ }
113
+ /**
114
+ * The configuration options for an AI `extract` operation.
115
+ * Extends the common options with a required `schema`.
116
+ */
117
+ export interface AIExtractOptions extends AICommonOptions {
118
+ /**
119
+ * The desired JSON schema of the output. This provides a strong contract
120
+ * for the AI to follow when generating its response.
121
+ */
122
+ schema: JSONSchema;
123
+ }
124
+ /**
125
+ * The configuration options for an AI `summarize` operation.
126
+ * Extends the common options. No additional properties are needed.
127
+ */
128
+ export interface AISummarizeOptions extends AICommonOptions {
129
+ }
130
+ /**
131
+ * Configuration options for audio/video transcription.
132
+ * Extends common options but excludes `prompt` as it's generated internally.
133
+ */
134
+ export interface AITranscribeOptions extends Omit<AICommonOptions, 'prompt'> {
135
+ /**
136
+ * If true, includes timestamps in the transcript (format: [MM:SS]).
137
+ * Only applicable when `includeTranscript` is true.
138
+ */
139
+ includeTimestamps?: boolean;
140
+ /**
141
+ * If true, includes the full transcript in the response.
142
+ * At least one of `includeTranscript` or `summarize` must be true.
143
+ */
144
+ includeTranscript?: boolean;
145
+ /**
146
+ * Participant identification configuration:
147
+ * - `true`: Auto-detect participants and label as "Participant 1", "Participant 2", etc.
148
+ * - `string[]`: Array of participant names/roles to identify (e.g., ['Customer', 'Agent'])
149
+ * - `undefined`: No participant identification (default)
150
+ *
151
+ * When provided, the transcript will include participant labels for each segment.
152
+ */
153
+ participants?: boolean | string[];
154
+ /**
155
+ * If true, includes a summary of the audio/video content.
156
+ * At least one of `includeTranscript` or `summarize` must be true.
157
+ */
158
+ summarize?: boolean;
159
+ }
160
+ /**
161
+ * The response structure from a successful AI `extract` or `summarize` operation.
162
+ */
163
+ export interface AIExecutionResult {
164
+ /**
165
+ * The primary data returned by the AI. For `extract`, this is the structured
166
+ * object matching the requested schema. For `summarize`, it's the summary string.
167
+ */
168
+ data: any;
169
+ /**
170
+ * Rich metadata about the AI execution, useful for auditing, debugging, and
171
+ * providing context to the user.
172
+ */
173
+ metadata: {
174
+ /** The number of tokens in the input prompt. */
175
+ inputTokens: number;
176
+ /** The number of tokens in the generated output. */
177
+ outputTokens: number;
178
+ /** A summary of the AI's internal reasoning process, if provided. */
179
+ reasoning?: string;
180
+ /** The number of tokens used for internal "thinking", if applicable. */
181
+ reasoningTokens?: number;
182
+ };
183
+ }
184
+ /**
185
+ * Extracts structured data from a given input using the Simple AI engine.
186
+ *
187
+ * @param input The source data for the extraction (string, document handle, or object).
188
+ * @param options The configuration for the extraction operation.
189
+ * @param context The execution context provided by the host.
190
+ * @returns A promise that resolves to an `AIExecutionResult` object.
191
+ * @throws Will throw an error if the operation fails or inputs are invalid.
192
+ */
193
+ export declare function extract(input: DocumentHandle | object | string, options: AIExtractOptions, context: Context): Promise<AIExecutionResult>;
194
+ /**
195
+ * Generates a summary for a given input using the Simple AI engine.
196
+ *
197
+ * @param input The source data for the summarization (string, document handle, or object).
198
+ * @param options The configuration for the summarization operation.
199
+ * @param context The execution context provided by the host.
200
+ * @returns A promise that resolves to an `AIExecutionResult` object containing the summary.
201
+ * @throws Will throw an error if the operation fails or inputs are invalid.
202
+ */
203
+ export declare function summarize(input: DocumentHandle | object | string, options: AISummarizeOptions, context: Context): Promise<AIExecutionResult>;
204
+ /**
205
+ * Transcribes audio or video from a document handle using the Simple AI engine.
206
+ *
207
+ * This function internally uses the `extract` operation with a dynamically generated
208
+ * schema based on the requested options. It supports participant identification,
209
+ * timestamps, and summarization.
210
+ *
211
+ * @param input The audio or video file as a DocumentHandle.
212
+ * @param options The configuration for the transcription operation.
213
+ * @param context The execution context provided by the host.
214
+ * @returns A promise that resolves to an `AIExecutionResult` with structured transcription data.
215
+ * @throws Will throw an error if the operation fails or inputs are invalid.
216
+ *
217
+ * @example
218
+ * // Basic transcript
219
+ * const result = await transcribe(audioHandle, { includeTranscript: true }, context)
220
+ * // Returns: { language: "en", transcript: "..." }
221
+ *
222
+ * @example
223
+ * // Transcript with participant identification
224
+ * const result = await transcribe(audioHandle, {
225
+ * includeTranscript: true,
226
+ * participants: ['Customer', 'Support Agent']
227
+ * }, context)
228
+ * // Returns: { language: "en", transcript: "Customer: Hello...\nSupport Agent: Hi...", participants: [...] }
229
+ *
230
+ * @example
231
+ * // Summary with auto-detected participants
232
+ * const result = await transcribe(videoHandle, {
233
+ * summarize: true,
234
+ * participants: true
235
+ * }, context)
236
+ * // Returns: { language: "en", summary: "...", participants: ["Participant 1", "Participant 2"] }
237
+ */
238
+ export declare function transcribe(input: DocumentHandle, options: AITranscribeOptions, context: Context): Promise<AIExecutionResult>;
239
+ export {};
package/dist/ai.js ADDED
@@ -0,0 +1,265 @@
1
+ import { execute as hostExecute } from './host';
2
+ // ============================================================================
3
+ // Internal Implementation
4
+ // ============================================================================
5
+ /**
6
+ * Recursively processes an object to detect and upload pending files.
7
+ * When a pending DocumentHandle is detected (has `pending: true` and `file_hash`),
8
+ * it calls the ephemeral upload host function and replaces the pending handle
9
+ * with the ephemeral handle returned from the upload.
10
+ *
11
+ * @internal
12
+ */
13
+ async function _uploadPendingFiles(obj, context) {
14
+ if (obj === null || typeof obj !== 'object') {
15
+ return obj;
16
+ }
17
+ if (obj.pending === true && obj.file_hash) {
18
+ const response = await hostExecute('action:documents/upload-ephemeral', obj, context);
19
+ if (!response.ok) {
20
+ throw new Error(response.error?.message || 'Failed to upload pending file');
21
+ }
22
+ return response.data;
23
+ }
24
+ if (Array.isArray(obj)) {
25
+ return Promise.all(obj.map(item => _uploadPendingFiles(item, context)));
26
+ }
27
+ return obj;
28
+ }
29
+ /**
30
+ * The internal, shared logic for executing any AI operation. This function
31
+ * is not exported and serves as the single, DRY implementation for all
32
+ * public-facing AI functions.
33
+ *
34
+ * @internal
35
+ */
36
+ async function _executeAIOperation(operation, input, options, context) {
37
+ const { model, prompt, reasoning, reasoningBudget, regenerate = false, systemPrompt, temperature, timeout, } = options;
38
+ const processedInput = await _uploadPendingFiles(input, context);
39
+ // 1. Construct the universal options payload for caching and execution.
40
+ const universalOptions = {
41
+ ...(temperature !== undefined && { temperature }),
42
+ ...(reasoningBudget !== undefined && { reasoningBudget }),
43
+ ...({ reasoning }),
44
+ };
45
+ // 2. Construct the full payload for the `ai-orchestrator` Go primitive.
46
+ // This includes the operation-specific `schema` if it exists.
47
+ const payload = {
48
+ input: processedInput,
49
+ model,
50
+ operation,
51
+ options: universalOptions,
52
+ prompt,
53
+ regenerate,
54
+ schema: options.schema, // Will be undefined for summarize, which is correct
55
+ systemPrompt,
56
+ timeout,
57
+ };
58
+ // 3. Call the trusted primitive. The SDK's `hostExecute` handles the complexity
59
+ // of the underlying `logic:` call and the async execution.
60
+ const response = await hostExecute('logic:dev.simple.system/ai-orchestrator', payload, context);
61
+ if (!response.ok) {
62
+ throw new Error(response.error?.message || `AI '${operation}' operation failed.`);
63
+ }
64
+ // 4. Transform the raw backend response into the clean, developer-facing API contract.
65
+ const aioData = response.data;
66
+ const data = aioData?.data;
67
+ const metadata = aioData?.metadata || {};
68
+ return {
69
+ data,
70
+ metadata: {
71
+ inputTokens: metadata.input_tokens,
72
+ outputTokens: metadata.output_tokens,
73
+ reasoning: metadata.reasoning,
74
+ reasoningTokens: metadata.reasoning_tokens,
75
+ },
76
+ };
77
+ }
78
+ // ============================================================================
79
+ // Public SDK Functions
80
+ // ============================================================================
81
+ /**
82
+ * Extracts structured data from a given input using the Simple AI engine.
83
+ *
84
+ * @param input The source data for the extraction (string, document handle, or object).
85
+ * @param options The configuration for the extraction operation.
86
+ * @param context The execution context provided by the host.
87
+ * @returns A promise that resolves to an `AIExecutionResult` object.
88
+ * @throws Will throw an error if the operation fails or inputs are invalid.
89
+ */
90
+ export async function extract(input, options, context) {
91
+ // Perform client-side validation specific to the `extract` operation.
92
+ if (!input) {
93
+ throw new Error('The `input` parameter is required for `extract`.');
94
+ }
95
+ if (!options.schema || typeof options.schema !== 'object') {
96
+ throw new Error('The `schema` parameter must be a valid JSONSchema object for `extract`.');
97
+ }
98
+ if (!options.prompt || typeof options.prompt !== 'string') {
99
+ throw new Error('The `prompt` parameter must be a non-empty string for `extract`.');
100
+ }
101
+ // Delegate to the shared internal execution logic.
102
+ return _executeAIOperation('extract', input, options, context);
103
+ }
104
+ /**
105
+ * Generates a summary for a given input using the Simple AI engine.
106
+ *
107
+ * @param input The source data for the summarization (string, document handle, or object).
108
+ * @param options The configuration for the summarization operation.
109
+ * @param context The execution context provided by the host.
110
+ * @returns A promise that resolves to an `AIExecutionResult` object containing the summary.
111
+ * @throws Will throw an error if the operation fails or inputs are invalid.
112
+ */
113
+ export async function summarize(input, options, context) {
114
+ // Perform client-side validation specific to the `summarize` operation.
115
+ if (!input) {
116
+ throw new Error('The `input` parameter is required for `summarize`.');
117
+ }
118
+ if (!options.prompt || typeof options.prompt !== 'string') {
119
+ throw new Error('The `prompt` parameter must be a non-empty string for `summarize`.');
120
+ }
121
+ // Delegate to the shared internal execution logic.
122
+ return _executeAIOperation('summarize', input, options, context);
123
+ }
124
+ /**
125
+ * Transcribes audio or video from a document handle using the Simple AI engine.
126
+ *
127
+ * This function internally uses the `extract` operation with a dynamically generated
128
+ * schema based on the requested options. It supports participant identification,
129
+ * timestamps, and summarization.
130
+ *
131
+ * @param input The audio or video file as a DocumentHandle.
132
+ * @param options The configuration for the transcription operation.
133
+ * @param context The execution context provided by the host.
134
+ * @returns A promise that resolves to an `AIExecutionResult` with structured transcription data.
135
+ * @throws Will throw an error if the operation fails or inputs are invalid.
136
+ *
137
+ * @example
138
+ * // Basic transcript
139
+ * const result = await transcribe(audioHandle, { includeTranscript: true }, context)
140
+ * // Returns: { language: "en", transcript: "..." }
141
+ *
142
+ * @example
143
+ * // Transcript with participant identification
144
+ * const result = await transcribe(audioHandle, {
145
+ * includeTranscript: true,
146
+ * participants: ['Customer', 'Support Agent']
147
+ * }, context)
148
+ * // Returns: { language: "en", transcript: "Customer: Hello...\nSupport Agent: Hi...", participants: [...] }
149
+ *
150
+ * @example
151
+ * // Summary with auto-detected participants
152
+ * const result = await transcribe(videoHandle, {
153
+ * summarize: true,
154
+ * participants: true
155
+ * }, context)
156
+ * // Returns: { language: "en", summary: "...", participants: ["Participant 1", "Participant 2"] }
157
+ */
158
+ export async function transcribe(input, options, context) {
159
+ // Validation
160
+ if (!input || typeof input !== 'object' || !input.file_hash) {
161
+ throw new Error('The `input` parameter must be a valid DocumentHandle for `transcribe`.');
162
+ }
163
+ const { includeTimestamps = false, includeTranscript = false, participants, summarize = false, } = options;
164
+ if (!includeTranscript && !summarize) {
165
+ throw new Error('At least one of `includeTranscript` or `summarize` must be true for `transcribe`.');
166
+ }
167
+ // Validate mime type is audio or video
168
+ const mimeType = input.mime_type?.toLowerCase() || '';
169
+ if (!mimeType.startsWith('audio/') && !mimeType.startsWith('video/')) {
170
+ throw new Error('The input file must be an audio or video file for `transcribe`.');
171
+ }
172
+ // Build the dynamic schema based on user options
173
+ const schemaProperties = {
174
+ language: {
175
+ description: 'The detected language of the audio (ISO 639-1 code, e.g., "en", "es")',
176
+ type: 'string',
177
+ },
178
+ };
179
+ const requiredFields = ['language'];
180
+ if (includeTranscript) {
181
+ let transcriptDesc = 'The full transcript of the audio';
182
+ if (participants) {
183
+ if (includeTimestamps) {
184
+ transcriptDesc = 'The full transcript with participant labels and timestamps. Format: [MM:SS] Participant Name: text';
185
+ }
186
+ else {
187
+ transcriptDesc = 'The full transcript with participant labels. Format: Participant Name: text';
188
+ }
189
+ }
190
+ else if (includeTimestamps) {
191
+ transcriptDesc = 'The full transcript with timestamps. Format: [MM:SS] text';
192
+ }
193
+ schemaProperties.transcript = {
194
+ description: transcriptDesc,
195
+ type: 'string',
196
+ };
197
+ requiredFields.push('transcript');
198
+ }
199
+ if (summarize) {
200
+ schemaProperties.summary = {
201
+ description: participants
202
+ ? 'A concise summary of the audio content, including key points from each participant'
203
+ : 'A concise summary of the audio content',
204
+ type: 'string',
205
+ };
206
+ requiredFields.push('summary');
207
+ }
208
+ // Add participants array to schema if participant identification is requested
209
+ if (participants) {
210
+ schemaProperties.participants = {
211
+ description: 'List of identified participants in the audio',
212
+ items: { type: 'string' },
213
+ type: 'array',
214
+ };
215
+ requiredFields.push('participants');
216
+ }
217
+ const schema = {
218
+ properties: schemaProperties,
219
+ required: requiredFields,
220
+ type: 'object',
221
+ };
222
+ // Build the prompt
223
+ let prompt = 'Analyze this audio/video file and provide:\n';
224
+ if (includeTranscript) {
225
+ if (participants) {
226
+ if (Array.isArray(participants)) {
227
+ prompt += `- A complete transcript identifying these participants: ${participants.join(', ')}. `;
228
+ }
229
+ else {
230
+ prompt += '- A complete transcript with participant identification (label participants as Participant 1, Participant 2, etc.). ';
231
+ }
232
+ if (includeTimestamps) {
233
+ prompt += 'Include timestamps in [MM:SS] format before each participant segment.\n';
234
+ }
235
+ else {
236
+ prompt += 'Format each line as "Participant Name: text".\n';
237
+ }
238
+ }
239
+ else {
240
+ prompt += includeTimestamps
241
+ ? '- A complete transcript with timestamps in [MM:SS] format before each segment\n'
242
+ : '- A complete transcript of all spoken content\n';
243
+ }
244
+ }
245
+ if (summarize) {
246
+ prompt += participants
247
+ ? '- A concise summary highlighting key points from each participant\n'
248
+ : '- A concise summary of the main points and key information\n';
249
+ }
250
+ if (participants) {
251
+ if (Array.isArray(participants)) {
252
+ prompt += `- Identify and distinguish between these participants: ${participants.join(', ')}\n`;
253
+ }
254
+ else {
255
+ prompt += '- Identify and list all distinct participants in the audio\n';
256
+ }
257
+ }
258
+ prompt += '- The detected language code (ISO 639-1 format)\n';
259
+ // Delegate to extract with our generated schema
260
+ return extract(input, {
261
+ ...options,
262
+ prompt,
263
+ schema,
264
+ }, context);
265
+ }
package/dist/build.js ADDED
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('node:fs/promises')
4
+ const os = require('node:os')
5
+ const path = require('node:path')
6
+ const esbuild = require('esbuild')
7
+
8
+ async function main() {
9
+ // eslint-disable-next-line node/prefer-global/process
10
+ const [entryPoint, outFile] = process.argv.slice(2)
11
+
12
+ if (!entryPoint || !outFile) {
13
+ console.error('Usage: simple-sdk-build <entryPoint> <outFile>')
14
+ // eslint-disable-next-line node/prefer-global/process
15
+ process.exit(1)
16
+ }
17
+
18
+ const entryPointAbs = path.resolve(entryPoint)
19
+ const outFileAbs = path.resolve(outFile)
20
+
21
+ /**
22
+ * This is a local esbuild plugin to solve our specific problem.
23
+ * It intercepts module resolution. When it sees a file inside the
24
+ * @simple/sdk package trying to import the exact relative path './host',
25
+ * it redirects the bundler to our worker-override.js script instead.
26
+ * This is the robust way to swap implementations at build time.
27
+ */
28
+ const simpleSdkHostAliasPlugin = {
29
+ name: 'simple-sdk-host-alias',
30
+ setup(build) {
31
+ // Find the absolute path to the SDK's dist directory.
32
+ // We need this to check if an import is coming from our SDK.
33
+ const sdkDistPath = path.dirname(require.resolve('@simple/sdk'))
34
+
35
+ // The 'onResolve' hook intercepts module lookups.
36
+ build.onResolve({ filter: /^\.\/host$/ }, (args) => {
37
+ // `filter` matches the exact import path string: './host'.
38
+ // `args.importer` is the absolute path of the file doing the import.
39
+
40
+ // If the file doing the import is NOT inside our SDK's dist folder,
41
+ // we ignore it and let esbuild handle it normally.
42
+ if (!args.importer.startsWith(sdkDistPath)) {
43
+ return
44
+ }
45
+
46
+ // It's a match! Redirect the import to our override file.
47
+ const overridePath = path.resolve(__dirname, 'worker-override.js')
48
+ return { path: overridePath }
49
+ })
50
+ },
51
+ }
52
+
53
+ // We will now create a temporary entry file that esbuild will use.
54
+ // This allows us to bundle and minify everything in a single, efficient step.
55
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'simple-sdk-build-'))
56
+ const tempEntryFile = path.join(tempDir, 'entry.js')
57
+
58
+ try {
59
+ /**
60
+ * This is the content of our new, dynamic entry point. It controls the
61
+ * precise order of operations.
62
+ */
63
+ const entryPointContent = `
64
+ // This is the only module we need to define the main handler.
65
+ // The user's code will be bundled into it via the dynamic import.
66
+
67
+ export default async function() {
68
+ const channel = { promise: null };
69
+
70
+ try {
71
+ // STEP 1: Create the promise channel and place it on the global scope.
72
+ globalThis.__SIMPLE_PROMISE_CHANNEL__ = channel;
73
+
74
+ // STEP 2: Dynamically import the user's application. This is a critical change.
75
+ // The 'import()' statement executes the user's top-level code, which will
76
+ // call simple.Handle and populate the channel.promise.
77
+ await import('${entryPointAbs.replace(/\\/g, '/')}');
78
+
79
+ // STEP 3: Now that the user's code has run, check the channel for the promise.
80
+ if (channel.promise) {
81
+ // Await the promise to get the final result of the async handler.
82
+ return await channel.promise;
83
+ } else {
84
+ console.warn('SDK build warning: simple.Handle was not called by the user application.');
85
+ return undefined;
86
+ }
87
+ } finally {
88
+ // STEP 4: Always clean up the global scope to maintain the sandbox.
89
+ delete globalThis.__SIMPLE_PROMISE_CHANNEL__;
90
+ }
91
+ };
92
+ `
93
+
94
+ await fs.writeFile(tempEntryFile, entryPointContent)
95
+
96
+ // Read the contents of the temp file to pass via stdin.
97
+ const tempFileContents = await fs.readFile(tempEntryFile, 'utf8')
98
+
99
+ // --- STAGE 1: Build the user's application and the wrapper together ---
100
+ const appBundleResult = await esbuild.build({
101
+ bundle: true,
102
+ define: {
103
+ __ASYNC_BUILD__: 'true',
104
+ __IS_WORKER_BUILD__: 'true',
105
+ },
106
+ format: 'iife',
107
+ globalName: '__SIMPLE_MAIN_HANDLER__', // Assign the IIFE result to a global
108
+ minify: true, // Minify the entire bundle
109
+ plugins: [simpleSdkHostAliasPlugin],
110
+ stdin: {
111
+ contents: tempFileContents,
112
+ // eslint-disable-next-line node/prefer-global/process
113
+ resolveDir: process.cwd(),
114
+ sourcefile: 'simple-sdk-virtual-entry.js', // Provide a fake filename for better error messages
115
+ },
116
+ write: false,
117
+ })
118
+
119
+ // The result is a minified IIFE that returns our async handler function.
120
+ const userScriptBundle = appBundleResult.outputFiles[0].text
121
+
122
+ // The final script for the worker just needs to execute this handler.
123
+ // The IIFE returns an object with a `default` property containing our async function.
124
+ const finalWorkerScript = `
125
+ ${userScriptBundle}
126
+ return __SIMPLE_MAIN_HANDLER__.default();
127
+ `
128
+
129
+ // --- STAGE 2: Build the final WASM loader with the correctly-built user script injected ---
130
+ await esbuild.build({
131
+ bundle: true,
132
+ define: {
133
+ __ASYNC_BUILD__: 'true',
134
+ // Inject the minified, wrapped script.
135
+ __USER_SCRIPT_BUNDLE__: JSON.stringify(finalWorkerScript),
136
+ },
137
+ minify: true,
138
+ outfile: outFileAbs,
139
+ stdin: {
140
+ contents: `import simple from '@simple/sdk'; simple.Handle(() => {});`,
141
+ // eslint-disable-next-line node/prefer-global/process
142
+ resolveDir: process.cwd(),
143
+ },
144
+ })
145
+
146
+ console.log(`✅ Simple SDK async build successful! Output: ${outFile}`)
147
+ }
148
+ catch (error) {
149
+ console.error('❌ Simple SDK async build failed:')
150
+ console.error(error)
151
+
152
+ // eslint-disable-next-line node/prefer-global/process
153
+ process.exit(1)
154
+ }
155
+ finally {
156
+ // Clean up the temporary directory
157
+ await fs.rm(tempDir, { force: true, recursive: true })
158
+ }
159
+ }
160
+
161
+ main()