signalwire-agents 0.1.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.
Files changed (245) hide show
  1. package/CLAUDE.md +112 -0
  2. package/README.md +417 -0
  3. package/dist/AgentBase.d.ts +481 -0
  4. package/dist/AgentBase.js +1155 -0
  5. package/dist/AgentBase.js.map +1 -0
  6. package/dist/AgentServer.d.ts +60 -0
  7. package/dist/AgentServer.js +128 -0
  8. package/dist/AgentServer.js.map +1 -0
  9. package/dist/AuthHandler.d.ts +56 -0
  10. package/dist/AuthHandler.js +119 -0
  11. package/dist/AuthHandler.js.map +1 -0
  12. package/dist/ConfigLoader.d.ts +68 -0
  13. package/dist/ConfigLoader.js +150 -0
  14. package/dist/ConfigLoader.js.map +1 -0
  15. package/dist/ContextBuilder.d.ts +431 -0
  16. package/dist/ContextBuilder.js +751 -0
  17. package/dist/ContextBuilder.js.map +1 -0
  18. package/dist/DataMap.d.ts +179 -0
  19. package/dist/DataMap.js +321 -0
  20. package/dist/DataMap.js.map +1 -0
  21. package/dist/Logger.d.ts +91 -0
  22. package/dist/Logger.js +191 -0
  23. package/dist/Logger.js.map +1 -0
  24. package/dist/PomBuilder.d.ts +144 -0
  25. package/dist/PomBuilder.js +249 -0
  26. package/dist/PomBuilder.js.map +1 -0
  27. package/dist/PromptManager.d.ts +85 -0
  28. package/dist/PromptManager.js +108 -0
  29. package/dist/PromptManager.js.map +1 -0
  30. package/dist/SchemaUtils.d.ts +42 -0
  31. package/dist/SchemaUtils.js +148 -0
  32. package/dist/SchemaUtils.js.map +1 -0
  33. package/dist/ServerlessAdapter.d.ts +100 -0
  34. package/dist/ServerlessAdapter.js +166 -0
  35. package/dist/ServerlessAdapter.js.map +1 -0
  36. package/dist/SessionManager.d.ts +86 -0
  37. package/dist/SessionManager.js +155 -0
  38. package/dist/SessionManager.js.map +1 -0
  39. package/dist/SslConfig.d.ts +77 -0
  40. package/dist/SslConfig.js +100 -0
  41. package/dist/SslConfig.js.map +1 -0
  42. package/dist/SwaigFunction.d.ts +88 -0
  43. package/dist/SwaigFunction.js +125 -0
  44. package/dist/SwaigFunction.js.map +1 -0
  45. package/dist/SwaigFunctionResult.d.ts +442 -0
  46. package/dist/SwaigFunctionResult.js +735 -0
  47. package/dist/SwaigFunctionResult.js.map +1 -0
  48. package/dist/SwmlBuilder.d.ts +37 -0
  49. package/dist/SwmlBuilder.js +55 -0
  50. package/dist/SwmlBuilder.js.map +1 -0
  51. package/dist/cli/agent-loader.d.ts +22 -0
  52. package/dist/cli/agent-loader.js +106 -0
  53. package/dist/cli/agent-loader.js.map +1 -0
  54. package/dist/cli/mock-data.d.ts +37 -0
  55. package/dist/cli/mock-data.js +79 -0
  56. package/dist/cli/mock-data.js.map +1 -0
  57. package/dist/cli/swaig-test.d.ts +32 -0
  58. package/dist/cli/swaig-test.js +405 -0
  59. package/dist/cli/swaig-test.js.map +1 -0
  60. package/dist/index.d.ts +36 -0
  61. package/dist/index.js +44 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/prefabs/ConciergeAgent.d.ts +64 -0
  64. package/dist/prefabs/ConciergeAgent.js +188 -0
  65. package/dist/prefabs/ConciergeAgent.js.map +1 -0
  66. package/dist/prefabs/FAQBotAgent.d.ts +68 -0
  67. package/dist/prefabs/FAQBotAgent.js +206 -0
  68. package/dist/prefabs/FAQBotAgent.js.map +1 -0
  69. package/dist/prefabs/InfoGathererAgent.d.ts +73 -0
  70. package/dist/prefabs/InfoGathererAgent.js +202 -0
  71. package/dist/prefabs/InfoGathererAgent.js.map +1 -0
  72. package/dist/prefabs/ReceptionistAgent.d.ts +65 -0
  73. package/dist/prefabs/ReceptionistAgent.js +205 -0
  74. package/dist/prefabs/ReceptionistAgent.js.map +1 -0
  75. package/dist/prefabs/SurveyAgent.d.ts +84 -0
  76. package/dist/prefabs/SurveyAgent.js +324 -0
  77. package/dist/prefabs/SurveyAgent.js.map +1 -0
  78. package/dist/prefabs/index.d.ts +13 -0
  79. package/dist/prefabs/index.js +14 -0
  80. package/dist/prefabs/index.js.map +1 -0
  81. package/dist/skills/SkillBase.d.ts +134 -0
  82. package/dist/skills/SkillBase.js +105 -0
  83. package/dist/skills/SkillBase.js.map +1 -0
  84. package/dist/skills/SkillManager.d.ts +83 -0
  85. package/dist/skills/SkillManager.js +159 -0
  86. package/dist/skills/SkillManager.js.map +1 -0
  87. package/dist/skills/SkillRegistry.d.ts +105 -0
  88. package/dist/skills/SkillRegistry.js +195 -0
  89. package/dist/skills/SkillRegistry.js.map +1 -0
  90. package/dist/skills/builtin/api_ninjas_trivia.d.ts +35 -0
  91. package/dist/skills/builtin/api_ninjas_trivia.js +161 -0
  92. package/dist/skills/builtin/api_ninjas_trivia.js.map +1 -0
  93. package/dist/skills/builtin/claude_skills.d.ts +34 -0
  94. package/dist/skills/builtin/claude_skills.js +156 -0
  95. package/dist/skills/builtin/claude_skills.js.map +1 -0
  96. package/dist/skills/builtin/custom_skills.d.ts +59 -0
  97. package/dist/skills/builtin/custom_skills.js +231 -0
  98. package/dist/skills/builtin/custom_skills.js.map +1 -0
  99. package/dist/skills/builtin/datasphere.d.ts +34 -0
  100. package/dist/skills/builtin/datasphere.js +163 -0
  101. package/dist/skills/builtin/datasphere.js.map +1 -0
  102. package/dist/skills/builtin/datasphere_serverless.d.ts +54 -0
  103. package/dist/skills/builtin/datasphere_serverless.js +155 -0
  104. package/dist/skills/builtin/datasphere_serverless.js.map +1 -0
  105. package/dist/skills/builtin/datetime.d.ts +31 -0
  106. package/dist/skills/builtin/datetime.js +95 -0
  107. package/dist/skills/builtin/datetime.js.map +1 -0
  108. package/dist/skills/builtin/google_maps.d.ts +33 -0
  109. package/dist/skills/builtin/google_maps.js +233 -0
  110. package/dist/skills/builtin/google_maps.js.map +1 -0
  111. package/dist/skills/builtin/index.d.ts +28 -0
  112. package/dist/skills/builtin/index.js +75 -0
  113. package/dist/skills/builtin/index.js.map +1 -0
  114. package/dist/skills/builtin/info_gatherer.d.ts +52 -0
  115. package/dist/skills/builtin/info_gatherer.js +237 -0
  116. package/dist/skills/builtin/info_gatherer.js.map +1 -0
  117. package/dist/skills/builtin/joke.d.ts +32 -0
  118. package/dist/skills/builtin/joke.js +142 -0
  119. package/dist/skills/builtin/joke.js.map +1 -0
  120. package/dist/skills/builtin/math.d.ts +33 -0
  121. package/dist/skills/builtin/math.js +120 -0
  122. package/dist/skills/builtin/math.js.map +1 -0
  123. package/dist/skills/builtin/mcp_gateway.d.ts +35 -0
  124. package/dist/skills/builtin/mcp_gateway.js +85 -0
  125. package/dist/skills/builtin/mcp_gateway.js.map +1 -0
  126. package/dist/skills/builtin/native_vector_search.d.ts +44 -0
  127. package/dist/skills/builtin/native_vector_search.js +248 -0
  128. package/dist/skills/builtin/native_vector_search.js.map +1 -0
  129. package/dist/skills/builtin/play_background_file.d.ts +34 -0
  130. package/dist/skills/builtin/play_background_file.js +135 -0
  131. package/dist/skills/builtin/play_background_file.js.map +1 -0
  132. package/dist/skills/builtin/spider.d.ts +34 -0
  133. package/dist/skills/builtin/spider.js +164 -0
  134. package/dist/skills/builtin/spider.js.map +1 -0
  135. package/dist/skills/builtin/swml_transfer.d.ts +44 -0
  136. package/dist/skills/builtin/swml_transfer.js +204 -0
  137. package/dist/skills/builtin/swml_transfer.js.map +1 -0
  138. package/dist/skills/builtin/weather_api.d.ts +34 -0
  139. package/dist/skills/builtin/weather_api.js +136 -0
  140. package/dist/skills/builtin/weather_api.js.map +1 -0
  141. package/dist/skills/builtin/web_search.d.ts +34 -0
  142. package/dist/skills/builtin/web_search.js +141 -0
  143. package/dist/skills/builtin/web_search.js.map +1 -0
  144. package/dist/skills/builtin/wikipedia_search.d.ts +34 -0
  145. package/dist/skills/builtin/wikipedia_search.js +166 -0
  146. package/dist/skills/builtin/wikipedia_search.js.map +1 -0
  147. package/dist/skills/index.d.ts +11 -0
  148. package/dist/skills/index.js +10 -0
  149. package/dist/skills/index.js.map +1 -0
  150. package/dist/types.d.ts +85 -0
  151. package/dist/types.js +5 -0
  152. package/dist/types.js.map +1 -0
  153. package/docs/agent-guide.md +869 -0
  154. package/docs/api-reference.md +2419 -0
  155. package/docs/architecture.md +843 -0
  156. package/docs/cli-guide.md +579 -0
  157. package/docs/configuration.md +422 -0
  158. package/docs/contexts-guide.md +787 -0
  159. package/docs/datamap-guide.md +1141 -0
  160. package/docs/prefabs-guide.md +830 -0
  161. package/docs/security.md +436 -0
  162. package/docs/serverless-guide.md +513 -0
  163. package/docs/skills-guide.md +845 -0
  164. package/docs/swaig-reference.md +1627 -0
  165. package/examples/call-flow.ts +60 -0
  166. package/examples/contexts-steps.ts +62 -0
  167. package/examples/datamap-tools.ts +52 -0
  168. package/examples/dynamic-config.ts +51 -0
  169. package/examples/multi-agent.ts +67 -0
  170. package/examples/pom-prompt.ts +68 -0
  171. package/examples/simple-agent.ts +33 -0
  172. package/package.json +35 -0
  173. package/src/AgentBase.ts +1279 -0
  174. package/src/AgentServer.ts +149 -0
  175. package/src/AuthHandler.ts +141 -0
  176. package/src/ConfigLoader.ts +165 -0
  177. package/src/ContextBuilder.ts +796 -0
  178. package/src/DataMap.ts +365 -0
  179. package/src/Logger.ts +197 -0
  180. package/src/PomBuilder.ts +298 -0
  181. package/src/PromptManager.ts +133 -0
  182. package/src/SchemaUtils.ts +170 -0
  183. package/src/ServerlessAdapter.ts +218 -0
  184. package/src/SessionManager.ts +161 -0
  185. package/src/SslConfig.ts +118 -0
  186. package/src/SwaigFunction.ts +168 -0
  187. package/src/SwaigFunctionResult.ts +864 -0
  188. package/src/SwmlBuilder.ts +62 -0
  189. package/src/cli/agent-loader.ts +122 -0
  190. package/src/cli/mock-data.ts +110 -0
  191. package/src/cli/swaig-test.ts +440 -0
  192. package/src/index.ts +99 -0
  193. package/src/prefabs/ConciergeAgent.ts +251 -0
  194. package/src/prefabs/FAQBotAgent.ts +263 -0
  195. package/src/prefabs/InfoGathererAgent.ts +276 -0
  196. package/src/prefabs/ReceptionistAgent.ts +277 -0
  197. package/src/prefabs/SurveyAgent.ts +413 -0
  198. package/src/prefabs/index.ts +23 -0
  199. package/src/skills/SkillBase.ts +184 -0
  200. package/src/skills/SkillManager.ts +177 -0
  201. package/src/skills/SkillRegistry.ts +231 -0
  202. package/src/skills/builtin/api_ninjas_trivia.ts +230 -0
  203. package/src/skills/builtin/claude_skills.ts +239 -0
  204. package/src/skills/builtin/custom_skills.ts +326 -0
  205. package/src/skills/builtin/datasphere.ts +225 -0
  206. package/src/skills/builtin/datasphere_serverless.ts +194 -0
  207. package/src/skills/builtin/datetime.ts +115 -0
  208. package/src/skills/builtin/google_maps.ts +347 -0
  209. package/src/skills/builtin/index.ts +79 -0
  210. package/src/skills/builtin/info_gatherer.ts +331 -0
  211. package/src/skills/builtin/joke.ts +180 -0
  212. package/src/skills/builtin/math.ts +150 -0
  213. package/src/skills/builtin/mcp_gateway.ts +99 -0
  214. package/src/skills/builtin/native_vector_search.ts +327 -0
  215. package/src/skills/builtin/play_background_file.ts +189 -0
  216. package/src/skills/builtin/spider.ts +232 -0
  217. package/src/skills/builtin/swml_transfer.ts +285 -0
  218. package/src/skills/builtin/weather_api.ts +193 -0
  219. package/src/skills/builtin/web_search.ts +212 -0
  220. package/src/skills/builtin/wikipedia_search.ts +248 -0
  221. package/src/skills/index.ts +12 -0
  222. package/src/types.ts +99 -0
  223. package/tests/AgentBase.test.ts +693 -0
  224. package/tests/AgentServer.test.ts +102 -0
  225. package/tests/AuthHandler.test.ts +121 -0
  226. package/tests/ConfigLoader.test.ts +121 -0
  227. package/tests/DataMap.test.ts +286 -0
  228. package/tests/Logger.test.ts +218 -0
  229. package/tests/PomBuilder.test.ts +164 -0
  230. package/tests/ProxyDetection.test.ts +185 -0
  231. package/tests/SchemaUtils.test.ts +106 -0
  232. package/tests/ServerlessAdapter.test.ts +200 -0
  233. package/tests/SessionManager.test.ts +139 -0
  234. package/tests/SslConfig.test.ts +97 -0
  235. package/tests/SwaigFunctionResult.test.ts +298 -0
  236. package/tests/cli/mock-data.test.ts +94 -0
  237. package/tests/cli/swaig-test.test.ts +96 -0
  238. package/tests/prefabs/prefabs.test.ts +854 -0
  239. package/tests/setup.ts +6 -0
  240. package/tests/skills/SkillBase.test.ts +118 -0
  241. package/tests/skills/SkillManager.test.ts +164 -0
  242. package/tests/skills/SkillRegistry.test.ts +124 -0
  243. package/tests/skills/builtin.test.ts +924 -0
  244. package/tsconfig.json +18 -0
  245. package/vitest.config.ts +9 -0
