@quilltap/plugin-utils 1.0.0 → 1.2.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/CHANGELOG.md CHANGED
@@ -2,6 +2,39 @@
2
2
 
3
3
  All notable changes to @quilltap/plugin-utils will be documented in this file.
4
4
 
5
+ ## [1.2.0] - 2025-12-31
6
+
7
+ ### Added
8
+
9
+ - **Roleplay Template Plugin Utilities**
10
+ - `createRoleplayTemplatePlugin()` - Create roleplay template plugins with full control
11
+ - `createSingleTemplatePlugin()` - Simplified helper for single-template plugins
12
+ - `validateTemplateConfig()` - Validate individual template configurations
13
+ - `validateRoleplayTemplatePlugin()` - Validate complete roleplay template plugins
14
+ - `CreateRoleplayTemplatePluginOptions` - Options interface for full control
15
+ - `CreateSingleTemplatePluginOptions` - Simplified options for single templates
16
+
17
+ - **New Export Path**
18
+ - `@quilltap/plugin-utils/roleplay-templates` - Direct import path for roleplay template utilities
19
+
20
+ ## [1.1.0] - 2025-12-30
21
+
22
+ ### Added
23
+
24
+ - **OpenAI-Compatible Provider Base Class**
25
+ - `OpenAICompatibleProvider` - Reusable base class for OpenAI-compatible LLM providers
26
+ - `OpenAICompatibleProviderConfig` - Configuration interface for customizing providers
27
+ - Supports streaming and non-streaming chat completions
28
+ - Configurable API key requirements (`requireApiKey` option)
29
+ - Customizable provider name for logging (`providerName` option)
30
+ - Customizable attachment error messages (`attachmentErrorMessage` option)
31
+ - Includes API key validation and model listing
32
+ - Uses the plugin logger bridge for consistent logging
33
+ - Peer dependency on `openai` package (optional, only needed if using this provider)
34
+
35
+ - **New Export Path**
36
+ - `@quilltap/plugin-utils/providers` - Direct import path for provider base classes
37
+
5
38
  ## [1.0.0] - 2025-12-30
6
39
 
7
40
  ### Added
package/README.md CHANGED
@@ -110,6 +110,101 @@ When running standalone:
110
110
  | `createConsoleLogger(prefix, minLevel?)` | Create a standalone console logger |
111
111
  | `createNoopLogger()` | Create a no-op logger |
112
112
 
