@tryhamster/gerbil 1.0.0-rc.0 → 1.0.0-rc.2

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 (94) hide show
  1. package/README.md +79 -14
  2. package/dist/auto-update-DsWBBnEk.mjs +3 -0
  3. package/dist/browser/index.d.mts +401 -5
  4. package/dist/browser/index.d.mts.map +1 -1
  5. package/dist/browser/index.mjs +1772 -146
  6. package/dist/browser/index.mjs.map +1 -1
  7. package/dist/{chrome-backend-CtwPENIW.mjs → chrome-backend-JEPeM2YE.mjs} +1 -1
  8. package/dist/{chrome-backend-C5Un08O4.mjs → chrome-backend-Y9F7W5VQ.mjs} +514 -73
  9. package/dist/chrome-backend-Y9F7W5VQ.mjs.map +1 -0
  10. package/dist/cli.mjs +3359 -646
  11. package/dist/cli.mjs.map +1 -1
  12. package/dist/frameworks/express.d.mts +1 -1
  13. package/dist/frameworks/express.mjs +3 -3
  14. package/dist/frameworks/fastify.d.mts +1 -1
  15. package/dist/frameworks/fastify.mjs +3 -3
  16. package/dist/frameworks/hono.d.mts +1 -1
  17. package/dist/frameworks/hono.mjs +3 -3
  18. package/dist/frameworks/next.d.mts +2 -2
  19. package/dist/frameworks/next.mjs +3 -3
  20. package/dist/frameworks/react.d.mts +1 -1
  21. package/dist/frameworks/trpc.d.mts +1 -1
  22. package/dist/frameworks/trpc.mjs +3 -3
  23. package/dist/gerbil-DeQlX_Mt.mjs +5 -0
  24. package/dist/gerbil-POAz8peb.d.mts +431 -0
  25. package/dist/gerbil-POAz8peb.d.mts.map +1 -0
  26. package/dist/gerbil-yoSpRHgv.mjs +1463 -0
  27. package/dist/gerbil-yoSpRHgv.mjs.map +1 -0
  28. package/dist/index.d.mts +395 -9
  29. package/dist/index.d.mts.map +1 -1
  30. package/dist/index.mjs +8 -6
  31. package/dist/index.mjs.map +1 -1
  32. package/dist/integrations/ai-sdk.d.mts +122 -4
  33. package/dist/integrations/ai-sdk.d.mts.map +1 -1
  34. package/dist/integrations/ai-sdk.mjs +239 -11
  35. package/dist/integrations/ai-sdk.mjs.map +1 -1
  36. package/dist/integrations/langchain.d.mts +132 -2
  37. package/dist/integrations/langchain.d.mts.map +1 -1
  38. package/dist/integrations/langchain.mjs +176 -8
  39. package/dist/integrations/langchain.mjs.map +1 -1
  40. package/dist/integrations/llamaindex.d.mts +1 -1
  41. package/dist/integrations/llamaindex.mjs +3 -3
  42. package/dist/integrations/mcp-client.mjs +4 -4
  43. package/dist/integrations/mcp-client.mjs.map +1 -1
  44. package/dist/integrations/mcp.d.mts +2 -2
  45. package/dist/integrations/mcp.d.mts.map +1 -1
  46. package/dist/integrations/mcp.mjs +6 -6
  47. package/dist/{mcp-R8kRLIKb.mjs → mcp-Bitg4sjX.mjs} +10 -37
  48. package/dist/mcp-Bitg4sjX.mjs.map +1 -0
  49. package/dist/microphone-D-6y9aiE.mjs +3 -0
  50. package/dist/{models-DKULvhOr.mjs → models-BAtL8qsA.mjs} +42 -7
  51. package/dist/models-BAtL8qsA.mjs.map +1 -0
  52. package/dist/{models-De2-_GmQ.d.mts → models-CE0fBq0U.d.mts} +2 -2
  53. package/dist/models-CE0fBq0U.d.mts.map +1 -0
  54. package/dist/{one-liner-BUQR0nqq.mjs → one-liner-B1rmFto6.mjs} +2 -2
  55. package/dist/{one-liner-BUQR0nqq.mjs.map → one-liner-B1rmFto6.mjs.map} +1 -1
  56. package/dist/repl-D20JO260.mjs +10 -0
  57. package/dist/skills/index.d.mts +303 -12
  58. package/dist/skills/index.d.mts.map +1 -1
  59. package/dist/skills/index.mjs +6 -6
  60. package/dist/skills-5DxAV-rn.mjs +1435 -0
  61. package/dist/skills-5DxAV-rn.mjs.map +1 -0
  62. package/dist/stt-Bv_dum-R.mjs +433 -0
  63. package/dist/stt-Bv_dum-R.mjs.map +1 -0
  64. package/dist/stt-KzSoNvwI.mjs +3 -0
  65. package/dist/{tools-BsiEE6f2.mjs → tools-IYPrqoek.mjs} +6 -7
  66. package/dist/{tools-BsiEE6f2.mjs.map → tools-IYPrqoek.mjs.map} +1 -1
  67. package/dist/tts-5yWeP_I0.mjs +3 -0
  68. package/dist/tts-DG6denWG.mjs +729 -0
  69. package/dist/tts-DG6denWG.mjs.map +1 -0
  70. package/dist/types-s6Py2_DL.d.mts +353 -0
  71. package/dist/types-s6Py2_DL.d.mts.map +1 -0
  72. package/dist/{utils-7vXqtq2Q.mjs → utils-CkB4Roi6.mjs} +1 -1
  73. package/dist/{utils-7vXqtq2Q.mjs.map → utils-CkB4Roi6.mjs.map} +1 -1
  74. package/docs/ai-sdk.md +137 -21
  75. package/docs/browser.md +241 -2
  76. package/docs/memory.md +72 -0
  77. package/docs/stt.md +494 -0
  78. package/docs/tts.md +569 -0
  79. package/docs/vision.md +396 -0
  80. package/package.json +17 -18
  81. package/dist/auto-update-BbNHbSU1.mjs +0 -3
  82. package/dist/chrome-backend-C5Un08O4.mjs.map +0 -1
  83. package/dist/gerbil-BfnsFWRE.mjs +0 -644
  84. package/dist/gerbil-BfnsFWRE.mjs.map +0 -1
  85. package/dist/gerbil-BjW-z7Fq.mjs +0 -5
  86. package/dist/gerbil-DZ1k3ChC.d.mts +0 -138
  87. package/dist/gerbil-DZ1k3ChC.d.mts.map +0 -1
  88. package/dist/mcp-R8kRLIKb.mjs.map +0 -1
  89. package/dist/models-DKULvhOr.mjs.map +0 -1
  90. package/dist/models-De2-_GmQ.d.mts.map +0 -1
  91. package/dist/skills-D3CEpgDc.mjs +0 -630
  92. package/dist/skills-D3CEpgDc.mjs.map +0 -1
  93. package/dist/types-BS1N92Jt.d.mts +0 -183
  94. package/dist/types-BS1N92Jt.d.mts.map +0 -1
