@optimizely-opal/opal-tools-sdk 0.1.3-dev → 0.1.6-dev

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/src/decorators.ts CHANGED
@@ -1,70 +1,25 @@
1
- import 'reflect-metadata';
2
- import { ParameterType, Parameter, AuthRequirement } from './models';
3
- import { registry } from './registry';
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import "reflect-metadata";
3
+
4
+ import { AuthRequirement, Parameter, ParameterType } from "./models";
5
+ import { registry } from "./registry";
4
6
 
5
7
  interface ParameterDefinition {
6
- name: string;
7
- type: ParameterType;
8
8
  description: string;
9
+ name: string;
9
10
  required: boolean;
11
+ type: ParameterType;
10
12
  }
11
13
 
12
14
  interface ToolOptions {
13
- name: string;
14
- description: string;
15
- parameters?: ParameterDefinition[];
16
15
  authRequirements?: {
17
16
  provider: string;
18
- scopeBundle: string;
19
17
  required?: boolean;
18
+ scopeBundle: string;
20
19
  };
21
- }
22
-
23
- /**
24
- * Map a TypeScript type to a ParameterType
25
- * @param type TypeScript type
26
- */
27
- function mapTypeToParameterType(type: any): ParameterType {
28
- if (type === String || type.name === 'String') {
29
- return ParameterType.String;
30
- } else if (type === Number || type.name === 'Number') {
31
- return ParameterType.Number;
32
- } else if (type === Boolean || type.name === 'Boolean') {
33
- return ParameterType.Boolean;
34
- } else if (type === Array || type.name === 'Array') {
35
- return ParameterType.List;
36
- } else if (type === Object || type.name === 'Object') {
37
- return ParameterType.Dictionary;
38
- }
39
-
40
- // Default to string
41
- return ParameterType.String;
42
- }
43
-
44
- /**
45
- * Extract parameters from a TypeScript interface
46
- * @param paramType Parameter type object
47
- */
48
- function extractParameters(paramType: any): Parameter[] {
49
- const parameters: Parameter[] = [];
50
-
51
- // This is very basic and doesn't handle complex types
52
- // For production use, this would need to be more sophisticated
53
- for (const key in paramType) {
54
- if (paramType.hasOwnProperty(key)) {
55
- const type = typeof paramType[key] === 'undefined' ? String : paramType[key].constructor;
56
- const required = true; // In a real implementation, we'd detect optional parameters
57
-
58
- parameters.push(new Parameter(
59
- key,
60
- mapTypeToParameterType(type),
61
- '', // Description - in a real impl we'd use TypeDoc or similar
62
- required
63
- ));
64
- }
65
- }
66
-
67
- return parameters;
20
+ description: string;
21
+ name: string;
22
+ parameters?: ParameterDefinition[];
68
23
  }
69
24
 
70
25
  /**
@@ -84,24 +39,30 @@ function extractParameters(paramType: any): Parameter[] {
84
39
  * ```
85
40
  */