113
+ ### OpenAI-Compatible Provider Base Class
114
+
115
+ Create custom LLM providers for OpenAI-compatible APIs with minimal code:
116
+
117
+ ```typescript
118
+ import { OpenAICompatibleProvider } from '@quilltap/plugin-utils';
119
+
120
+ // Create a provider for any OpenAI-compatible API
121
+ export class MyLLMProvider extends OpenAICompatibleProvider {
122
+ constructor() {
123
+ super({
124
+ baseUrl: 'https://api.my-llm-service.com/v1',
125
+ providerName: 'MyLLM',
126
+ requireApiKey: true,
127
+ attachmentErrorMessage: 'MyLLM does not support file attachments',
128
+ });
129
+ }
130
+ }
131
+ ```
132
+
133
+ This gives you a complete `LLMProvider` implementation with:
134
+ - Streaming and non-streaming chat completions
135
+ - API key validation
136
+ - Model listing
137
+ - Proper error handling and logging
138
+
139
+ **Configuration Options:**
140
+
141
+ | Option | Type | Default | Description |
142
+ |--------|------|---------|-------------|
143
+ | `baseUrl` | `string` | (required) | API endpoint URL with version path |
144
+ | `providerName` | `string` | `'OpenAICompatible'` | Name used in log messages |
145
+ | `requireApiKey` | `boolean` | `false` | Whether API key is mandatory |
146
+ | `attachmentErrorMessage` | `string` | (default message) | Error shown for attachment failures |
147
+
148
+ **Note:** Requires `openai` as a peer dependency:
149
+ ```bash
150
+ npm install openai
151
+ ```
152
+
153
+ ### Roleplay Template Plugin Utilities
154
+
155
+ Create roleplay template plugins with built-in validation and logging:
156
+
157
+ ```typescript
158
+ import { createSingleTemplatePlugin } from '@quilltap/plugin-utils';
159
+
160
+ // Simple single-template plugin
161
+ export const plugin = createSingleTemplatePlugin({
162
+ templateId: 'my-rp-format',
163
+ displayName: 'My RP Format',
164
+ description: 'A custom roleplay formatting style',
165
+ systemPrompt: `[FORMATTING INSTRUCTIONS]
166
+ 1. DIALOGUE: Use quotation marks
167
+ 2. ACTIONS: Use asterisks *like this*
168
+ 3. THOUGHTS: Use angle brackets <like this>`,
169
+ tags: ['custom', 'roleplay'],
170
+ enableLogging: true,
171
+ });
172
+ ```
173
+
174
+ For plugins providing multiple templates:
175
+
176
+ ```typescript
177
+ import { createRoleplayTemplatePlugin } from '@quilltap/plugin-utils';
178
+
179
+ export const plugin = createRoleplayTemplatePlugin({
180
+ metadata: {
181
+ templateId: 'rp-format-pack',
182
+ displayName: 'RP Format Pack',
183
+ description: 'A collection of roleplay formats',
184
+ },
185
+ templates: [
186
+ {
187
+ name: 'Screenplay',
188
+ description: 'Screenplay-style formatting',
189
+ systemPrompt: '...',
190
+ },
191
+ {
192
+ name: 'Novel',
193
+ description: 'Novel-style prose',
194
+ systemPrompt: '...',
195
+ },
196
+ ],
197
+ enableLogging: true,
198
+ });
199
+ ```
200
+
201
+ | Function | Description |
202
+ |----------|-------------|
203
+ | `createRoleplayTemplatePlugin(options)` | Create a plugin with full control over metadata and templates |
204
+ | `createSingleTemplatePlugin(options)` | Simplified helper for plugins with a single template |
205
+ | `validateTemplateConfig(template)` | Validate an individual template configuration |
206
+ | `validateRoleplayTemplatePlugin(plugin)` | Validate a complete roleplay template plugin |
207
+
113
208
  ## Example: Complete Plugin Provider
114
209
 