@@ -0,0 +1,1435 @@
1
+ import { a as getInstance } from "./one-liner-B1rmFto6.mjs";
2
+ import { execSync } from "node:child_process";
3
+ import { unlinkSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+ import { z } from "zod";
8
+ import { execSync as execSync$1 } from "child_process";
9
+ import { existsSync as existsSync$1, readFileSync as readFileSync$1, unlinkSync as unlinkSync$1, writeFileSync as writeFileSync$1 } from "fs";
10
+ import { tmpdir as tmpdir$1 } from "os";
11
+ import { join as join$1, resolve } from "path";
12
+
13
+ //#region src/skills/registry.ts
14
+ const registry = /* @__PURE__ */ new Map();
15
+ const skillSources = /* @__PURE__ */ new Map();
16
+ const RESERVED_NAMES = new Set([
17
+ "repl",
18
+ "chat",
19
+ "skills",
20
+ "tools",
21
+ "model",
22
+ "integrate",
23
+ "benchmark",
24
+ "info",
25
+ "serve",
26
+ "cache",
27
+ "generate",
28
+ "models",
29
+ "bench",
30
+ "r",
31
+ "c",
32
+ "g",
33
+ "help",
34
+ "version",
35
+ "gerbil"
36
+ ]);
37
+ /**
38
+ * Define and register a skill
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * const sentiment = defineSkill({
43
+ * name: "sentiment",
44
+ * description: "Analyze sentiment of text",
45
+ * input: z.object({ text: z.string() }),
46
+ * output: z.object({ sentiment: z.enum(["positive", "negative", "neutral"]) }),
47
+ * async run({ input, gerbil }) {
48
+ * return gerbil.json(`Analyze sentiment: ${input.text}`, { schema: this.output });
49
+ * }
50
+ * });
51
+ * ```
52
+ */
53
+ function defineSkill(definition) {
54
+ if (!/^[a-z][a-z0-9-]*$/.test(definition.name)) throw new Error(`Skill name must be kebab-case starting with a letter: ${definition.name}`);
55
+ if (RESERVED_NAMES.has(definition.name)) throw new Error(`Skill name "${definition.name}" is reserved for CLI commands. Choose a different name.`);
56
+ const execute = async (input) => skill.run(input);
57
+ const skill = Object.assign(execute, {
58
+ definition,
59
+ async run(input, gerbil) {
60
+ const g = gerbil ?? await getInstance(definition.model);
61
+ let validatedInput = input;
62
+ if (definition.input) {
63
+ const parsed = definition.input.safeParse(input);
64
+ if (!parsed.success) throw new Error(`Invalid input for skill "${definition.name}": ${parsed.error.message}`);
65
+ validatedInput = parsed.data;
66
+ }
67
+ const ctx = {
68
+ input: validatedInput,
69
+ gerbil: g,
70
+ rawInput: input,
71
+ definition
72
+ };
73
+ const result = await definition.run.call(definition, ctx);
74
+ if (definition.output && typeof result !== "string") {
75
+ const parsed = definition.output.safeParse(result);
76
+ if (!parsed.success) throw new Error(`Invalid output from skill "${definition.name}": ${parsed.error.message}`);
77
+ return parsed.data;
78
+ }
79
+ return result;
80
+ }
81
+ });
82
+ registry.set(definition.name, skill);
83
+ return skill;
84
+ }
85
+ /**
86
+ * Get a skill by name
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * const sentiment = useSkill("sentiment");
91
+ * const result = await sentiment({ text: "I love this!" });
92
+ * ```
93
+ */
94
+ function useSkill(name) {
95
+ const skill = registry.get(name);
96
+ if (!skill) throw new Error(`Skill not found: "${name}". Available: ${listSkills().join(", ") || "none"}`);
97
+ return skill;
98
+ }
99
+ /**
100
+ * List all registered skill names
101
+ */
102
+ function listSkills() {
103
+ return Array.from(registry.keys()).sort();
104
+ }
105
+ /**
106
+ * Get skill metadata
107
+ */
108
+ function getSkillInfo(name) {
109
+ const skill = registry.get(name);
110
+ if (!skill) return;
111
+ return {
112
+ name: skill.definition.name,
113
+ description: skill.definition.description,
114
+ version: skill.definition.version,
115
+ author: skill.definition.author,
116
+ builtin: !skillSources.has(name),
117
+ source: skillSources.get(name)
118
+ };
119
+ }
120
+ /**
121
+ * Check if a skill exists
122
+ */
123
+ function hasSkill(name) {
124
+ return registry.has(name);
125
+ }
126
+ /**
127
+ * Remove a skill from registry
128
+ */
129
+ function removeSkill(name) {
130
+ skillSources.delete(name);
131
+ return registry.delete(name);
132
+ }
133
+ /**
134
+ * Clear all skills from registry
135
+ */
136
+ function clearSkills() {
137
+ registry.clear();
138
+ skillSources.clear();
139
+ }
140
+ /**
141
+ * Get all skill info
142
+ */
143
+ function getAllSkillInfo() {
144
+ return listSkills().map((name) => getSkillInfo(name));
145
+ }
146
+ /**
147
+ * Register a skill with its source file (used by loader)
148
+ * @internal
149
+ */
150
+ function registerSkillWithSource(skill, source) {
151
+ registry.set(skill.definition.name, skill);
152
+ skillSources.set(skill.definition.name, source);
153
+ }
154
+
155
+ //#endregion
156
+ //#region src/skills/loader.ts
157
+ /**
158
+ * Skill Loader
159
+ *
160
+ * Load skills from files, directories, and packages.
161
+ */
162
+ /**
163
+ * Load skills from the project's .gerbil/skills/ directory
164
+ *
165
+ * This is the standard location for project-specific skills.
166
+ * Creates the directory structure if it doesn't exist.
167
+ *
168
+ * @example
169
+ * ```ts
170
+ * // Load from .gerbil/skills/ in current working directory
171
+ * const loaded = await loadProjectSkills();
172
+ * console.log(`Loaded ${loaded.length} project skills`);
173
+ * ```
174
+ */
175
+ async function loadProjectSkills(cwd = process.cwd()) {
176
+ const path$1 = await import("node:path");
177
+ const fs$1 = await import("node:fs");
178
+ const gerbilDir = path$1.join(cwd, ".gerbil");
179
+ const skillsDir = path$1.join(gerbilDir, "skills");
180
+ if (!fs$1.existsSync(skillsDir)) return [];
181
+ return loadSkills(skillsDir);
182
+ }
183
+ /**
184
+ * Load all skills from a directory
185
+ *
186
+ * @example
187
+ * ```ts
188
+ * // Load all *.skill.ts and *.skill.js files
189
+ * const loaded = await loadSkills("./skills");
190
+ * console.log(`Loaded ${loaded.length} skills`);
191
+ * ```
192
+ */
193
+ async function loadSkills(dir, options = {}) {
194
+ const { patterns = ["*.skill.ts", "*.skill.js"] } = options;
195
+ const loaded = [];
196
+ try {
197
+ const fs$1 = await import("node:fs");
198
+ const path$1 = await import("node:path");
199
+ const resolvedDir = path$1.resolve(dir);
200
+ if (!fs$1.existsSync(resolvedDir)) throw new Error(`Skills directory not found: ${dir}`);
201
+ const files = fs$1.readdirSync(resolvedDir);
202
+ for (const file of files) if (patterns.some((pattern) => {
203
+ return (/* @__PURE__ */ new RegExp(`^${pattern.replace(/\*/g, ".*").replace(/\./g, "\\.")}$`)).test(file);
204
+ })) {
205
+ const skill = await loadSkill(path$1.join(resolvedDir, file));
206
+ if (skill) loaded.push(skill.definition.name);
207
+ }
208
+ } catch (error) {
209
+ if (error.code === "MODULE_NOT_FOUND") throw new Error(`Skills directory not found: ${dir}`);
210
+ throw error;
211
+ }
212
+ return loaded;
213
+ }
214
+ /**
215
+ * Load a single skill from a file
216
+ *
217
+ * @example
218
+ * ```ts
219
+ * const skill = await loadSkill("./skills/sentiment.skill.ts");
220
+ * if (skill) {
221
+ * const result = await skill({ text: "Hello" });
222
+ * }
223
+ * ```
224
+ */
225
+ async function loadSkill(filePath) {
226
+ try {
227
+ const resolvedPath = (await import("node:path")).resolve(filePath);
228
+ const skill = (await import(pathToFileURL(resolvedPath).href)).default;
229
+ if (!skill?.definition) return null;
230
+ registerSkillWithSource(skill, resolvedPath);
231
+ return skill;
232
+ } catch (_error) {
233
+ return null;
234
+ }
235
+ }
236
+ /**
237
+ * Load skills from an npm package
238
+ *
239
+ * @example
240
+ * ```ts
241
+ * // Load skills from a package
242
+ * const loaded = await loadSkillPackage("gerbil-skills-extra");
243
+ * ```
244
+ */
245
+ async function loadSkillPackage(packageName) {
246
+ try {
247
+ const module = await import(packageName);
248
+ const loaded = [];
249
+ for (const [_key, value] of Object.entries(module)) if (isSkill(value)) {
250
+ const skill = value;
251
+ registerSkillWithSource(skill, `npm:${packageName}`);
252
+ loaded.push(skill.definition.name);
253
+ }
254
+ if (module.default && typeof module.default === "object") {
255
+ for (const [_key, value] of Object.entries(module.default)) if (isSkill(value)) {
256
+ const skill = value;
257
+ registerSkillWithSource(skill, `npm:${packageName}`);
258
+ loaded.push(skill.definition.name);
259
+ }
260
+ }
261
+ return loaded;
262
+ } catch (error) {
263
+ throw new Error(`Failed to load skill package "${packageName}": ${error}`);
264
+ }
265
+ }
266
+ /**
267
+ * Check if a value is a Skill
268
+ */
269
+ function isSkill(value) {
270
+ return typeof value === "function" && typeof value.definition === "object" && typeof value.definition.name === "string" && typeof value.definition.run === "function";
271
+ }
272
+
273
+ //#endregion
274
+ //#region src/skills/builtin/analyze-screenshot.ts
275
+ /**
276
+ * Analyze Screenshot Skill
277
+ *
278
+ * Analyze a UI screenshot for design, accessibility, or QA purposes.
279
+ */
280
+ const AnalyzeScreenshotInput = z.object({
281
+ image: z.string(),
282
+ type: z.enum([
283
+ "ui-review",
284
+ "accessibility",
285
+ "suggestions",
286
+ "qa",
287
+ "ux-audit",
288
+ "mobile-check"
289
+ ]).default("ui-review"),
290
+ focus: z.array(z.string()).optional()
291
+ });
292
+ const analysisPrompts = {
293
+ "ui-review": `Review this UI screenshot as a design expert. Analyze:
294
+ - Visual hierarchy and layout
295
+ - Typography and readability
296
+ - Color scheme and contrast
297
+ - Spacing and alignment
298
+ - Component consistency
299
+ - Overall design quality
300
+
301
+ Provide specific observations and suggestions.`,
302
+ accessibility: `Analyze this screenshot for accessibility issues. Check for:
303
+ - Color contrast ratios
304
+ - Text size and readability
305
+ - Touch target sizes
306
+ - Visual affordances for interactive elements
307
+ - Potential issues for users with visual impairments
308
+ - Missing alt text indicators
309
+
310
+ List specific issues found and how to fix them.`,
311
+ suggestions: `As a senior UX designer, review this UI and suggest improvements:
312
+ - Modern design patterns that could be applied
313
+ - Usability improvements
314
+ - Visual polish opportunities
315
+ - User experience enhancements
316
+ - Ways to make it more engaging
317
+
318
+ Be specific and actionable.`,
319
+ qa: `Perform QA analysis on this screenshot. Look for:
320
+ - Visual bugs or glitches
321
+ - Alignment and spacing issues
322
+ - Broken or missing elements
323
+ - Inconsistencies with design standards
324
+ - Text overflow or truncation issues
325
+ - Responsive design problems
326
+
327
+ Report each issue with its location and severity.`,
328
+ "ux-audit": `Conduct a UX audit of this interface:
329
+ - User flow clarity
330
+ - Call-to-action visibility
331
+ - Information architecture
332
+ - Cognitive load assessment
333
+ - Error prevention
334
+ - User feedback mechanisms
335
+
336
+ Provide a structured audit report.`,
337
+ "mobile-check": `Evaluate this UI for mobile usability:
338
+ - Touch target sizes (minimum 44x44px)
339
+ - Thumb-zone accessibility
340
+ - Content priority on small screens
341
+ - Gesture affordances
342
+ - Loading indicators
343
+ - Mobile-specific patterns
344
+
345
+ Identify mobile-specific issues and recommendations.`
346
+ };
347
+ const analyzeScreenshot = defineSkill({
348
+ name: "analyze-screenshot",
349
+ description: "Analyze a UI screenshot for design, accessibility, or QA",
350
+ version: "1.0.0",
351
+ model: "ministral-3b",
352
+ input: AnalyzeScreenshotInput,
353
+ maxTokens: 1200,
354
+ temperature: .3,
355
+ async run({ input, gerbil }) {
356
+ const { image, type = "ui-review", focus } = input;
357
+ if (!gerbil.supportsVision()) throw new Error(`Current model doesn't support vision. Load a vision model like "ministral-3b" first.`);
358
+ let prompt = analysisPrompts[type];
359
+ if (focus && focus.length > 0) prompt += `\n\nPay special attention to: ${focus.join(", ")}.`;
360
+ return (await gerbil.generate(prompt, {
361
+ images: [{ source: image }],
362
+ maxTokens: this.maxTokens,
363
+ temperature: this.temperature
364
+ })).text;
365
+ }
366
+ });
367
+
368
+ //#endregion
369
+ //#region src/skills/builtin/announce.ts
370
+ /**
371
+ * Announce Skill
372
+ *
373
+ * Generate and speak an announcement using AI.
374
+ * Useful for scripts that need to give voice updates.
375
+ */
376
+ const AnnounceInput = z.object({
377
+ message: z.string(),
378
+ style: z.enum([
379
+ "casual",
380
+ "formal",
381
+ "excited",
382
+ "calm",
383
+ "urgent"
384
+ ]).default("casual"),
385
+ voice: z.enum([
386
+ "af_heart",
387
+ "af_bella",
388
+ "af_nicole",
389
+ "am_fenrir",
390
+ "am_michael",
391
+ "bf_emma",
392
+ "bm_george"
393
+ ]).default("af_heart"),
394
+ speed: z.number().min(.5).max(2).default(1),
395
+ textOnly: z.boolean().default(false)
396
+ });
397
+ const stylePrompts = {
398
+ casual: "Rephrase this as a casual, friendly announcement. Keep it natural and conversational.",
399
+ formal: "Rephrase this as a professional, formal announcement. Be clear and dignified.",
400
+ excited: "Rephrase this as an excited, enthusiastic announcement! Add energy and positivity.",
401
+ calm: "Rephrase this as a calm, soothing announcement. Be gentle and reassuring.",
402
+ urgent: "Rephrase this as an urgent announcement. Be direct and emphasize importance."
403
+ };
404
+ function saveWav$3(filename, audio, sampleRate) {
405
+ const buffer = Buffer.alloc(44 + audio.length * 2);
406
+ buffer.write("RIFF", 0);
407
+ buffer.writeUInt32LE(36 + audio.length * 2, 4);
408
+ buffer.write("WAVE", 8);
409
+ buffer.write("fmt ", 12);
410
+ buffer.writeUInt32LE(16, 16);
411
+ buffer.writeUInt16LE(1, 20);
412
+ buffer.writeUInt16LE(1, 22);
413
+ buffer.writeUInt32LE(sampleRate, 24);
414
+ buffer.writeUInt32LE(sampleRate * 2, 28);
415
+ buffer.writeUInt16LE(2, 32);
416
+ buffer.writeUInt16LE(16, 34);
417
+ buffer.write("data", 36);
418
+ buffer.writeUInt32LE(audio.length * 2, 40);
419
+ for (let i = 0; i < audio.length; i++) {
420
+ const s = Math.max(-1, Math.min(1, audio[i]));
421
+ buffer.writeInt16LE(Math.round(s * 32767), 44 + i * 2);
422
+ }
423
+ writeFileSync$1(filename, buffer);
424
+ }
425
+ const announce = defineSkill({
426
+ name: "announce",
427
+ description: "Generate and speak an AI-crafted announcement",
428
+ version: "1.0.0",
429
+ input: AnnounceInput,
430
+ temperature: .7,
431
+ maxTokens: 150,
432
+ async run({ input, gerbil }) {
433
+ const { message, style = "casual", voice = "af_heart", speed = 1, textOnly = false } = input;
434
+ const announcementText = (await gerbil.generate(message, {
435
+ system: `${stylePrompts[style]}
436
+ Keep it brief (1-2 sentences max).
437
+ Output only the announcement text, nothing else.`,
438
+ temperature: this.temperature,
439
+ maxTokens: this.maxTokens
440
+ })).text.trim();
441
+ if (textOnly) return announcementText;
442
+ const speechResult = await gerbil.speak(announcementText, {
443
+ voice,
444
+ speed
445
+ });
446
+ const tempFile = join$1(tmpdir$1(), `gerbil-announce-${Date.now()}.wav`);
447
+ saveWav$3(tempFile, speechResult.audio, speechResult.sampleRate);
448
+ try {
449
+ const platform = process.platform;
450
+ if (platform === "darwin") execSync$1(`afplay "${tempFile}"`, { stdio: "inherit" });
451
+ else if (platform === "linux") try {
452
+ execSync$1(`aplay "${tempFile}"`, { stdio: "inherit" });
453
+ } catch {
454
+ execSync$1(`paplay "${tempFile}"`, { stdio: "inherit" });
455
+ }
456
+ else if (platform === "win32") execSync$1(`powershell -c "(New-Object Media.SoundPlayer '${tempFile}').PlaySync()"`, { stdio: "inherit" });
457
+ } finally {
458
+ try {
459
+ unlinkSync$1(tempFile);
460
+ } catch {}
461
+ }
462
+ return announcementText;
463
+ }
464
+ });
465
+
466
+ //#endregion
467
+ //#region src/skills/builtin/caption-image.ts
468
+ /**
469
+ * Caption Image Skill
470
+ *
471
+ * Generate captions, alt text, or social media descriptions for images.
472
+ */
473
+ const CaptionImageInput = z.object({
474
+ image: z.string(),
475
+ type: z.enum([
476
+ "alt-text",
477
+ "caption",
478
+ "social-media",
479
+ "seo",
480
+ "artistic",
481
+ "technical"
482
+ ]).default("caption"),
483
+ tone: z.enum([
484
+ "professional",
485
+ "casual",
486
+ "playful",
487
+ "formal",
488
+ "descriptive"
489
+ ]).default("professional"),
490
+ maxLength: z.enum([
491
+ "short",
492
+ "medium",
493
+ "long"
494
+ ]).default("medium"),
495
+ platform: z.enum([
496
+ "twitter",
497
+ "instagram",
498
+ "linkedin",
499
+ "facebook"
500
+ ]).optional()
501
+ });
502
+ const captionPrompts = {
503
+ "alt-text": `Generate accessible alt text for this image.
504
+ - Describe the essential content concisely
505
+ - Include relevant details for screen reader users
506
+ - Avoid starting with "Image of" or "Picture of"
507
+ - Be factual and objective`,
508
+ caption: `Write a caption for this image.
509
+ - Capture the essence of the image
510
+ - Make it engaging and informative
511
+ - Consider the context and mood`,
512
+ "social-media": `Create a social media caption for this image.
513
+ - Make it engaging and shareable
514
+ - Include relevant emoji where appropriate
515
+ - Suggest hashtags if applicable
516
+ - Optimize for engagement`,
517
+ seo: `Write SEO-optimized alt text and description for this image.
518
+ - Include relevant keywords naturally
519
+ - Be descriptive but concise
520
+ - Focus on searchable terms
521
+ - Maintain readability`,
522
+ artistic: `Write an artistic, evocative caption for this image.
523
+ - Capture the mood and emotion
524
+ - Use creative language
525
+ - Tell a micro-story if appropriate
526
+ - Be memorable and unique`,
527
+ technical: `Write a technical description of this image.
528
+ - Describe components and elements precisely
529
+ - Use appropriate technical terminology
530
+ - Note measurements or specifications if visible
531
+ - Be objective and detailed`
532
+ };
533
+ const lengthGuides = {
534
+ short: 100,
535
+ medium: 200,
536
+ long: 400
537
+ };
538
+ const platformGuides = {
539
+ twitter: "Keep under 280 characters. Make it punchy and tweetable.",
540
+ instagram: "Include relevant hashtags. Can be longer and more storytelling.",
541
+ linkedin: "Keep it professional. Focus on value and insights.",
542
+ facebook: "Can be conversational. Ask questions to encourage engagement."
543
+ };
544
+ const captionImage = defineSkill({
545
+ name: "caption-image",
546
+ description: "Generate captions, alt text, or social media descriptions for images",
547
+ version: "1.0.0",
548
+ model: "ministral-3b",
549
+ input: CaptionImageInput,
550
+ temperature: .7,
551
+ async run({ input, gerbil }) {
552
+ const { image, type = "caption", tone = "professional", maxLength = "medium", platform } = input;
553
+ if (!gerbil.supportsVision()) throw new Error(`Current model doesn't support vision. Load a vision model like "ministral-3b" first.`);
554
+ let prompt = captionPrompts[type];
555
+ prompt += `\n\nTone: ${tone}`;
556
+ if (platform && type === "social-media") prompt += `\n\nPlatform: ${platform}. ${platformGuides[platform]}`;
557
+ return (await gerbil.generate(prompt, {
558
+ images: [{ source: image }],
559
+ maxTokens: lengthGuides[maxLength],
560
+ temperature: this.temperature
561
+ })).text;
562
+ }
563
+ });
564
+
565
+ //#endregion
566
+ //#region src/skills/builtin/commit.ts
567
+ /**
568
+ * Commit Skill
569
+ *
570
+ * Generate git commit messages from staged changes.
571
+ */
572
+ const CommitInput = z.object({
573
+ diff: z.string().optional(),
574
+ type: z.enum([
575
+ "conventional",
576
+ "simple",
577
+ "detailed"
578
+ ]).default("conventional"),
579
+ maxLength: z.number().default(72)
580
+ });
581
+ async function getGitDiff() {
582
+ try {
583
+ const { execSync: execSync$2 } = await import("node:child_process");
584
+ return execSync$2("git diff --staged", { encoding: "utf-8" });
585
+ } catch {
586
+ return "";
587
+ }
588
+ }
589
+ function getSystemPrompt(type) {
590
+ switch (type) {
591
+ case "conventional": return `Generate a git commit message following the Conventional Commits format.
592
+ Use one of these types: feat, fix, docs, style, refactor, perf, test, chore.
593
+ Format: type(scope): description
594
+ Keep it under 72 characters.
595
+ No period at the end.
596
+ Only output the commit message, nothing else.`;
597
+ case "detailed": return `Generate a detailed git commit message with a subject line and body.
598
+ Subject: under 72 chars, imperative mood
599
+ Body: explain what and why
600
+ Only output the commit message, nothing else.`;
601
+ default: return `Generate a concise git commit message.
602
+ Use imperative mood (e.g., "Add", "Fix", "Update").
603
+ Keep it under 72 characters.
604
+ No period at the end.
605
+ Only output the commit message, nothing else.`;
606
+ }
607
+ }
608
+ const commit = defineSkill({
609
+ name: "commit",
610
+ description: "Generate a git commit message from staged changes",
611
+ version: "1.0.0",
612
+ input: CommitInput,
613
+ temperature: .3,
614
+ maxTokens: 100,
615
+ async run({ input, gerbil }) {
616
+ const { diff, type = "conventional", maxLength = 72 } = input;
617
+ const actualDiff = diff || await getGitDiff();
618
+ if (!actualDiff || actualDiff.trim().length === 0) throw new Error("No changes staged. Run `git add` first.");
619
+ let message = (await gerbil.generate(`Generate a commit message for the following diff:\n\n${actualDiff}`, {
620
+ system: getSystemPrompt(type),
621
+ maxTokens: this.maxTokens,
622
+ temperature: this.temperature
623
+ })).text.trim();
624
+ message = message.replace(/<think>[\s\S]*?<\/think>/g, "").trim();
625
+ message = message.replace(/<\/?think>/g, "").trim();
626
+ message = message.replace(/^["']|["']$/g, "");
627
+ message = message.split("\n")[0];
628
+ if (message.length > maxLength) message = `${message.substring(0, maxLength - 3)}...`;
629
+ return message;
630
+ }
631
+ });
632
+
633
+ //#endregion
634
+ //#region src/skills/builtin/compare-images.ts
635
+ /**
636
+ * Compare Images Skill
637
+ *
638
+ * Compare two images and describe their differences.
639
+ */
640
+ const CompareImagesInput = z.object({
641
+ image1: z.string(),
642
+ image2: z.string(),
643
+ type: z.enum([
644
+ "visual-diff",
645
+ "design-comparison",
646
+ "before-after",
647
+ "a-b-test",
648
+ "version-diff"
649
+ ]).default("visual-diff"),
650
+ focus: z.array(z.string()).optional()
651
+ });
652
+ const comparisonPrompts = {
653
+ "visual-diff": `Compare these two images and identify all visual differences.
654
+ - List specific elements that differ
655
+ - Note position, color, size, and content changes
656
+ - Identify additions and removals
657
+ - Rate the significance of each change`,
658
+ "design-comparison": `Compare these two designs as a design expert.
659
+ - Analyze layout differences
660
+ - Compare typography and colors
661
+ - Evaluate which design is more effective
662
+ - Suggest which elements work better in each`,
663
+ "before-after": `Analyze these before and after images.
664
+ - Describe what changed
665
+ - Evaluate if the changes are improvements
666
+ - Note any unintended consequences
667
+ - Summarize the overall transformation`,
668
+ "a-b-test": `Analyze these two variants for A/B testing purposes.
669
+ - Identify the key differences being tested
670
+ - Predict which might perform better and why
671
+ - Note potential user experience impacts
672
+ - Suggest what metrics to measure`,
673
+ "version-diff": `Compare these two versions of the UI.
674
+ - Document all changes between versions
675
+ - Categorize changes (bug fix, feature, style)
676
+ - Identify breaking changes
677
+ - Note any regressions`
678
+ };
679
+ const compareImages = defineSkill({
680
+ name: "compare-images",
681
+ description: "Compare two images and describe their differences",
682
+ version: "1.0.0",
683
+ model: "ministral-3b",
684
+ input: CompareImagesInput,
685
+ maxTokens: 1500,
686
+ temperature: .3,
687
+ async run({ input, gerbil }) {
688
+ const { image1, image2, type = "visual-diff", focus } = input;
689
+ if (!gerbil.supportsVision()) throw new Error(`Current model doesn't support vision. Load a vision model like "ministral-3b" first.`);
690
+ let prompt = `I'm showing you two images for comparison.\n\n${comparisonPrompts[type]}`;
691
+ if (focus && focus.length > 0) prompt += `\n\nFocus specifically on: ${focus.join(", ")}.`;
692
+ prompt += "\n\nThe first image is shown first, followed by the second image.";
693
+ return (await gerbil.generate(prompt, {
694
+ images: [{ source: image1 }, { source: image2 }],
695
+ maxTokens: this.maxTokens,
696
+ temperature: this.temperature
697
+ })).text;
698
+ }
699
+ });
700
+
701
+ //#endregion
702
+ //#region src/skills/builtin/describe-image.ts
703
+ /**
704
+ * Describe Image Skill
705
+ *
706
+ * Describe an image using vision AI.
707
+ */
708
+ const DescribeImageInput = z.object({
709
+ image: z.string(),
710
+ focus: z.enum([
711
+ "general",
712
+ "details",
713
+ "text",
714
+ "objects",
715
+ "scene",
716
+ "colors"
717
+ ]).default("general"),
718
+ format: z.enum([
719
+ "paragraph",
720
+ "bullets",
721
+ "structured"
722
+ ]).default("paragraph"),
723
+ detail: z.enum([
724
+ "brief",
725
+ "normal",
726
+ "comprehensive"
727
+ ]).default("normal")
728
+ });
729
+ const focusPrompts = {
730
+ general: "Describe this image comprehensively, covering the main subject, context, and notable elements.",
731
+ details: "Describe this image in detail, including colors, textures, lighting, and small elements.",
732
+ text: "Extract and describe any text visible in this image. Include the text content and its context.",
733
+ objects: "List and describe all objects you can identify in this image, including their positions.",
734
+ scene: "Describe the scene, setting, and atmosphere of this image. What story does it tell?",
735
+ colors: "Analyze the color palette and visual composition of this image."
736
+ };
737
+ const detailLengths = {
738
+ brief: 150,
739
+ normal: 400,
740
+ comprehensive: 800
741
+ };
742
+ const describeImage = defineSkill({
743
+ name: "describe-image",
744
+ description: "Describe an image using vision AI",
745
+ version: "1.0.0",
746
+ model: "ministral-3b",
747
+ input: DescribeImageInput,
748
+ temperature: .5,
749
+ async run({ input, gerbil }) {
750
+ const { image, focus = "general", format = "paragraph", detail = "normal" } = input;
751
+ if (!gerbil.supportsVision()) throw new Error(`Current model doesn't support vision. Load a vision model like "ministral-3b" first.`);
752
+ const prompt = `${focusPrompts[focus]}\n\n${{
753
+ paragraph: "Write your description as natural flowing paragraphs.",
754
+ bullets: "Format your description as clear bullet points.",
755
+ structured: "Structure your description with sections: Overview, Main Elements, Details, Observations."
756
+ }[format]}`;
757
+ return (await gerbil.generate(prompt, {
758
+ images: [{ source: image }],
759
+ maxTokens: detailLengths[detail],
760
+ temperature: this.temperature
761
+ })).text;
762
+ }
763
+ });
764
+
765
+ //#endregion
766
+ //#region src/skills/builtin/explain.ts
767
+ /**
768
+ * Explain Skill
769
+ *
770
+ * Explain code or concepts at various levels.
771
+ * Optionally speaks the explanation aloud.
772
+ */
773
+ const ExplainInput = z.object({
774
+ content: z.string(),
775
+ level: z.enum([
776
+ "beginner",
777
+ "intermediate",
778
+ "expert"
779
+ ]).default("intermediate"),
780
+ language: z.string().optional(),
781
+ speak: z.boolean().default(false),
782
+ voice: z.enum([
783
+ "af_heart",
784
+ "af_bella",
785
+ "bf_emma",
786
+ "am_fenrir"
787
+ ]).default("af_heart")
788
+ });
789
+ const levelGuide = {
790
+ beginner: "Explain like I'm new to programming. Use simple terms and analogies.",
791
+ intermediate: "Explain for someone with programming experience.",
792
+ expert: "Explain with technical depth, including implementation details."
793
+ };
794
+ function saveWav$2(filename, audio, sampleRate) {
795
+ const buffer = Buffer.alloc(44 + audio.length * 2);
796
+ buffer.write("RIFF", 0);
797
+ buffer.writeUInt32LE(36 + audio.length * 2, 4);
798
+ buffer.write("WAVE", 8);
799
+ buffer.write("fmt ", 12);
800
+ buffer.writeUInt32LE(16, 16);
801
+ buffer.writeUInt16LE(1, 20);
802
+ buffer.writeUInt16LE(1, 22);
803
+ buffer.writeUInt32LE(sampleRate, 24);
804
+ buffer.writeUInt32LE(sampleRate * 2, 28);
805
+ buffer.writeUInt16LE(2, 32);
806
+ buffer.writeUInt16LE(16, 34);
807
+ buffer.write("data", 36);
808
+ buffer.writeUInt32LE(audio.length * 2, 40);
809
+ for (let i = 0; i < audio.length; i++) {
810
+ const s = Math.max(-1, Math.min(1, audio[i]));
811
+ buffer.writeInt16LE(Math.round(s * 32767), 44 + i * 2);
812
+ }
813
+ writeFileSync(filename, buffer);
814
+ }
815
+ function playAudio$1(audioFile) {
816
+ const platform = process.platform;
817
+ if (platform === "darwin") execSync(`afplay "${audioFile}"`, { stdio: "inherit" });
818
+ else if (platform === "linux") try {
819
+ execSync(`aplay "${audioFile}"`, { stdio: "inherit" });
820
+ } catch {
821
+ execSync(`paplay "${audioFile}"`, { stdio: "inherit" });
822
+ }
823
+ else if (platform === "win32") execSync(`powershell -c "(New-Object Media.SoundPlayer '${audioFile}').PlaySync()"`, { stdio: "inherit" });
824
+ }
825
+ const explain = defineSkill({
826
+ name: "explain",
827
+ description: "Explain code or concepts at various levels (optionally speak aloud)",
828
+ version: "1.0.0",
829
+ input: ExplainInput,
830
+ temperature: .5,
831
+ maxTokens: 500,
832
+ async run({ input, gerbil }) {
833
+ const { content, level = "intermediate", language, speak: speak$1 = false, voice = "af_heart" } = input;
834
+ let systemPrompt = `You are a patient teacher.
835
+ ${levelGuide[level]}`;
836
+ if (language) systemPrompt += `\nThis is ${language} code.`;
837
+ systemPrompt += "\nBe clear and concise.";
838
+ const explanation = (await gerbil.generate(`Explain this:\n\n${content}`, {
839
+ system: systemPrompt,
840
+ maxTokens: this.maxTokens,
841
+ temperature: this.temperature
842
+ })).text;
843
+ if (speak$1) {
844
+ const sentences = explanation.split(/(?<=[.!?])\s+/).filter((s) => s.trim());
845
+ for (const sentence of sentences) {
846
+ if (!sentence.trim()) continue;
847
+ const speechResult = await gerbil.speak(sentence, { voice });
848
+ const tempFile = join(tmpdir(), `gerbil-explain-${Date.now()}.wav`);
849
+ saveWav$2(tempFile, speechResult.audio, speechResult.sampleRate);
850
+ try {
851
+ playAudio$1(tempFile);
852
+ } finally {
853
+ try {
854
+ unlinkSync(tempFile);
855
+ } catch {}
856
+ }
857
+ }
858
+ }
859
+ return explanation;
860
+ }
861
+ });
862
+
863
+ //#endregion
864
+ //#region src/skills/builtin/extract.ts
865
+ /**
866
+ * Extract Skill
867
+ *
868
+ * Extract structured data from content.
869
+ */
870
+ const ExtractInput = z.object({
871
+ content: z.string(),
872
+ schema: z.any(),
873
+ context: z.string().optional()
874
+ });
875
+ const extract = defineSkill({
876
+ name: "extract",
877
+ description: "Extract structured data from content",
878
+ version: "1.0.0",
879
+ input: ExtractInput,
880
+ temperature: .3,
881
+ async run({ input, gerbil }) {
882
+ const { content, schema, context } = input;
883
+ let prompt = "Extract structured data from the following content.";
884
+ if (context) prompt += `\nContext: ${context}`;
885
+ prompt += `\n\nContent:\n${content}`;
886
+ return gerbil.json(prompt, { schema });
887
+ }
888
+ });
889
+
890
+ //#endregion
891
+ //#region src/skills/builtin/extract-from-image.ts
892
+ /**
893
+ * Extract from Image Skill
894
+ *
895
+ * Extract text, code, data, or structured information from images.
896
+ */
897
+ const ExtractFromImageInput = z.object({
898
+ image: z.string(),
899
+ extract: z.enum([
900
+ "text",
901
+ "code",
902
+ "data",
903
+ "table",
904
+ "diagram",
905
+ "form",
906
+ "receipt"
907
+ ]).default("text"),
908
+ outputFormat: z.enum([
909
+ "raw",
910
+ "json",
911
+ "markdown",
912
+ "csv"
913
+ ]).default("raw"),
914
+ language: z.string().optional()
915
+ });
916
+ const extractionPrompts = {
917
+ text: `Extract all visible text from this image.
918
+ - Preserve the original formatting and structure as much as possible
919
+ - Include headings, paragraphs, and any labels
920
+ - Note any text that's unclear or partially visible
921
+ - Maintain the reading order`,
922
+ code: `Extract the code visible in this image.
923
+ - Preserve exact syntax, indentation, and formatting
924
+ - Include comments if visible
925
+ - Note any parts that are unclear
926
+ - Format as a proper code block`,
927
+ data: `Extract all data, numbers, and structured information from this image.
928
+ - Include labels and their associated values
929
+ - Preserve numerical precision
930
+ - Note units and currencies
931
+ - Identify any patterns or relationships`,
932
+ table: `Extract the table data from this image.
933
+ - Identify all columns and rows
934
+ - Preserve cell alignment where meaningful
935
+ - Handle merged cells appropriately
936
+ - Include headers and any totals`,
937
+ diagram: `Describe and extract information from this diagram or flowchart.
938
+ - List all nodes/boxes and their labels
939
+ - Describe connections and their directions
940
+ - Note any annotations or legends
941
+ - Explain the flow or relationships`,
942
+ form: `Extract form fields and their values from this image.
943
+ - List each field label and its value
944
+ - Note required fields if indicated
945
+ - Include dropdown selections
946
+ - Preserve the form structure`,
947
+ receipt: `Extract receipt/invoice information from this image.
948
+ - Vendor/store name
949
+ - Date and time
950
+ - Line items with quantities and prices
951
+ - Subtotals, taxes, and total
952
+ - Payment method if shown`
953
+ };
954
+ const formatInstructions = {
955
+ raw: "Output the extracted content as plain text.",
956
+ json: "Output the extracted content as valid JSON with appropriate structure.",
957
+ markdown: "Format the output as Markdown with proper headings and formatting.",
958
+ csv: "Output tabular data in CSV format with proper quoting."
959
+ };
960
+ const extractFromImage = defineSkill({
961
+ name: "extract-from-image",
962
+ description: "Extract text, code, tables, or data from images",
963
+ version: "1.0.0",
964
+ model: "ministral-3b",
965
+ input: ExtractFromImageInput,
966
+ maxTokens: 2e3,
967
+ temperature: .1,
968
+ async run({ input, gerbil }) {
969
+ const { image, extract: extract$1 = "text", outputFormat = "raw", language } = input;
970
+ if (!gerbil.supportsVision()) throw new Error(`Current model doesn't support vision. Load a vision model like "ministral-3b" first.`);
971
+ let prompt = extractionPrompts[extract$1];
972
+ if (language && extract$1 === "code") prompt += `\n\nThe code appears to be ${language}.`;
973
+ prompt += `\n\n${formatInstructions[outputFormat]}`;
974
+ return (await gerbil.generate(prompt, {
975
+ images: [{ source: image }],
976
+ maxTokens: this.maxTokens,
977
+ temperature: this.temperature
978
+ })).text;
979
+ }
980
+ });
981
+
982
+ //#endregion
983
+ //#region src/skills/builtin/read-aloud.ts
984
+ /**
985
+ * Read Aloud Skill
986
+ *
987
+ * Read text or file content aloud using TTS.
988
+ * Supports streaming for long content.
989
+ */
990
+ const ReadAloudInput = z.object({
991
+ content: z.string(),
992
+ isFile: z.boolean().optional(),
993
+ voice: z.enum([
994
+ "af_heart",
995
+ "af_bella",
996
+ "af_nicole",
997
+ "am_fenrir",
998
+ "am_michael",
999
+ "bf_emma",
1000
+ "bm_george"
1001
+ ]).default("af_heart"),
1002
+ speed: z.number().min(.5).max(2).default(1),
1003
+ maxLength: z.number().default(5e3),
1004
+ summarizeIfLong: z.boolean().default(false)
1005
+ });
1006
+ function saveWav$1(filename, audio, sampleRate) {
1007
+ const buffer = Buffer.alloc(44 + audio.length * 2);
1008
+ buffer.write("RIFF", 0);
1009
+ buffer.writeUInt32LE(36 + audio.length * 2, 4);
1010
+ buffer.write("WAVE", 8);
1011
+ buffer.write("fmt ", 12);
1012
+ buffer.writeUInt32LE(16, 16);
1013
+ buffer.writeUInt16LE(1, 20);
1014
+ buffer.writeUInt16LE(1, 22);
1015
+ buffer.writeUInt32LE(sampleRate, 24);
1016
+ buffer.writeUInt32LE(sampleRate * 2, 28);
1017
+ buffer.writeUInt16LE(2, 32);
1018
+ buffer.writeUInt16LE(16, 34);
1019
+ buffer.write("data", 36);
1020
+ buffer.writeUInt32LE(audio.length * 2, 40);
1021
+ for (let i = 0; i < audio.length; i++) {
1022
+ const s = Math.max(-1, Math.min(1, audio[i]));
1023
+ buffer.writeInt16LE(Math.round(s * 32767), 44 + i * 2);
1024
+ }
1025
+ writeFileSync$1(filename, buffer);
1026
+ }
1027
+ function playAudio(audioFile) {
1028
+ const platform = process.platform;
1029
+ if (platform === "darwin") execSync$1(`afplay "${audioFile}"`, { stdio: "inherit" });
1030
+ else if (platform === "linux") try {
1031
+ execSync$1(`aplay "${audioFile}"`, { stdio: "inherit" });
1032
+ } catch {
1033
+ execSync$1(`paplay "${audioFile}"`, { stdio: "inherit" });
1034
+ }
1035
+ else if (platform === "win32") execSync$1(`powershell -c "(New-Object Media.SoundPlayer '${audioFile}').PlaySync()"`, { stdio: "inherit" });
1036
+ }
1037
+ const readAloud = defineSkill({
1038
+ name: "read-aloud",
1039
+ description: "Read text or file content aloud using TTS",
1040
+ version: "1.0.0",
1041
+ input: ReadAloudInput,
1042
+ async run({ input, gerbil }) {
1043
+ const { content, isFile, voice = "af_heart", speed = 1, maxLength = 5e3, summarizeIfLong = false } = input;
1044
+ const shouldReadFile = isFile ?? existsSync$1(content);
1045
+ let textToRead;
1046
+ if (shouldReadFile) {
1047
+ if (!existsSync$1(content)) throw new Error(`File not found: ${content}`);
1048
+ textToRead = readFileSync$1(content, "utf-8");
1049
+ } else textToRead = content;
1050
+ if (textToRead.length > maxLength) if (summarizeIfLong) textToRead = (await gerbil.generate(`Summarize this content in a way that's good for reading aloud (2-3 paragraphs max):\n\n${textToRead.slice(0, 1e4)}`, {
1051
+ system: "You are a skilled narrator. Create a clear, spoken-word friendly summary.",
1052
+ maxTokens: 500,
1053
+ temperature: .3
1054
+ })).text;
1055
+ else textToRead = textToRead.slice(0, maxLength) + "...";
1056
+ const sentences = textToRead.split(/(?<=[.!?])\s+/).filter((s) => s.trim());
1057
+ let totalDuration = 0;
1058
+ for (const sentence of sentences) {
1059
+ if (!sentence.trim()) continue;
1060
+ const result = await gerbil.speak(sentence, {
1061
+ voice,
1062
+ speed
1063
+ });
1064
+ totalDuration += result.duration;
1065
+ const tempFile = join$1(tmpdir$1(), `gerbil-read-${Date.now()}.wav`);
1066
+ saveWav$1(tempFile, result.audio, result.sampleRate);
1067
+ try {
1068
+ playAudio(tempFile);
1069
+ } finally {
1070
+ try {
1071
+ unlinkSync$1(tempFile);
1072
+ } catch {}
1073
+ }
1074
+ }
1075
+ return `Read ${textToRead.length} characters from ${shouldReadFile ? `file "${content}"` : "text"} (${totalDuration.toFixed(1)}s audio)`;
1076
+ }
1077
+ });
1078
+
1079
+ //#endregion
1080
+ //#region src/skills/builtin/review.ts
1081
+ /**
1082
+ * Review Skill
1083
+ *
1084
+ * Code review with configurable focus areas.
1085
+ */
1086
+ const ReviewInput = z.object({
1087
+ code: z.string(),
1088
+ focus: z.array(z.enum([
1089
+ "security",
1090
+ "performance",
1091
+ "style",
1092
+ "bugs",
1093
+ "all"
1094
+ ])).default(["all"]),
1095
+ format: z.enum([
1096
+ "inline",
1097
+ "summary",
1098
+ "detailed"
1099
+ ]).default("summary")
1100
+ });
1101
+ const review = defineSkill({
1102
+ name: "review",
1103
+ description: "Code review with configurable focus areas",
1104
+ version: "1.0.0",
1105
+ input: ReviewInput,
1106
+ temperature: .3,
1107
+ maxTokens: 600,
1108
+ async run({ input, gerbil }) {
1109
+ const { code, focus = ["all"], format = "summary" } = input;
1110
+ let systemPrompt = `You are a senior code reviewer.
1111
+ Review the code for:`;
1112
+ if (focus.includes("all")) systemPrompt += "\n- Security vulnerabilities\n- Performance issues\n- Code style and readability\n- Potential bugs";
1113
+ else {
1114
+ if (focus.includes("security")) systemPrompt += "\n- Security vulnerabilities";
1115
+ if (focus.includes("performance")) systemPrompt += "\n- Performance issues";
1116
+ if (focus.includes("style")) systemPrompt += "\n- Code style and readability";
1117
+ if (focus.includes("bugs")) systemPrompt += "\n- Potential bugs";
1118
+ }
1119
+ if (format === "summary") systemPrompt += "\n\nProvide a brief summary of issues found.";
1120
+ else if (format === "detailed") systemPrompt += "\n\nProvide detailed feedback with suggestions.";
1121
+ else systemPrompt += "\n\nProvide inline-style comments.";
1122
+ return (await gerbil.generate(`Review this code:\n\n${code}`, {
1123
+ system: systemPrompt,
1124
+ maxTokens: this.maxTokens,
1125
+ temperature: this.temperature
1126
+ })).text;
1127
+ }
1128
+ });
1129
+
1130
+ //#endregion
1131
+ //#region src/skills/builtin/speak.ts
1132
+ /**
1133
+ * Speak Skill
1134
+ *
1135
+ * Convert text to speech using on-device TTS.
1136
+ */
1137
+ const SpeakInput = z.object({
1138
+ text: z.string(),
1139
+ voice: z.enum([
1140
+ "af_heart",
1141
+ "af_bella",
1142
+ "af_nicole",
1143
+ "af_sarah",
1144
+ "am_fenrir",
1145
+ "am_michael",
1146
+ "bf_emma",
1147
+ "bf_isabella",
1148
+ "bm_george",
1149
+ "bm_lewis"
1150
+ ]).default("af_heart"),
1151
+ speed: z.number().min(.5).max(2).default(1),
1152
+ output: z.string().optional()
1153
+ });
1154
+ /**
1155
+ * Save Float32Array as WAV file
1156
+ */
1157
+ function saveWav(filename, audio, sampleRate) {
1158
+ const buffer = Buffer.alloc(44 + audio.length * 2);
1159
+ buffer.write("RIFF", 0);
1160
+ buffer.writeUInt32LE(36 + audio.length * 2, 4);
1161
+ buffer.write("WAVE", 8);
1162
+ buffer.write("fmt ", 12);
1163
+ buffer.writeUInt32LE(16, 16);
1164
+ buffer.writeUInt16LE(1, 20);
1165
+ buffer.writeUInt16LE(1, 22);
1166
+ buffer.writeUInt32LE(sampleRate, 24);
1167
+ buffer.writeUInt32LE(sampleRate * 2, 28);
1168
+ buffer.writeUInt16LE(2, 32);
1169
+ buffer.writeUInt16LE(16, 34);
1170
+ buffer.write("data", 36);
1171
+ buffer.writeUInt32LE(audio.length * 2, 40);
1172
+ for (let i = 0; i < audio.length; i++) {
1173
+ const s = Math.max(-1, Math.min(1, audio[i]));
1174
+ buffer.writeInt16LE(Math.round(s * 32767), 44 + i * 2);
1175
+ }
1176
+ writeFileSync$1(filename, buffer);
1177
+ }
1178
+ const speak = defineSkill({
1179
+ name: "speak",
1180
+ description: "Convert text to speech using on-device TTS (Kokoro-82M)",
1181
+ version: "1.0.0",
1182
+ input: SpeakInput,
1183
+ async run({ input, gerbil }) {
1184
+ const { text, voice = "af_heart", speed = 1, output } = input;
1185
+ const result = await gerbil.speak(text, {
1186
+ voice,
1187
+ speed
1188
+ });
1189
+ if (output) {
1190
+ saveWav(output, result.audio, result.sampleRate);
1191
+ return `Saved ${result.duration.toFixed(1)}s of audio to ${output}`;
1192
+ }
1193
+ const tempFile = join$1(tmpdir$1(), `gerbil-speak-${Date.now()}.wav`);
1194
+ saveWav(tempFile, result.audio, result.sampleRate);
1195
+ try {
1196
+ const platform = process.platform;
1197
+ if (platform === "darwin") execSync$1(`afplay "${tempFile}"`, { stdio: "inherit" });
1198
+ else if (platform === "linux") try {
1199
+ execSync$1(`aplay "${tempFile}"`, { stdio: "inherit" });
1200
+ } catch {
1201
+ try {
1202
+ execSync$1(`paplay "${tempFile}"`, { stdio: "inherit" });
1203
+ } catch {
1204
+ execSync$1(`play "${tempFile}"`, { stdio: "inherit" });
1205
+ }
1206
+ }
1207
+ else if (platform === "win32") execSync$1(`powershell -c "(New-Object Media.SoundPlayer '${tempFile}').PlaySync()"`, { stdio: "inherit" });
1208
+ } finally {
1209
+ try {
1210
+ unlinkSync$1(tempFile);
1211
+ } catch {}
1212
+ }
1213
+ return `Spoke ${result.duration.toFixed(1)}s of audio (voice: ${voice}, speed: ${speed}x)`;
1214
+ }
1215
+ });
1216
+
1217
+ //#endregion
1218
+ //#region src/skills/builtin/summarize.ts
1219
+ /**
1220
+ * Summarize Skill
1221
+ *
1222
+ * Summarize content in various lengths and formats.
1223
+ */
1224
+ const SummarizeInput = z.object({
1225
+ content: z.string(),
1226
+ length: z.enum([
1227
+ "short",
1228
+ "medium",
1229
+ "long"
1230
+ ]).default("medium"),
1231
+ format: z.enum(["paragraph", "bullets"]).default("paragraph"),
1232
+ focus: z.array(z.string()).optional()
1233
+ });
1234
+ const lengthGuide = {
1235
+ short: "2-3 sentences",
1236
+ medium: "1 paragraph (4-6 sentences)",
1237
+ long: "2-3 paragraphs"
1238
+ };
1239
+ const maxTokensGuide = {
1240
+ short: 100,
1241
+ medium: 200,
1242
+ long: 400
1243
+ };
1244
+ const summarize = defineSkill({
1245
+ name: "summarize",
1246
+ description: "Summarize content in various lengths and formats",
1247
+ version: "1.0.0",
1248
+ input: SummarizeInput,
1249
+ temperature: .3,
1250
+ async run({ input, gerbil }) {
1251
+ const { content, length = "medium", format = "paragraph", focus } = input;
1252
+ let systemPrompt = `You are a summarization expert.
1253
+ Summarize the content in ${lengthGuide[length]}.`;
1254
+ if (format === "bullets") systemPrompt += "\nUse bullet points.";
1255
+ if (focus && focus.length > 0) systemPrompt += `\nFocus on: ${focus.join(", ")}.`;
1256
+ systemPrompt += "\nOnly output the summary, nothing else.";
1257
+ return (await gerbil.generate(`Summarize this:\n\n${content}`, {
1258
+ system: systemPrompt,
1259
+ maxTokens: maxTokensGuide[length],
1260
+ temperature: this.temperature
1261
+ })).text;
1262
+ }
1263
+ });
1264
+
1265
+ //#endregion
1266
+ //#region src/skills/builtin/test.ts
1267
+ /**
1268
+ * Test Skill
1269
+ *
1270
+ * Generate tests for code.
1271
+ */
1272
+ const TestInput = z.object({
1273
+ code: z.string(),
1274
+ framework: z.enum([
1275
+ "jest",
1276
+ "vitest",
1277
+ "mocha",
1278
+ "playwright"
1279
+ ]).default("vitest"),
1280
+ style: z.enum([
1281
+ "unit",
1282
+ "integration",
1283
+ "e2e"
1284
+ ]).default("unit")
1285
+ });
1286
+ const test = defineSkill({
1287
+ name: "test",
1288
+ description: "Generate tests for code",
1289
+ version: "1.0.0",
1290
+ input: TestInput,
1291
+ temperature: .3,
1292
+ maxTokens: 800,
1293
+ async run({ input, gerbil }) {
1294
+ const { code, framework, style } = input;
1295
+ const systemPrompt = `You are a test engineer.
1296
+ Generate ${style} tests using ${framework} for the provided code.
1297
+ Include edge cases and error scenarios.
1298
+ Only output the test code, no explanations.`;
1299
+ return (await gerbil.generate(`Generate tests for:\n\n${code}`, {
1300
+ system: systemPrompt,
1301
+ maxTokens: this.maxTokens,
1302
+ temperature: this.temperature
1303
+ })).text;
1304
+ }
1305
+ });
1306
+
1307
+ //#endregion
1308
+ //#region src/skills/builtin/title.ts
1309
+ /**
1310
+ * Title Skill
1311
+ *
1312
+ * Generate titles for content.
1313
+ */
1314
+ const TitleInput = z.object({
1315
+ content: z.string(),
1316
+ style: z.enum([
1317
+ "professional",
1318
+ "clickbait",
1319
+ "seo",
1320
+ "simple"
1321
+ ]).default("professional"),
1322
+ maxLength: z.number().default(60)
1323
+ });
1324
+ const styleGuide = {
1325
+ professional: "Clear, informative, and professional",
1326
+ clickbait: "Engaging and curiosity-inducing",
1327
+ seo: "SEO-optimized with relevant keywords",
1328
+ simple: "Simple and straightforward"
1329
+ };
1330
+ const title = defineSkill({
1331
+ name: "title",
1332
+ description: "Generate titles for content",
1333
+ version: "1.0.0",
1334
+ input: TitleInput,
1335
+ temperature: .7,
1336
+ maxTokens: 30,
1337
+ async run({ input, gerbil }) {
1338
+ const { content, style = "professional", maxLength = 60 } = input;
1339
+ const systemPrompt = `Generate a title for the content.
1340
+ Style: ${styleGuide[style]}
1341
+ Max length: ${maxLength} characters
1342
+ Only output the title, nothing else.`;
1343
+ let generatedTitle = (await gerbil.generate(`Generate a title for:\n\n${content.substring(0, 1e3)}`, {
1344
+ system: systemPrompt,
1345
+ maxTokens: this.maxTokens,
1346
+ temperature: this.temperature
1347
+ })).text.trim();
1348
+ generatedTitle = generatedTitle.replace(/^["']|["']$/g, "");
1349
+ if (generatedTitle.length > maxLength) generatedTitle = `${generatedTitle.substring(0, maxLength - 3)}...`;
1350
+ return generatedTitle;
1351
+ }
1352
+ });
1353
+
1354
+ //#endregion
1355
+ //#region src/skills/builtin/transcribe.ts
1356
+ /**
1357
+ * Transcribe Skill
1358
+ *
1359
+ * Convert audio to text using on-device STT (Whisper).
1360
+ */
1361
+ const TranscribeInput = z.object({
1362
+ audio: z.string(),
1363
+ model: z.enum([
1364
+ "whisper-tiny.en",
1365
+ "whisper-base.en",
1366
+ "whisper-small.en",
1367
+ "whisper-large-v3-turbo"
1368
+ ]).default("whisper-tiny.en"),
1369
+ language: z.string().optional(),
1370
+ timestamps: z.boolean().default(false),
1371
+ output: z.string().optional()
1372
+ });
1373
+ const transcribe = defineSkill({
1374
+ name: "transcribe",
1375
+ description: "Transcribe audio to text using on-device STT (Whisper)",
1376
+ version: "1.0.0",
1377
+ input: TranscribeInput,
1378
+ async run({ input, gerbil }) {
1379
+ const { audio, model = "whisper-tiny.en", language, timestamps = false, output } = input;
1380
+ const filePath = resolve(audio);
1381
+ if (!existsSync$1(filePath)) throw new Error(`Audio file not found: ${filePath}`);
1382
+ const audioData = new Uint8Array(readFileSync$1(filePath));
1383
+ await gerbil.loadSTT(model);
1384
+ const result = await gerbil.transcribe(audioData, {
1385
+ language,
1386
+ timestamps
1387
+ });
1388
+ let text;
1389
+ if (timestamps && result.segments) text = result.segments.map((seg) => `[${seg.start.toFixed(1)}s - ${seg.end.toFixed(1)}s] ${seg.text}`).join("\n");
1390
+ else text = result.text;
1391
+ if (output) {
1392
+ writeFileSync$1(output, text);
1393
+ return `Transcribed ${result.duration.toFixed(1)}s of audio to ${output}`;
1394
+ }
1395
+ return text;
1396
+ }
1397
+ });
1398
+
1399
+ //#endregion
1400
+ //#region src/skills/builtin/translate.ts
1401
+ /**
1402
+ * Translate Skill
1403
+ *
1404
+ * Translate text between languages.
1405
+ */
1406
+ const TranslateInput = z.object({
1407
+ text: z.string(),
1408
+ from: z.string().optional(),
1409
+ to: z.string(),
1410
+ preserveFormatting: z.boolean().default(true)
1411
+ });
1412
+ const translate = defineSkill({
1413
+ name: "translate",
1414
+ description: "Translate text between languages",
1415
+ version: "1.0.0",
1416
+ input: TranslateInput,
1417
+ temperature: .3,
1418
+ async run({ input, gerbil }) {
1419
+ const { text, from, to, preserveFormatting } = input;
1420
+ let systemPrompt = `You are a professional translator.
1421
+ Translate the text to ${to}.`;
1422
+ if (from) systemPrompt += `\nThe source language is ${from}.`;
1423
+ if (preserveFormatting) systemPrompt += "\nPreserve the original formatting (paragraphs, lists, etc.).";
1424
+ systemPrompt += "\nOnly output the translation, nothing else.";
1425
+ return (await gerbil.generate(`Translate:\n\n${text}`, {
1426
+ system: systemPrompt,
1427
+ maxTokens: Math.ceil(text.length / 2),
1428
+ temperature: this.temperature
1429
+ })).text;
1430
+ }
1431
+ });
1432
+
1433
+ //#endregion
1434
+ export { defineSkill as C, listSkills as D, hasSkill as E, removeSkill as O, clearSkills as S, getSkillInfo as T, analyzeScreenshot as _, summarize as a, loadSkillPackage as b, readAloud as c, explain as d, describeImage as f, announce as g, captionImage as h, test as i, useSkill as k, extractFromImage as l, commit as m, transcribe as n, speak as o, compareImages as p, title as r, review as s, translate as t, extract as u, loadProjectSkills as v, getAllSkillInfo as w, loadSkills as x, loadSkill as y };
1435
+ //# sourceMappingURL=skills-5DxAV-rn.mjs.map