86
41
  export function tool(options: ToolOptions) {
87
- return function(target: any, propertyKey?: string, descriptor?: PropertyDescriptor) {
42
+ return function (
43
+ target: any,
44
+ propertyKey?: string,
45
+ descriptor?: PropertyDescriptor,
46
+ ) {
88
47
  const isMethod = propertyKey && descriptor;
89
48
  const handler = isMethod ? descriptor.value : target;
90
49
 
91
50
  // Generate endpoint from name - ensure hyphens instead of underscores
92
- const endpoint = `/tools/${options.name.replace(/_/g, '-')}`;
51
+ const endpoint = `/tools/${options.name.replace(/_/g, "-")}`;
93
52
 
94
53
  // Convert parameter definitions to Parameter objects
95
54
  const parameters: Parameter[] = [];
96
55
  if (options.parameters && options.parameters.length > 0) {
97
56
  // Use the explicitly provided parameter definitions
98
57
  for (const paramDef of options.parameters) {
99
- parameters.push(new Parameter(
100
- paramDef.name,
101
- paramDef.type,
102
- paramDef.description,
103
- paramDef.required
104
- ));
58
+ parameters.push(
59
+ new Parameter(
60
+ paramDef.name,
61
+ paramDef.type,
62
+ paramDef.description,
63
+ paramDef.required,
64
+ ),
65
+ );
105
66
  }
106
67
  }
107
68
 
@@ -112,8 +73,8 @@ export function tool(options: ToolOptions) {
112
73
  new AuthRequirement(
113
74
  options.authRequirements.provider,
114
75
  options.authRequirements.scopeBundle,
115
- options.authRequirements.required ?? true
116
- )
76
+ options.authRequirements.required ?? true,
77
+ ),
117
78
  ];
118
79
  }
119
80
 
@@ -125,7 +86,7 @@ export function tool(options: ToolOptions) {
125
86
  handler,
126
87
  parameters,
127
88
  endpoint,
128
- authRequirements
89
+ authRequirements,
129
90
  );
130
91
  }
131
92
 
package/src/index.ts CHANGED
@@ -1,6 +1,10 @@
1
- import 'reflect-metadata';
1
+ import "reflect-metadata";
2
2
 
3
- export { ToolsService } from './service';
4
- export { tool } from './decorators';
5
- export { requiresAuth } from './auth';
6
- export * from './models';
3
+ export { requiresAuth } from "./auth";
4
+ export { Block, isBlockResponse } from "./block";
5
+ export type * from "./block";
6
+ export { tool } from "./decorators";
7
+ export * from "./models";
8
+ export { registerTool } from "./registerTool";
9
+ export type { RequestHandlerExtra } from "./registerTool";
10
+ export { ToolsService } from "./service";
package/src/models.ts CHANGED
@@ -2,44 +2,39 @@
2
2
  * Types of parameters supported by Opal tools
3
3
  */
4
4
  export enum ParameterType {
5
- String = 'string',
6
- Integer = 'integer',
7
- Number = 'number',
8
- Boolean = 'boolean',
9
- List = 'array',
10
- Dictionary = 'object'
5
+ Boolean = "boolean",
6
+ Dictionary = "object",
7
+ Integer = "integer",
8
+ List = "array",
9
+ Number = "number",
10
+ String = "string",
11
11
  }
12
12
 
13
13
  /**
14
- * Parameter definition for an Opal tool
14
+ * Authentication data for an Opal tool
15
15
  */
16
- export class Parameter {
17
- /**
18
- * Create a new parameter definition
19
- * @param name Parameter name
20
- * @param type Parameter type
21
- * @param description Parameter description
22
- * @param required Whether the parameter is required
23
- */
24
- constructor(
25
- public name: string,
26
- public type: ParameterType,
27
- public description: string,
28
- public required: boolean
29
- ) {}
16
+ export type AuthData = {
17
+ credentials: Credentials;
18
+ provider: string;
19
+ };
30
20
 
31
- /**
32
- * Convert to JSON for the discovery endpoint
33
- */
34
- toJSON() {
35
- return {
36
- name: this.name,
37
- type: this.type,
38
- description: this.description,
39
- required: this.required
40
- };
41
- }
42
- }
21
+ /**
22
+ * Authentication credentials structure
23
+ */
24
+ export type Credentials = {
25
+ access_token: string;
26
+ customer_id: string;
27
+ instance_id: string;
28
+ org_sso_id?: string;
29
+ product_sku: string;
30
+ };
31
+
32
+ /**
33
+ * Execution environment for an Opal tool
34
+ */
35
+ export type Environment = {
36
+ execution_mode: "headless" | "interactive";
37
+ };
43
38
 