115
210
  ```typescript
package/dist/index.d.mts CHANGED
@@ -1,6 +1,8 @@
1
1
  export { AnthropicToolDefinition, GoogleToolDefinition, LogContext, LogLevel, OpenAIToolDefinition, PluginLogger, ToolCall, ToolCallRequest, ToolFormatOptions, ToolResult, UniversalTool, createConsoleLogger, createNoopLogger } from '@quilltap/plugin-types';
2
2
  export { ToolCallFormat, ToolConvertTarget, applyDescriptionLimit, convertFromAnthropicFormat, convertFromGoogleFormat, convertToAnthropicFormat, convertToGoogleFormat, convertToolTo, convertToolsTo, detectToolCallFormat, hasToolCalls, parseAnthropicToolCalls, parseGoogleToolCalls, parseOpenAIToolCalls, parseToolCalls } from './tools/index.mjs';
3
3
  export { PluginLoggerWithChild, __clearCoreLoggerFactory, __injectCoreLoggerFactory, createPluginLogger, getLogLevelFromEnv, hasCoreLogger } from './logging/index.mjs';
4
+ export { OpenAICompatibleProvider, OpenAICompatibleProviderConfig } from './providers/index.mjs';
5
+ export { CreateRoleplayTemplatePluginOptions, CreateSingleTemplatePluginOptions, createRoleplayTemplatePlugin, createSingleTemplatePlugin, validateRoleplayTemplatePlugin, validateTemplateConfig } from './roleplay-templates/index.mjs';
4
6
 
5
7
  /**
6
8
  * @quilltap/plugin-utils
@@ -22,6 +24,6 @@ export { PluginLoggerWithChild, __clearCoreLoggerFactory, __injectCoreLoggerFact
22
24
  * Version of the plugin-utils package.
23
25
  * Can be used at runtime to check compatibility.
24
26
  */
25
- declare const PLUGIN_UTILS_VERSION = "1.0.0";
27
+ declare const PLUGIN_UTILS_VERSION = "1.2.0";
26
28
 
27
29
  export { PLUGIN_UTILS_VERSION };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export { AnthropicToolDefinition, GoogleToolDefinition, LogContext, LogLevel, OpenAIToolDefinition, PluginLogger, ToolCall, ToolCallRequest, ToolFormatOptions, ToolResult, UniversalTool, createConsoleLogger, createNoopLogger } from '@quilltap/plugin-types';
2
2
  export { ToolCallFormat, ToolConvertTarget, applyDescriptionLimit, convertFromAnthropicFormat, convertFromGoogleFormat, convertToAnthropicFormat, convertToGoogleFormat, convertToolTo, convertToolsTo, detectToolCallFormat, hasToolCalls, parseAnthropicToolCalls, parseGoogleToolCalls, parseOpenAIToolCalls, parseToolCalls } from './tools/index.js';
3
3
  export { PluginLoggerWithChild, __clearCoreLoggerFactory, __injectCoreLoggerFactory, createPluginLogger, getLogLevelFromEnv, hasCoreLogger } from './logging/index.js';
4
+ export { OpenAICompatibleProvider, OpenAICompatibleProviderConfig } from './providers/index.js';
5
+ export { CreateRoleplayTemplatePluginOptions, CreateSingleTemplatePluginOptions, createRoleplayTemplatePlugin, createSingleTemplatePlugin, validateRoleplayTemplatePlugin, validateTemplateConfig } from './roleplay-templates/index.js';
4
6
 
5
7
  /**
6
8
  * @quilltap/plugin-utils
@@ -22,6 +24,6 @@ export { PluginLoggerWithChild, __clearCoreLoggerFactory, __injectCoreLoggerFact
22
24
  * Version of the plugin-utils package.
23
25
  * Can be used at runtime to check compatibility.
24
26
  */
25
- declare const PLUGIN_UTILS_VERSION = "1.0.0";
27
+ declare const PLUGIN_UTILS_VERSION = "1.2.0";
26
28
 
27
29
  export { PLUGIN_UTILS_VERSION };
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,11 +17,20 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
21
31
  var src_exports = {};
22
32
  __export(src_exports, {
33
+ OpenAICompatibleProvider: () => OpenAICompatibleProvider,
23
34
  PLUGIN_UTILS_VERSION: () => PLUGIN_UTILS_VERSION,
24
35
  __clearCoreLoggerFactory: () => __clearCoreLoggerFactory,
25
36
  __injectCoreLoggerFactory: () => __injectCoreLoggerFactory,
@@ -33,6 +44,8 @@ __export(src_exports, {
33
44
  createConsoleLogger: () => import_plugin_types.createConsoleLogger,
34
45
  createNoopLogger: () => import_plugin_types.createNoopLogger,
35
46
  createPluginLogger: () => createPluginLogger,
47
+ createRoleplayTemplatePlugin: () => createRoleplayTemplatePlugin,
48
+ createSingleTemplatePlugin: () => createSingleTemplatePlugin,
36
49
  detectToolCallFormat: () => detectToolCallFormat,
37
50
  getLogLevelFromEnv: () => getLogLevelFromEnv,
38
51
  hasCoreLogger: () => hasCoreLogger,
@@ -40,7 +53,9 @@ __export(src_exports, {
40
53
  parseAnthropicToolCalls: () => parseAnthropicToolCalls,
41
54
  parseGoogleToolCalls: () => parseGoogleToolCalls,
42
55
  parseOpenAIToolCalls: () => parseOpenAIToolCalls,
43
- parseToolCalls: () => parseToolCalls
56
+ parseToolCalls: () => parseToolCalls,
57
+ validateRoleplayTemplatePlugin: () => validateRoleplayTemplatePlugin,
58
+ validateTemplateConfig: () => validateTemplateConfig
44
59
  });
45
60
  module.exports = __toCommonJS(src_exports);
46
61
 
@@ -331,10 +346,419 @@ function getLogLevelFromEnv() {
331
346
  // src/logging/index.ts
332
347
  var import_plugin_types = require("@quilltap/plugin-types");
333
348
 
349
+ // src/providers/openai-compatible.ts
350
+ var import_openai = __toESM(require("openai"));
351
+ var OpenAICompatibleProvider = class {
352
+ /**
353
+ * Creates a new OpenAI-compatible provider instance.
354
+ *
355
+ * @param config - Configuration object or base URL string (for backward compatibility)
356
+ */
357
+ constructor(config) {
358
+ /** File attachments are not supported by default */
359
+ this.supportsFileAttachments = false;
360
+ /** No MIME types are supported for attachments */
361
+ this.supportedMimeTypes = [];
362
+ /** Image generation is not supported by default */
363
+ this.supportsImageGeneration = false;
364
+ /** Web search is not supported */
365
+ this.supportsWebSearch = false;
366
+ if (typeof config === "string") {
367
+ this.baseUrl = config;
368
+ this.providerName = "OpenAICompatible";
369
+ this.requireApiKey = false;
370
+ this.attachmentErrorMessage = "OpenAI-compatible provider file attachment support varies by implementation (not yet implemented)";
371
+ } else {
372
+ this.baseUrl = config.baseUrl;
373
+ this.providerName = config.providerName ?? "OpenAICompatible";
374
+ this.requireApiKey = config.requireApiKey ?? false;
375
+ this.attachmentErrorMessage = config.attachmentErrorMessage ?? "OpenAI-compatible provider file attachment support varies by implementation (not yet implemented)";
376
+ }
377
+ this.logger = createPluginLogger(`${this.providerName}Provider`);
378
+ this.logger.debug(`${this.providerName} provider instantiated`, {
379
+ context: `${this.providerName}Provider.constructor`,
380
+ baseUrl: this.baseUrl
381
+ });
382
+ }
383
+ /**
384
+ * Collects attachment failures for messages with attachments.
385
+ * Since attachments are not supported, all attachments are marked as failed.
386
+ *
387
+ * @param params - LLM parameters containing messages
388
+ * @returns Object with empty sent array and failed attachments
389
+ */
390
+ collectAttachmentFailures(params) {
391
+ const failed = [];
392
+ for (const msg of params.messages) {
393
+ if (msg.attachments) {
394
+ for (const attachment of msg.attachments) {
395
+ failed.push({
396
+ id: attachment.id,
397
+ error: this.attachmentErrorMessage
398
+ });
399
+ }
400
+ }
401
+ }
402
+ return { sent: [], failed };
403
+ }
404
+ /**
405
+ * Validates that an API key is provided when required.
406
+ * @throws Error if API key is required but not provided
407
+ */
408
+ validateApiKeyRequirement(apiKey) {
409
+ if (this.requireApiKey && !apiKey) {
410
+ throw new Error(`${this.providerName} provider requires an API key`);
411
+ }
412
+ }
413
+ /**
414
+ * Gets the effective API key to use for requests.
415
+ * Returns 'not-needed' for providers that don't require keys.
416
+ */
417
+ getEffectiveApiKey(apiKey) {
418
+ return this.requireApiKey ? apiKey : apiKey || "not-needed";
419
+ }
420
+ /**
421
+ * Sends a message and returns the complete response.
422
+ *
423
+ * @param params - LLM parameters including messages, model, and settings
424
+ * @param apiKey - API key for authentication
425
+ * @returns Complete LLM response with content and usage statistics
426
+ */
427
+ async sendMessage(params, apiKey) {
428
+ this.logger.debug(`${this.providerName} sendMessage called`, {
429
+ context: `${this.providerName}Provider.sendMessage`,
430
+ model: params.model,
431
+ baseUrl: this.baseUrl
432
+ });
433
+ this.validateApiKeyRequirement(apiKey);
434
+ const attachmentResults = this.collectAttachmentFailures(params);
435
+ const client = new import_openai.default({
436
+ apiKey: this.getEffectiveApiKey(apiKey),
437
+ baseURL: this.baseUrl
438
+ });
439
+ const messages = params.messages.filter((m) => m.role !== "tool").map((m) => ({
440
+ role: m.role,
441
+ content: m.content
442
+ }));
443
+ try {
444
+ const response = await client.chat.completions.create({
445
+ model: params.model,
446
+ messages,
447
+ temperature: params.temperature ?? 0.7,
448
+ max_tokens: params.maxTokens ?? 4096,
449
+ top_p: params.topP ?? 1,
450
+ stop: params.stop
451
+ });
452
+ const choice = response.choices[0];
453
+ this.logger.debug(`Received ${this.providerName} response`, {
454
+ context: `${this.providerName}Provider.sendMessage`,
455
+ finishReason: choice.finish_reason,
456
+ promptTokens: response.usage?.prompt_tokens,
457
+ completionTokens: response.usage?.completion_tokens
458
+ });
459
+ return {
460
+ content: choice.message.content ?? "",
461
+ finishReason: choice.finish_reason,
462
+ usage: {
463
+ promptTokens: response.usage?.prompt_tokens ?? 0,
464
+ completionTokens: response.usage?.completion_tokens ?? 0,
465
+ totalTokens: response.usage?.total_tokens ?? 0
466
+ },
467
+ raw: response,
468
+ attachmentResults
469
+ };
470
+ } catch (error) {
471
+ this.logger.error(
472
+ `${this.providerName} API error in sendMessage`,
473
+ { context: `${this.providerName}Provider.sendMessage`, baseUrl: this.baseUrl },
474
+ error instanceof Error ? error : void 0
475
+ );
476
+ throw error;
477
+ }
478
+ }
479
+ /**
480
+ * Sends a message and streams the response.
481
+ *
482
+ * @param params - LLM parameters including messages, model, and settings
483
+ * @param apiKey - API key for authentication
484
+ * @yields Stream chunks with content and final usage statistics
485
+ */
486
+ async *streamMessage(params, apiKey) {
487
+ this.logger.debug(`${this.providerName} streamMessage called`, {
488
+ context: `${this.providerName}Provider.streamMessage`,
489
+ model: params.model,
490
+ baseUrl: this.baseUrl
491
+ });
492
+ this.validateApiKeyRequirement(apiKey);
493
+ const attachmentResults = this.collectAttachmentFailures(params);
494
+ const client = new import_openai.default({
495
+ apiKey: this.getEffectiveApiKey(apiKey),
496
+ baseURL: this.baseUrl
497
+ });
498
+ const messages = params.messages.filter((m) => m.role !== "tool").map((m) => ({
499
+ role: m.role,
500
+ content: m.content
501
+ }));
502
+ try {
503
+ const stream = await client.chat.completions.create({
504
+ model: params.model,
505
+ messages,
506
+ temperature: params.temperature ?? 0.7,
507
+ max_tokens: params.maxTokens ?? 4096,
508
+ top_p: params.topP ?? 1,
509
+ stream: true,
510
+ stream_options: { include_usage: true }
511
+ });
512
+ let chunkCount = 0;
513
+ for await (const chunk of stream) {
514
+ chunkCount++;
515
+ const content = chunk.choices[0]?.delta?.content;
516
+ const finishReason = chunk.choices[0]?.finish_reason;
517
+ const hasUsage = chunk.usage;
518
+ if (content && !(finishReason && hasUsage)) {
519
+ yield {
520
+ content,
521
+ done: false
522
+ };
523
+ }
524
+ if (finishReason && hasUsage) {
525
+ this.logger.debug("Stream completed", {
526
+ context: `${this.providerName}Provider.streamMessage`,
527
+ finishReason,
528
+ chunks: chunkCount,
529
+ promptTokens: chunk.usage?.prompt_tokens,
530
+ completionTokens: chunk.usage?.completion_tokens
531
+ });
532
+ yield {
533
+ content: "",
534
+ done: true,
535
+ usage: {
536
+ promptTokens: chunk.usage?.prompt_tokens ?? 0,
537
+ completionTokens: chunk.usage?.completion_tokens ?? 0,
538
+ totalTokens: chunk.usage?.total_tokens ?? 0
539
+ },
540
+ attachmentResults
541
+ };
542
+ }
543
+ }
544
+ } catch (error) {
545
+ this.logger.error(
546
+ `${this.providerName} API error in streamMessage`,
547
+ { context: `${this.providerName}Provider.streamMessage`, baseUrl: this.baseUrl },
548
+ error instanceof Error ? error : void 0
549
+ );
550
+ throw error;
551
+ }
552
+ }
553
+ /**
554
+ * Validates an API key by attempting to list models.
555
+ *
556
+ * @param apiKey - API key to validate
557
+ * @returns true if the API key is valid, false otherwise
558
+ */
559
+ async validateApiKey(apiKey) {
560
+ this.logger.debug(`Validating ${this.providerName} API connection`, {
561
+ context: `${this.providerName}Provider.validateApiKey`,
562
+ baseUrl: this.baseUrl
563
+ });
564
+ if (this.requireApiKey && !apiKey) {
565
+ return false;
566
+ }
567
+ try {
568
+ const client = new import_openai.default({
569
+ apiKey: this.getEffectiveApiKey(apiKey),
570
+ baseURL: this.baseUrl
571
+ });
572
+ await client.models.list();
573
+ this.logger.debug(`${this.providerName} API validation successful`, {
574
+ context: `${this.providerName}Provider.validateApiKey`
575
+ });
576
+ return true;
577
+ } catch (error) {
578
+ this.logger.error(
579
+ `${this.providerName} API validation failed`,
580
+ { context: `${this.providerName}Provider.validateApiKey`, baseUrl: this.baseUrl },
581
+ error instanceof Error ? error : void 0
582
+ );
583
+ return false;
584
+ }
585
+ }
586
+ /**
587
+ * Fetches available models from the API.
588
+ *
589
+ * @param apiKey - API key for authentication
590
+ * @returns Sorted array of model IDs, or empty array on failure
591
+ */
592
+ async getAvailableModels(apiKey) {
593
+ this.logger.debug(`Fetching ${this.providerName} models`, {
594
+ context: `${this.providerName}Provider.getAvailableModels`,
595
+ baseUrl: this.baseUrl
596
+ });
597
+ if (this.requireApiKey && !apiKey) {
598
+ this.logger.error(`${this.providerName} provider requires an API key to fetch models`, {
599
+ context: `${this.providerName}Provider.getAvailableModels`
600
+ });
601
+ return [];
602
+ }
603
+ try {
604
+ const client = new import_openai.default({
605
+ apiKey: this.getEffectiveApiKey(apiKey),
606
+ baseURL: this.baseUrl
607
+ });
608
+ const models = await client.models.list();
609
+ const modelList = models.data.map((m) => m.id).sort();
610
+ this.logger.debug(`Retrieved ${this.providerName} models`, {
611
+ context: `${this.providerName}Provider.getAvailableModels`,
612
+ modelCount: modelList.length
613
+ });
614
+ return modelList;
615
+ } catch (error) {
616
+ this.logger.error(
617
+ `Failed to fetch ${this.providerName} models`,
618
+ { context: `${this.providerName}Provider.getAvailableModels`, baseUrl: this.baseUrl },
619
+ error instanceof Error ? error : void 0
620
+ );
621
+ return [];
622
+ }
623
+ }
624
+ /**
625
+ * Image generation is not supported by default.
626
+ * @throws Error indicating image generation is not supported
627
+ */
628
+ async generateImage(_params, _apiKey) {
629
+ throw new Error(
630
+ `${this.providerName} image generation support varies by implementation (not yet implemented)`
631
+ );
632
+ }
633
+ };
634
+
635
+ // src/roleplay-templates/builder.ts
636
+ function createRoleplayTemplatePlugin(options) {
637
+ const { metadata, templates, initialize, enableLogging = false } = options;
638
+ const templateArray = Array.isArray(templates) ? templates : [templates];
639
+ if (templateArray.length === 0) {
640
+ throw new Error("At least one template is required");
641
+ }
642
+ for (const template of templateArray) {
643
+ if (!template.name || template.name.trim() === "") {
644
+ throw new Error("Template name is required");
645
+ }
646
+ if (!template.systemPrompt || template.systemPrompt.trim() === "") {
647
+ throw new Error(`Template "${template.name}" requires a systemPrompt`);
648
+ }
649
+ }
650
+ const plugin = {
651
+ metadata: {
652
+ ...metadata,
653
+ // Ensure tags from templates are included in metadata if not already set
654
+ tags: metadata.tags ?? Array.from(
655
+ new Set(templateArray.flatMap((t) => t.tags ?? []))
656
+ )
657
+ },
658
+ templates: templateArray
659
+ };
660
+ if (initialize || enableLogging) {
661
+ plugin.initialize = async () => {
662
+ if (enableLogging) {
663
+ const logger = createPluginLogger(metadata.templateId);
664
+ logger.debug("Roleplay template plugin loaded", {
665
+ context: "init",
666
+ templateId: metadata.templateId,
667
+ displayName: metadata.displayName,
668
+ templateCount: templateArray.length,
669
+ templateNames: templateArray.map((t) => t.name)
670
+ });
671
+ }
672
+ if (initialize) {
673
+ await initialize();
674
+ }
675
+ };
676
+ }
677
+ return plugin;
678
+ }
679
+ function createSingleTemplatePlugin(options) {
680
+ const {
681
+ templateId,
682
+ displayName,
683
+ description,
684
+ systemPrompt,
685
+ author,
686
+ tags,
687
+ version,
688
+ initialize,
689
+ enableLogging
690
+ } = options;
691
+ return createRoleplayTemplatePlugin({
692
+ metadata: {
693
+ templateId,
694
+ displayName,
695
+ description,
696
+ author,
697
+ tags,
698
+ version
699
+ },
700
+ templates: {
701
+ name: displayName,
702
+ description,
703
+ systemPrompt,
704
+ tags
705
+ },
706
+ initialize,
707
+ enableLogging
708
+ });
709
+ }
710
+ function validateTemplateConfig(template) {
711
+ if (!template.name || template.name.trim() === "") {
712
+ throw new Error("Template name is required");
713
+ }
714
+ if (template.name.length > 100) {
715
+ throw new Error("Template name must be 100 characters or less");
716
+ }
717
+ if (!template.systemPrompt || template.systemPrompt.trim() === "") {
718
+ throw new Error("Template systemPrompt is required");
719
+ }
720
+ if (template.description && template.description.length > 500) {
721
+ throw new Error("Template description must be 500 characters or less");
722
+ }
723
+ if (template.tags) {
724
+ if (!Array.isArray(template.tags)) {
725
+ throw new Error("Template tags must be an array");
726
+ }
727
+ for (const tag of template.tags) {
728
+ if (typeof tag !== "string") {
729
+ throw new Error("All tags must be strings");
730
+ }
731
+ }
732
+ }
733
+ return true;
734
+ }
735
+ function validateRoleplayTemplatePlugin(plugin) {
736
+ if (!plugin.metadata) {
737
+ throw new Error("Plugin metadata is required");
738
+ }
739
+ if (!plugin.metadata.templateId || plugin.metadata.templateId.trim() === "") {
740
+ throw new Error("Plugin metadata.templateId is required");
741
+ }
742
+ if (!/^[a-z0-9-]+$/.test(plugin.metadata.templateId)) {
743
+ throw new Error("Plugin templateId must be lowercase alphanumeric with hyphens only");
744
+ }
745
+ if (!plugin.metadata.displayName || plugin.metadata.displayName.trim() === "") {
746
+ throw new Error("Plugin metadata.displayName is required");
747
+ }
748
+ if (!plugin.templates || !Array.isArray(plugin.templates) || plugin.templates.length === 0) {
749
+ throw new Error("Plugin must have at least one template");
750
+ }
751
+ for (const template of plugin.templates) {
752
+ validateTemplateConfig(template);
753
+ }
754
+ return true;
755
+ }
756
+
334
757
  // src/index.ts
335
- var PLUGIN_UTILS_VERSION = "1.0.0";
758
+ var PLUGIN_UTILS_VERSION = "1.2.0";
336
759
  // Annotate the CommonJS export names for ESM import in node:
337
760
  0 && (module.exports = {
761
+ OpenAICompatibleProvider,
338
762
  PLUGIN_UTILS_VERSION,
339
763
  __clearCoreLoggerFactory,
340
764
  __injectCoreLoggerFactory,
@@ -348,6 +772,8 @@ var PLUGIN_UTILS_VERSION = "1.0.0";
348
772
  createConsoleLogger,
349
773
  createNoopLogger,
350
774
  createPluginLogger,
775
+ createRoleplayTemplatePlugin,
776
+ createSingleTemplatePlugin,
351
777
  detectToolCallFormat,
352
778
  getLogLevelFromEnv,
353
779
  hasCoreLogger,
@@ -355,6 +781,8 @@ var PLUGIN_UTILS_VERSION = "1.0.0";
355
781
  parseAnthropicToolCalls,
356
782
  parseGoogleToolCalls,
357
783
  parseOpenAIToolCalls,
358
- parseToolCalls
784
+ parseToolCalls,
785
+ validateRoleplayTemplatePlugin,
786
+ validateTemplateConfig
359
787
  });
360
788
  //# sourceMappingURL=index.js.map