@@ -0,0 +1,177 @@
1
+ /**
2
+ * SkillManager - Manages skill lifecycle for an agent.
3
+ *
4
+ * Handles loading, unloading, validation, and injection of
5
+ * skill tools, hints, global data, and prompt sections.
6
+ */
7
+
8
+ import { SkillBase } from './SkillBase.js';
9
+ import { getLogger } from '../Logger.js';
10
+
11
+ const log = getLogger('SkillManager');
12
+
13
+ /**
14
+ * Manages the lifecycle of skills attached to an agent.
15
+ *
16
+ * Handles loading, unloading, validation, and aggregation of skill tools,
17
+ * hints, global data, and prompt sections.
18
+ */
19
+ export class SkillManager {
20
+ private skills: Map<string, SkillBase> = new Map();
21
+
22
+ /**
23
+ * Add a skill to the manager, validating env vars and calling setup().
24
+ * @param skill - The skill instance to add.
25
+ */
26
+ async addSkill(skill: SkillBase): Promise<void> {
27
+ const name = skill.skillName;
28
+
29
+ if (this.skills.has(skill.instanceId)) {
30
+ throw new Error(`Skill instance '${skill.instanceId}' is already loaded`);
31
+ }
32
+
33
+ // Validate required env vars
34
+ const missingEnvVars = skill.validateEnvVars();
35
+ if (missingEnvVars.length) {
36
+ log.warn(`Skill '${name}' missing env vars: ${missingEnvVars.join(', ')}`);
37
+ }
38
+
39
+ // Setup
40
+ await skill.setup();
41
+ skill.markInitialized();
42
+
43
+ this.skills.set(skill.instanceId, skill);
44
+ log.debug(`Added skill '${name}' (${skill.instanceId})`);
45
+ }
46
+
47
+ /**
48
+ * Remove a skill by its instance ID, calling cleanup() before removal.
49
+ * @param instanceId - The unique instance ID of the skill to remove.
50
+ * @returns True if the skill was found and removed, false otherwise.
51
+ */
52
+ async removeSkill(instanceId: string): Promise<boolean> {
53
+ const skill = this.skills.get(instanceId);
54
+ if (!skill) return false;
55
+
56
+ await skill.cleanup();
57
+ this.skills.delete(instanceId);
58
+ log.debug(`Removed skill '${skill.skillName}' (${instanceId})`);
59
+ return true;
60
+ }
61
+
62
+ /**
63
+ * Remove all skill instances matching a given skill name.
64
+ * @param skillName - The skill name to match against.
65
+ * @returns The number of skill instances removed.
66
+ */
67
+ async removeSkillByName(skillName: string): Promise<number> {
68
+ let count = 0;
69
+ for (const [id, skill] of this.skills) {
70
+ if (skill.skillName === skillName) {
71
+ await skill.cleanup();
72
+ this.skills.delete(id);
73
+ count++;
74
+ }
75
+ }
76
+ return count;
77
+ }
78
+
79
+ /**
80
+ * Check if any skill instance with the given name is currently loaded.
81
+ * @param skillName - The skill name to check for.
82
+ * @returns True if at least one instance with this name is loaded.
83
+ */
84
+ hasSkill(skillName: string): boolean {
85
+ for (const skill of this.skills.values()) {
86
+ if (skill.skillName === skillName) return true;
87
+ }
88
+ return false;
89
+ }
90
+
91
+ /**
92
+ * Get a skill by its unique instance ID.
93
+ * @param instanceId - The instance ID to look up.
94
+ * @returns The skill instance, or undefined if not found.
95
+ */
96
+ getSkill(instanceId: string): SkillBase | undefined {
97
+ return this.skills.get(instanceId);
98
+ }
99
+
100
+ /**
101
+ * List all loaded skill instances with their name, ID, and initialization state.
102
+ * @returns Array of skill summary objects.
103
+ */
104
+ listSkills(): { name: string; instanceId: string; initialized: boolean }[] {
105
+ return Array.from(this.skills.values()).map(s => ({
106
+ name: s.skillName,
107
+ instanceId: s.instanceId,
108
+ initialized: s.isInitialized(),
109
+ }));
110
+ }
111
+
112
+ /**
113
+ * Aggregate tool definitions from all loaded skills.
114
+ * @returns Combined array of all skill tool definitions.
115
+ */
116
+ getAllTools(): ReturnType<SkillBase['getTools']> {
117
+ const tools: ReturnType<SkillBase['getTools']> = [];
118
+ for (const skill of this.skills.values()) {
119
+ tools.push(...skill.getTools());
120
+ }
121
+ return tools;
122
+ }
123
+
124
+ /**
125
+ * Aggregate prompt sections from all loaded skills.
126
+ * @returns Combined array of all skill prompt sections.
127
+ */
128
+ getAllPromptSections(): ReturnType<SkillBase['getPromptSections']> {
129
+ const sections: ReturnType<SkillBase['getPromptSections']> = [];
130
+ for (const skill of this.skills.values()) {
131
+ sections.push(...skill.getPromptSections());
132
+ }
133
+ return sections;
134
+ }
135
+
136
+ /**
137
+ * Aggregate speech recognition hints from all loaded skills.
138
+ * @returns Combined array of all skill hint strings.
139
+ */
140
+ getAllHints(): string[] {
141
+ const hints: string[] = [];
142
+ for (const skill of this.skills.values()) {
143
+ hints.push(...skill.getHints());
144
+ }
145
+ return hints;
146
+ }
147
+
148
+ /**
149
+ * Merge global data from all loaded skills into a single object.
150
+ * @returns Combined global data (later skills override earlier ones on key conflicts).
151
+ */
152
+ getMergedGlobalData(): Record<string, unknown> {
153
+ const data: Record<string, unknown> = {};
154
+ for (const skill of this.skills.values()) {
155
+ Object.assign(data, skill.getGlobalData());
156
+ }
157
+ return data;
158
+ }
159
+
160
+ /**
161
+ * Get the number of currently loaded skill instances.
162
+ * @returns The count of loaded skills.
163
+ */
164
+ get size(): number {
165
+ return this.skills.size;
166
+ }
167
+
168
+ /**
169
+ * Remove all skills and clean up.
170
+ */
171
+ async clear(): Promise<void> {
172
+ for (const skill of this.skills.values()) {
173
+ await skill.cleanup();
174
+ }
175
+ this.skills.clear();
176
+ }
177
+ }
@@ -0,0 +1,231 @@
1
+ /**
2
+ * SkillRegistry - Global singleton registry for discovering and loading skills.
3
+ *
4
+ * Skills can be registered programmatically or discovered from directories.
5
+ * Supports SIGNALWIRE_SKILL_PATHS env var for custom skill directories.
6
+ */
7
+
8
+ import { SkillBase, type SkillConfig, type SkillManifest } from './SkillBase.js';
9
+ import { getLogger } from '../Logger.js';
10
+
11
+ const log = getLogger('SkillRegistry');
12
+
13
+ /** Factory function that creates a SkillBase instance from optional configuration. */
14
+ export type SkillFactory = (config?: SkillConfig) => SkillBase;
15
+
16
+ /** Internal registry entry associating a skill name with its factory and optional manifest. */
17
+ interface RegistryEntry {
18
+ /** Registered skill name. */
19
+ name: string;
20
+ /** Factory function to create instances of this skill. */
21
+ factory: SkillFactory;
22
+ /** Optional pre-loaded manifest for the skill. */
23
+ manifest?: SkillManifest;
24
+ }
25
+
26
+ let _instance: SkillRegistry | null = null;
27
+
28
+ /**
29
+ * Global singleton registry for discovering and instantiating skills.
30
+ *
31
+ * Skills can be registered programmatically or auto-discovered from directories.
32
+ * Supports the SIGNALWIRE_SKILL_PATHS environment variable for custom skill directories.
33
+ */
34
+ export class SkillRegistry {
35
+ private registry: Map<string, RegistryEntry> = new Map();
36
+ private searchPaths: string[];
37
+
38
+ constructor() {
39
+ const envPaths = process.env['SIGNALWIRE_SKILL_PATHS'];
40
+ this.searchPaths = envPaths ? envPaths.split(':').filter(Boolean) : [];
41
+ }
42
+
43
+ /**
44
+ * Get the global singleton instance, creating it on first access.
45
+ * @returns The shared SkillRegistry instance.
46
+ */
47
+ static getInstance(): SkillRegistry {
48
+ if (!_instance) {
49
+ _instance = new SkillRegistry();
50
+ }
51
+ return _instance;
52
+ }
53
+
54
+ /**
55
+ * Reset the global singleton (for testing).
56
+ */
57
+ static resetInstance(): void {
58
+ _instance = null;
59
+ }
60
+
61
+ /**
62
+ * Register a skill factory by name, optionally with a pre-loaded manifest.
63
+ * @param name - The unique skill name.
64
+ * @param factory - Factory function to create skill instances.
65
+ * @param manifest - Optional manifest metadata for the skill.
66
+ */
67
+ register(name: string, factory: SkillFactory, manifest?: SkillManifest): void {
68
+ if (this.registry.has(name)) {
69
+ log.warn(`Overwriting skill registration: ${name}`);
70
+ }
71
+ this.registry.set(name, { name, factory, manifest });
72
+ log.debug(`Registered skill: ${name}`);
73
+ }
74
+
75
+ /**
76
+ * Unregister a skill by name, removing it from the registry.
77
+ * @param name - The skill name to unregister.
78
+ * @returns True if the skill was found and removed.
79
+ */
80
+ unregister(name: string): boolean {
81
+ return this.registry.delete(name);
82
+ }
83
+
84
+ /**
85
+ * Create a new skill instance by looking up its factory in the registry.
86
+ * @param name - The registered skill name.
87
+ * @param config - Optional configuration to pass to the factory.
88
+ * @returns A new skill instance, or null if the name is not registered.
89
+ */
90
+ create(name: string, config?: SkillConfig): SkillBase | null {
91
+ const entry = this.registry.get(name);
92
+ if (!entry) {
93
+ log.warn(`Skill not found in registry: ${name}`);
94
+ return null;
95
+ }
96
+ return entry.factory(config);
97
+ }
98
+
99
+ /**
100
+ * Check if a skill name is registered.
101
+ * @param name - The skill name to check.
102
+ * @returns True if the skill is registered.
103
+ */
104
+ has(name: string): boolean {
105
+ return this.registry.has(name);
106
+ }
107
+
108
+ /**
109
+ * Get the manifest for a registered skill, if available.
110
+ * @param name - The skill name to look up.
111
+ * @returns The skill manifest, or undefined if not found or not provided.
112
+ */
113
+ getManifest(name: string): SkillManifest | undefined {
114
+ return this.registry.get(name)?.manifest;
115
+ }
116
+
117
+ /**
118
+ * List all registered skill names.
119
+ * @returns Array of registered skill name strings.
120
+ */
121
+ listRegistered(): string[] {
122
+ return Array.from(this.registry.keys());
123
+ }
124
+
125
+ /**
126
+ * List all registered skills with their optional manifests.
127
+ * @returns Array of objects containing skill name and manifest.
128
+ */
129
+ listRegisteredWithManifests(): { name: string; manifest?: SkillManifest }[] {
130
+ return Array.from(this.registry.values()).map(e => ({
131
+ name: e.name,
132
+ manifest: e.manifest,
133
+ }));
134
+ }
135
+
136
+ /**
137
+ * Add a directory path to search during skill discovery.
138
+ * @param path - Absolute path to a directory containing skill files.
139
+ */
140
+ addSearchPath(path: string): void {
141
+ if (!this.searchPaths.includes(path)) {
142
+ this.searchPaths.push(path);
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Get all configured search paths for skill discovery.
148
+ * @returns Copy of the search paths array.
149
+ */
150
+ getSearchPaths(): string[] {
151
+ return [...this.searchPaths];
152
+ }
153
+
154
+ /**
155
+ * Discover and register skills from a directory by importing each file.
156
+ * Looks for modules exporting a `createSkill` factory or a SkillBase default export.
157
+ * @param dirPath - Absolute path to the directory to scan.
158
+ * @returns Array of newly discovered skill names.
159
+ */
160
+ async discoverFromDirectory(dirPath: string): Promise<string[]> {
161
+ const { readdir } = await import('node:fs/promises');
162
+ const { join } = await import('node:path');
163
+ const { pathToFileURL } = await import('node:url');
164
+
165
+ const discovered: string[] = [];
166
+ let entries: string[];
167
+
168
+ try {
169
+ const dirEntries = await readdir(dirPath, { withFileTypes: true });
170
+ entries = dirEntries
171
+ .filter(e => (e.isFile() && (e.name.endsWith('.ts') || e.name.endsWith('.js'))) || e.isDirectory())
172
+ .map(e => e.name);
173
+ } catch {
174
+ log.warn(`Cannot read skill directory: ${dirPath}`);
175
+ return discovered;
176
+ }
177
+
178
+ for (const entry of entries) {
179
+ const fullPath = join(dirPath, entry);
180
+ try {
181
+ const fileUrl = pathToFileURL(
182
+ entry.endsWith('.ts') || entry.endsWith('.js') ? fullPath : join(fullPath, 'skill.ts'),
183
+ ).href;
184
+ const mod = await import(fileUrl);
185
+
186
+ // Look for a factory function or a SkillBase subclass
187
+ if (typeof mod.createSkill === 'function') {
188
+ const name = entry.replace(/\.(ts|js)$/, '');
189
+ this.register(name, mod.createSkill);
190
+ discovered.push(name);
191
+ } else if (typeof mod.default === 'function' && mod.default.prototype instanceof SkillBase) {
192
+ const name = entry.replace(/\.(ts|js)$/, '');
193
+ this.register(name, (config) => new mod.default(config));
194
+ discovered.push(name);
195
+ }
196
+ } catch (err) {
197
+ log.debug(`Could not load skill from ${fullPath}: ${err}`);
198
+ }
199
+ }
200
+
201
+ return discovered;
202
+ }
203
+
204
+ /**
205
+ * Discover and register skills from all configured search paths.
206
+ * @returns Array of all newly discovered skill names.
207
+ */
208
+ async discoverAll(): Promise<string[]> {
209
+ const all: string[] = [];
210
+ for (const path of this.searchPaths) {
211
+ const found = await this.discoverFromDirectory(path);
212
+ all.push(...found);
213
+ }
214
+ return all;
215
+ }
216
+
217
+ /**
218
+ * Get the number of registered skills.
219
+ * @returns The count of registered skills.
220
+ */
221
+ get size(): number {
222
+ return this.registry.size;
223
+ }
224
+
225
+ /**
226
+ * Clear all registrations.
227
+ */
228
+ clear(): void {
229
+ this.registry.clear();
230
+ }
231
+ }
@@ -0,0 +1,230 @@
1
+ /**
2
+ * API Ninjas Trivia Skill - Fetches trivia questions from the API Ninjas service.
3
+ *
4
+ * Tier 2 built-in skill: requires API_NINJAS_KEY environment variable.
5
+ * Uses the API Ninjas Trivia endpoint to retrieve random trivia questions
6
+ * with optional category filtering.
7
+ */
8
+
9
+ import { SkillBase } from '../SkillBase.js';
10
+ import type {
11
+ SkillManifest,
12
+ SkillToolDefinition,
13
+ SkillPromptSection,
14
+ SkillConfig,
15
+ } from '../SkillBase.js';
16
+ import { SwaigFunctionResult } from '../../SwaigFunctionResult.js';
17
+
18
+ /** Response shape from the API Ninjas trivia endpoint. */
19
+ interface TriviaResponse {
20
+ /** Trivia category name. */
21
+ category: string;
22
+ /** The trivia question text. */
23
+ question: string;
24
+ /** The correct answer. */
25
+ answer: string;
26
+ }
27
+
28
+ const VALID_CATEGORIES = [
29
+ 'artliterature',
30
+ 'language',
31
+ 'sciencenature',
32
+ 'general',
33
+ 'fooddrink',
34
+ 'peopleplaces',
35
+ 'geography',
36
+ 'historyholidays',
37
+ 'entertainment',
38
+ 'toysgames',
39
+ 'music',
40
+ 'mathematics',
41
+ 'religionmythology',
42
+ 'sportsleisure',
43
+ ] as const;
44
+
45
+ /**
46
+ * Fetches trivia questions from the API Ninjas service.
47
+ *
48
+ * Tier 2 built-in skill. Requires the `API_NINJAS_KEY` environment variable.
49
+ * Supports optional `default_category` and `reveal_answer` config options.
50
+ */
51
+ export class ApiNinjasTriviaSkill extends SkillBase {
52
+ /**
53
+ * @param config - Optional configuration; supports `default_category` and `reveal_answer`.
54
+ */
55
+ constructor(config?: SkillConfig) {
56
+ super('api_ninjas_trivia', config);
57
+ }
58
+
59
+ /** @returns Manifest declaring API_NINJAS_KEY as required and config schema for category/reveal. */
60
+ getManifest(): SkillManifest {
61
+ return {
62
+ name: 'api_ninjas_trivia',
63
+ description:
64
+ 'Fetches trivia questions from the API Ninjas service. Supports multiple categories for varied trivia topics.',
65
+ version: '1.0.0',
66
+ author: 'SignalWire',
67
+ tags: ['trivia', 'api', 'entertainment', 'quiz', 'external'],
68
+ requiredEnvVars: ['API_NINJAS_KEY'],
69
+ configSchema: {
70
+ default_category: {
71
+ type: 'string',
72
+ description:
73
+ 'Default trivia category if none is specified by the user.',
74
+ },
75
+ reveal_answer: {
76
+ type: 'boolean',
77
+ description:
78
+ 'Whether to include the answer in the response. Defaults to false so the AI can quiz the user.',
79
+ default: false,
80
+ },
81
+ },
82
+ };
83
+ }
84
+
85
+ /** @returns A single `get_trivia` tool that fetches a random trivia question with optional category. */
86
+ getTools(): SkillToolDefinition[] {
87
+ const defaultCategory = this.getConfig<string | undefined>(
88
+ 'default_category',
89
+ undefined,
90
+ );
91
+ const revealAnswer = this.getConfig<boolean>('reveal_answer', false);
92
+
93
+ return [
94
+ {
95
+ name: 'get_trivia',
96
+ description:
97
+ 'Get a random trivia question. Optionally specify a category to narrow the topic.',
98
+ parameters: {
99
+ category: {
100
+ type: 'string',
101
+ description:
102
+ 'Trivia category. Available categories: ' +
103
+ VALID_CATEGORIES.join(', ') +
104
+ '. If not specified, a random category is used.',
105
+ },
106
+ },
107
+ handler: async (args: Record<string, unknown>) => {
108
+ const apiKey = process.env['API_NINJAS_KEY'];
109
+ if (!apiKey) {
110
+ return new SwaigFunctionResult(
111
+ 'Trivia service is not configured. The API_NINJAS_KEY environment variable is missing.',
112
+ );
113
+ }
114
+
115
+ let category = (args.category as string | undefined) ?? defaultCategory;
116
+
117
+ // Validate category if provided
118
+ if (category && typeof category === 'string') {
119
+ const normalized = category.toLowerCase().trim().replace(/[\s_-]/g, '');
120
+ if (
121
+ !VALID_CATEGORIES.includes(
122
+ normalized as (typeof VALID_CATEGORIES)[number],
123
+ )
124
+ ) {
125
+ return new SwaigFunctionResult(
126
+ `Unknown trivia category "${category}". Available categories: ${VALID_CATEGORIES.join(', ')}.`,
127
+ );
128
+ }
129
+ category = normalized;
130
+ }
131
+
132
+ try {
133
+ const url = category
134
+ ? `https://api.api-ninjas.com/v1/trivia?category=${encodeURIComponent(category)}`
135
+ : 'https://api.api-ninjas.com/v1/trivia';
136
+
137
+ const response = await fetch(url, {
138
+ headers: {
139
+ 'X-Api-Key': apiKey,
140
+ },
141
+ });
142
+
143
+ if (!response.ok) {
144
+ return new SwaigFunctionResult(
145
+ `Failed to fetch trivia: HTTP ${response.status} ${response.statusText}. Please try again later.`,
146
+ );
147
+ }
148
+
149
+ const data = (await response.json()) as TriviaResponse[];
150
+
151
+ if (!Array.isArray(data) || data.length === 0) {
152
+ return new SwaigFunctionResult(
153
+ 'No trivia questions were returned. Please try again or try a different category.',
154
+ );
155
+ }
156
+
157
+ const trivia = data[0]!;
158
+
159
+ if (revealAnswer) {
160
+ return new SwaigFunctionResult(
161
+ `Trivia (${trivia.category}): ${trivia.question} Answer: ${trivia.answer}`,
162
+ );
163
+ }
164
+
165
+ // When not revealing the answer, provide it in a structured way
166
+ // so the AI knows the answer but can quiz the user
167
+ return new SwaigFunctionResult(
168
+ `Here is a trivia question from the "${trivia.category}" category. ` +
169
+ `Question: ${trivia.question} ` +
170
+ `[The correct answer is: ${trivia.answer}. Do not reveal this unless the user attempts an answer or asks for it.]`,
171
+ );
172
+ } catch (err) {
173
+ const message = err instanceof Error ? err.message : String(err);
174
+ return new SwaigFunctionResult(
175
+ `Failed to fetch trivia question: ${message}`,
176
+ );
177
+ }
178
+ },
179
+ },
180
+ ];
181
+ }
182
+
183
+ /** @returns Prompt section describing trivia capabilities and quiz behavior. */
184
+ getPromptSections(): SkillPromptSection[] {
185
+ const revealAnswer = this.getConfig<boolean>('reveal_answer', false);
186
+
187
+ const bullets: string[] = [
188
+ 'Use the get_trivia tool to fetch a random trivia question.',
189
+ `Available categories: ${VALID_CATEGORIES.join(', ')}.`,
190
+ 'If no category is specified, a random category will be selected.',
191
+ ];
192
+
193
+ if (!revealAnswer) {
194
+ bullets.push(
195
+ 'When you receive a trivia question, ask it to the user and let them try to answer before revealing the correct answer.',
196
+ 'If the user gets it wrong, give them a hint or tell them the correct answer.',
197
+ );
198
+ } else {
199
+ bullets.push(
200
+ 'Trivia answers are included in the response. Share the question and answer with the user.',
201
+ );
202
+ }
203
+
204
+ bullets.push(
205
+ 'Keep trivia fun and engaging. Encourage the user to try more questions.',
206
+ );
207
+
208
+ return [
209
+ {
210
+ title: 'Trivia Questions',
211
+ body: 'You can fetch and ask trivia questions from a wide range of categories.',
212
+ bullets,
213
+ },
214
+ ];
215
+ }
216
+
217
+ /** @returns Speech recognition hints for trivia-related keywords. */
218
+ getHints(): string[] {
219
+ return ['trivia', 'quiz', 'question', 'fun fact'];
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Factory function for creating ApiNinjasTriviaSkill instances.
225
+ * @param config - Optional skill configuration.
226
+ * @returns A new ApiNinjasTriviaSkill instance.
227
+ */
228
+ export function createSkill(config?: SkillConfig): ApiNinjasTriviaSkill {
229
+ return new ApiNinjasTriviaSkill(config);
230
+ }