44
39
  /**
45
40
  * Authentication requirements for an Opal tool
@@ -54,7 +49,7 @@ export class AuthRequirement {
54
49
  constructor(
55
50
  public provider: string,
56
51
  public scopeBundle: string,
57
- public required: boolean = true
52
+ public required: boolean = true,
58
53
  ) {}
59
54
 
60
55
  /**
@@ -63,8 +58,8 @@ export class AuthRequirement {
63
58
  toJSON() {
64
59
  return {
65
60
  provider: this.provider,
61
+ required: this.required,
66
62
  scope_bundle: this.scopeBundle,
67
- required: this.required
68
63
  };
69
64
  }
70
65
  }
@@ -76,7 +71,7 @@ export class Function {
76
71
  /**
77
72
  * HTTP method for the endpoint (default: POST)
78
73
  */
79
- public httpMethod: string = 'POST';
74
+ public httpMethod: string = "POST";
80
75
 
81
76
  /**
82
77
  * Create a new function definition
@@ -91,23 +86,26 @@ export class Function {
91
86
  public description: string,
92
87
  public parameters: Parameter[],
93
88
  public endpoint: string,
94
- public authRequirements?: AuthRequirement[]
89
+ public authRequirements?: AuthRequirement[],
95
90
  ) {}
96
91
 
97
92
  /**
98
93
  * Convert to JSON for the discovery endpoint
99
94
  */
100
95
  toJSON() {
96
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
101
97
  const result: any = {
102
- name: this.name,
103
98
  description: this.description,
104
- parameters: this.parameters.map(p => p.toJSON()),
105
99
  endpoint: this.endpoint,
106
- http_method: this.httpMethod
100
+ http_method: this.httpMethod,
101
+ name: this.name,
102
+ parameters: this.parameters.map((p) => p.toJSON()),
107
103
  };
108
104
 
109
105
  if (this.authRequirements && this.authRequirements.length > 0) {
110
- result.auth_requirements = this.authRequirements.map(auth => auth.toJSON());
106
+ result.auth_requirements = this.authRequirements.map((auth) =>
107
+ auth.toJSON(),
108
+ );
111
109
  }
112
110
 
113
111
  return result;
@@ -115,51 +113,23 @@ export class Function {
115
113
  }
116
114
 
117
115
  /**
118
- * Authentication credentials structure
119
- */
120
- export type Credentials ={
121
- access_token: string;
122
- org_sso_id?: string;
123
- customer_id: string;
124
- instance_id: string;
125
- product_sku: string;
126
- }
127
-
128
- /**
129
- * Authentication data for an Opal tool
130
- */
131
- export type AuthData = {
132
- provider: string;
133
- credentials: Credentials;
134
- }
135
-
136
- /**
137
- * Execution environment for an Opal tool
138
- */
139
- export type Environment = {
140
- execution_mode: "headless" | "interactive";
141
- }
142
-
143
- /**
144
- * Island field definition for interactive UI components
116
+ * Island action definition for interactive UI components
145
117
  */
146
- export class IslandField {
118
+ export class IslandAction {
147
119
  /**
148
- * Create a new island field
149
- * @param name Programmatic field identifier
150
- * @param label Human-readable label
151
- * @param type Field type
152
- * @param value Current field value
153
- * @param hidden Whether to hide from user
154
- * @param options Available options for selection
120
+ * Create a new island action
121
+ * @param name Programmatic action identifier
122
+ * @param label Human-readable button label
123
+ * @param type UI element type
124
+ * @param endpoint API endpoint to call
125
+ * @param operation Operation type
155
126
  */
156
127
  constructor(
157
128
  public name: string,
158
129
  public label: string,
159
- public type: "string" | "boolean" | "json",
160
- public value: string = "",
161
- public hidden: boolean = false,
162
- public options: string[] = []
130
+ public type: string,
131
+ public endpoint: string,
132
+ public operation: string = "create",
163
133
  ) {}
164
134
 
165
135
  /**
@@ -167,34 +137,35 @@ export class IslandField {
167
137
  */
168
138
  toJSON() {
169
139
  return {
170
- name: this.name,
140
+ endpoint: this.endpoint,
171
141
  label: this.label,
142
+ name: this.name,
143
+ operation: this.operation,
172
144
  type: this.type,
173
- value: this.value,
174
- hidden: this.hidden,
175
- options: this.options,
176
145
  };
177
146
  }
178
147
  }
179
148
 
180
149
  /**
181
- * Island action definition for interactive UI components
150
+ * Island field definition for interactive UI components
182
151
  */
183
- export class IslandAction {
152
+ export class IslandField {
184
153
  /**
185
- * Create a new island action
186
- * @param name Programmatic action identifier
187
- * @param label Human-readable button label
188
- * @param type UI element type
189
- * @param endpoint API endpoint to call
190
- * @param operation Operation type
154
+ * Create a new island field
155
+ * @param name Programmatic field identifier
156
+ * @param label Human-readable label
157
+ * @param type Field type
158
+ * @param value Current field value
159
+ * @param hidden Whether to hide from user
160
+ * @param options Available options for selection
191
161
  */
192
162
  constructor(
193
163
  public name: string,
194
164
  public label: string,
195
- public type: string,
196
- public endpoint: string,
197
- public operation: string = "create"
165
+ public type: "boolean" | "json" | "string",
166
+ public value: string = "",
167
+ public hidden: boolean = false,
168
+ public options: string[] = [],
198
169
  ) {}
199
170
 
200
171
  /**
@@ -202,11 +173,12 @@ export class IslandAction {
202
173
  */
203
174
  toJSON() {
204
175
  return {
205
- name: this.name,
176
+ hidden: this.hidden,
206
177
  label: this.label,
178
+ name: this.name,
179
+ options: this.options,
207
180
  type: this.type,
208
- endpoint: this.endpoint,
209
- operation: this.operation,
181
+ value: this.value,
210
182
  };
211
183
  }
212
184
  }
@@ -215,17 +187,21 @@ export class IslandAction {
215
187
  * Island configuration for interactive UI components
216
188
  */
217
189
  export class IslandConfig {
218
- static Field = IslandField;
219
190
  static Action = IslandAction;
191
+ static Field = IslandField;
220
192
 
221
193
  /**
222
194
  * Create a new island configuration
223
195
  * @param fields List of island fields
224
196
  * @param actions List of island actions
197
+ * @param type Optional island type
198
+ * @param icon Optional island icon
225
199
  */
226
200
  constructor(
227
201
  public fields: IslandField[],
228
- public actions: IslandAction[]
202
+ public actions: IslandAction[],
203
+ public type?: string,
204
+ public icon?: string,
229
205
  ) {}
230
206
 
231
207
  /**
@@ -233,8 +209,8 @@ export class IslandConfig {
233
209
  */
234
210
  toJSON() {
235
211
  return {
236
- fields: this.fields.map((field) => field.toJSON()),
237
212
  actions: this.actions.map((action) => action.toJSON()),
213
+ fields: this.fields.map((field) => field.toJSON()),
238
214
  };
239
215
  }
240
216
  }
@@ -265,21 +241,26 @@ export class IslandResponseConfig {
265
241
  export class IslandResponse {
266
242
  static ResponseConfig = IslandResponseConfig;
267
243
 
268
- public type: "island" = "island";
244
+ public type = "island" as const;
269
245
 
270
246
  /**
271
247
  * Create a new island response
272
248
  * @param config Response configuration
249
+ * @param message Optional message for the island response
273
250
  */
274
- constructor(public config: IslandResponseConfig) {}
251
+ constructor(
252
+ public config: IslandResponseConfig,
253
+ public message?: string,
254
+ ) {}
275
255
 
276
256
  /**
277
257
  * Create an island response with a list of islands
278
258
  * @param islands List of island configurations
259
+ * @param message Optional message for the island response
279
260
  * @returns New IslandResponse instance
280
261
  */
281
- static create(islands: IslandConfig[]): IslandResponse {
282
- return new IslandResponse(new IslandResponseConfig(islands));
262
+ static create(islands: IslandConfig[], message?: string): IslandResponse {
263
+ return new IslandResponse(new IslandResponseConfig(islands), message);
283
264
  }
284
265
 
285
266
  /**
@@ -291,3 +272,34 @@ export class IslandResponse {
291
272
  };
292
273
  }
293
274
  }
275
+
276
+ /**
277
+ * Parameter definition for an Opal tool
278
+ */
279
+ export class Parameter {
280
+ /**
281
+ * Create a new parameter definition
282
+ * @param name Parameter name
283
+ * @param type Parameter type
284
+ * @param description Parameter description
285
+ * @param required Whether the parameter is required
286
+ */
287
+ constructor(
288
+ public name: string,
289
+ public type: ParameterType,
290
+ public description: string,
291
+ public required: boolean,
292
+ ) {}
293
+
294
+ /**
295
+ * Convert to JSON for the discovery endpoint
296
+ */
297
+ toJSON() {
298
+ return {
299
+ description: this.description,
300
+ name: this.name,
301
+ required: this.required,
302
+ type: this.type,
303
+ };
304
+ }
305
+ }
@@ -0,0 +1,181 @@
1
+ import { z } from "zod/v4";
2
+
3
+ import type { BlockResponse } from "./block";
4
+
5
+ import {
6
+ AuthRequirement,
7
+ Credentials,
8
+ Parameter,
9
+ ParameterType,
10
+ } from "./models";
11
+ import { registry } from "./registry";
12
+
13
+ /**
14
+ * Extra context passed to tool handlers
15
+ */
16
+ export type RequestHandlerExtra = {
17
+ /** Authentication data if provided */
18
+ auth?: {
19
+ credentials: Credentials;
20
+ provider: string;
21
+ };
22
+ /** Execution mode: 'headless' for non-interactive, 'interactive' for user interaction (defaults to 'headless') */
23
+ mode: "headless" | "interactive";
24
+ };
25
+
26
+ type ToolOptions<
27
+ TSchema extends Record<string, z.ZodTypeAny>,
28
+ TType extends "block" | "json" = "json",
29
+ > = {
30
+ authRequirements?: {
31
+ provider: string;
32
+ required?: boolean;
33
+ scopeBundle: string;
34
+ };
35
+ description: string;
36
+ inputSchema: TSchema;
37
+ type?: TType;
38
+ };
39
+
40
+ /**
41
+ * Register a function as an Opal tool
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * const greetTool = registerTool('greet', {
46
+ * description: 'Greet a user',
47
+ * inputSchema: {
48
+ * name: z.string().describe('The name to greet')
49
+ * }
50
+ * }, async (params) => {
51
+ * // params is automatically typed as { name: string }
52
+ * return `Hello, ${params.name}!`;
53
+ * });
54
+ * ```
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * // With auth and execution mode
59
+ * const fetchTool = registerTool('fetch_data', {
60
+ * description: 'Fetch data from API',
61
+ * inputSchema: {
62
+ * url: z.string().describe('URL to fetch')
63
+ * },
64
+ * authRequirements: {
65
+ * provider: 'api-service',
66
+ * scopeBundle: 'read'
67
+ * }
68
+ * }, async (params, extra) => {
69
+ * // extra.mode: 'headless' | 'interactive'
70
+ * // extra.auth: { provider, credentials }
71
+ * const headers = extra?.auth ? { Authorization: extra.auth.credentials.token } : {};
72
+ * return fetch(params.url, { headers });
73
+ * });
74
+ * ```
75
+ */
76
+ // Overload for block tools
77
+ export function registerTool<TSchema extends Record<string, z.ZodTypeAny>>(
78
+ name: string,
79
+ options: ToolOptions<TSchema, "block">,
80
+ handler: (
81
+ params: { [K in keyof TSchema]: z.infer<TSchema[K]> },
82
+ extra?: RequestHandlerExtra,
83
+ ) => BlockResponse | Promise<BlockResponse>,
84
+ ): typeof handler;
85
+ // Overload for JSON tools (or when type is omitted)
86
+ export function registerTool<TSchema extends Record<string, z.ZodTypeAny>>(
87
+ name: string,
88
+ options: Omit<ToolOptions<TSchema>, "type"> | ToolOptions<TSchema, "json">,
89
+ handler: (
90
+ params: { [K in keyof TSchema]: z.infer<TSchema[K]> },
91
+ extra?: RequestHandlerExtra,
92
+ ) => Promise<unknown> | unknown,
93
+ ): typeof handler;
94
+ // Implementation
95
+ export function registerTool<TSchema extends Record<string, z.ZodTypeAny>>(
96
+ name: string,
97
+ options: ToolOptions<TSchema, "block" | "json">,
98
+ handler: (
99
+ params: { [K in keyof TSchema]: z.infer<TSchema[K]> },
100
+ extra?: RequestHandlerExtra,
101
+ ) => Promise<unknown> | unknown,
102
+ ): typeof handler {
103
+ // Register the tool with all services
104
+ const responseType = options.type || "json";
105
+ for (const service of registry.services) {
106
+ service.registerTool(
107
+ name,
108
+ options.description,
109
+ handler,
110
+ jsonSchemaToParameters(z.toJSONSchema(z.object(options.inputSchema))),
111
+ `/tools/${name.replace(/_/g, "-")}`,
112
+ options.authRequirements
113
+ ? [
114
+ new AuthRequirement(
115
+ options.authRequirements.provider,
116
+ options.authRequirements.scopeBundle,
117
+ options.authRequirements.required ?? true,
118
+ ),
119
+ ]
120
+ : undefined,
121
+ responseType,
122
+ true,
123
+ );
124
+ }
125
+
126
+ return handler;
127
+ }
128
+
129
+ /**
130
+ * Convert JSON Schema to Parameter definitions
131
+ * This converts the output of z.toJSONSchema() back to the Parameter[] format
132
+ * expected by the legacy discovery endpoint
133
+ */
134
+ function jsonSchemaToParameters(jsonSchema: {
135
+ properties?: Record<string, unknown>;
136
+ required?: string[];
137
+ }): Parameter[] {
138
+ const parameters: Parameter[] = [];
139
+
140
+ if (!jsonSchema.properties) {
141
+ return parameters;
142
+ }
143
+
144
+ const required = jsonSchema.required || [];
145
+
146
+ for (const [key, value] of Object.entries(jsonSchema.properties)) {
147
+ const prop = value as { description?: string; type?: string };
148
+ parameters.push(
149
+ new Parameter(
150
+ key,
151
+ jsonSchemaTypeToParameterType(prop.type || "string"),
152
+ prop.description || "",
153
+ required.includes(key),
154
+ ),
155
+ );
156
+ }
157
+
158
+ return parameters;
159
+ }
160
+
161
+ /**
162
+ * Map JSON Schema type to ParameterType
163
+ */
164
+ function jsonSchemaTypeToParameterType(jsonType: string): ParameterType {
165
+ switch (jsonType) {
166
+ case "array":
167
+ return ParameterType.List;
168
+ case "boolean":
169
+ return ParameterType.Boolean;
170
+ case "integer":
171
+ return ParameterType.Integer;
172
+ case "number":
173
+ return ParameterType.Number;
174
+ case "object":
175
+ return ParameterType.Dictionary;
176
+ case "string":
177
+ return ParameterType.String;
178
+ default:
179
+ return ParameterType.String;
180
+ }
181
+ }