@tryhamster/gerbil 1.6.3 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{architectures-CpmH4Gha.mjs → architectures-BHkqQ9xp.mjs} +1 -1
- package/dist/{architectures-CpmH4Gha.mjs.map → architectures-BHkqQ9xp.mjs.map} +1 -1
- package/dist/browser/index.d.ts +8 -0
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/cli.mjs +13 -13
- package/dist/cli.mjs.map +1 -1
- package/dist/{defaults-KnrRSIeJ.mjs → defaults-C_bJK9zs.mjs} +1 -1
- package/dist/{defaults-KnrRSIeJ.mjs.map → defaults-C_bJK9zs.mjs.map} +1 -1
- package/dist/frameworks/express.d.mts +2 -2
- package/dist/frameworks/express.mjs +3 -3
- package/dist/frameworks/fastify.d.mts +2 -2
- package/dist/frameworks/fastify.mjs +2 -2
- package/dist/frameworks/hono.d.mts +2 -2
- package/dist/frameworks/hono.mjs +2 -2
- package/dist/frameworks/next.d.mts +4 -4
- package/dist/frameworks/next.mjs +2 -2
- package/dist/frameworks/react.d.mts +2 -2
- package/dist/frameworks/trpc.d.mts +2 -2
- package/dist/frameworks/trpc.mjs +2 -2
- package/dist/gerbil-BFk5jV0h.mjs +4 -0
- package/dist/{gerbil-qJhKR8JR.mjs → gerbil-Cgmb4Dit.mjs} +41 -5
- package/dist/gerbil-Cgmb4Dit.mjs.map +1 -0
- package/dist/{gerbil-BHPSiECS.d.mts → gerbil-DU1aRO6v.d.mts} +25 -4
- package/dist/gerbil-DU1aRO6v.d.mts.map +1 -0
- package/dist/gpu/hooks.d.mts +3 -3
- package/dist/gpu/hooks.mjs +2 -2
- package/dist/gpu/index.d.mts +3 -3
- package/dist/gpu/index.mjs +5 -5
- package/dist/{gpu-BZVtzX9N.mjs → gpu-kQVLpV3n.mjs} +4 -4
- package/dist/{gpu-BZVtzX9N.mjs.map → gpu-kQVLpV3n.mjs.map} +1 -1
- package/dist/{index-CRDf-nS4.d.mts → index-B3tjyDJI.d.mts} +2 -2
- package/dist/{index-CRDf-nS4.d.mts.map → index-B3tjyDJI.d.mts.map} +1 -1
- package/dist/{index-CZVIHkaH.d.mts → index-ElJKy9i9.d.mts} +119 -26
- package/dist/index-ElJKy9i9.d.mts.map +1 -0
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +11 -11
- package/dist/indexeddb-store-B6HYVgQ_.mjs +4 -0
- package/dist/{indexeddb-store-BWIMtxxH.mjs → indexeddb-store-BiamQL7K.mjs} +2 -2
- package/dist/{indexeddb-store-BWIMtxxH.mjs.map → indexeddb-store-BiamQL7K.mjs.map} +1 -1
- package/dist/integrations/ai-sdk.d.mts +2 -2
- package/dist/integrations/ai-sdk.mjs +2 -2
- package/dist/integrations/langchain.d.mts +2 -2
- package/dist/integrations/langchain.mjs +2 -2
- package/dist/integrations/llamaindex.d.mts +2 -2
- package/dist/integrations/llamaindex.mjs +2 -2
- package/dist/integrations/mcp-client.mjs +2 -2
- package/dist/integrations/mcp.d.mts +4 -4
- package/dist/integrations/mcp.mjs +5 -5
- package/dist/{mcp-DIygAeaN.mjs → mcp-DTusP2m5.mjs} +3 -3
- package/dist/{mcp-DIygAeaN.mjs.map → mcp-DTusP2m5.mjs.map} +1 -1
- package/dist/memory/index.d.mts +2 -2
- package/dist/memory/index.mjs +4 -4
- package/dist/{memory-Dj0J1v88.mjs → memory-BCKc_Jfp.mjs} +2 -2
- package/dist/{memory-Dj0J1v88.mjs.map → memory-BCKc_Jfp.mjs.map} +1 -1
- package/dist/memory-BwYh_DZh.mjs +4 -0
- package/dist/{memory-DVN0MnIG.mjs → memory-Dnjhc1du.mjs} +2 -2
- package/dist/{memory-DVN0MnIG.mjs.map → memory-Dnjhc1du.mjs.map} +1 -1
- package/dist/{moonshine-stt-BFIm_Xxk.mjs → moonshine-stt-9wc6t11v.mjs} +341 -127
- package/dist/moonshine-stt-9wc6t11v.mjs.map +1 -0
- package/dist/moonshine-stt-B7rDn6Q9.mjs +4 -0
- package/dist/{one-liner-CGozk7xR.mjs → one-liner-Bk5x7gYH.mjs} +2 -2
- package/dist/{one-liner-CGozk7xR.mjs.map → one-liner-Bk5x7gYH.mjs.map} +1 -1
- package/dist/{outetts-Cr5oGk4z.d.mts → outetts-CAL3_K3j.d.mts} +1 -1
- package/dist/{outetts-Cr5oGk4z.d.mts.map → outetts-CAL3_K3j.d.mts.map} +1 -1
- package/dist/repl-DV6l-jT8.mjs +9 -0
- package/dist/skills/index.d.mts +4 -4
- package/dist/skills/index.mjs +4 -4
- package/dist/{skills-DJ0cCjfD.mjs → skills-BhcwnL2l.mjs} +2 -2
- package/dist/{skills-DJ0cCjfD.mjs.map → skills-BhcwnL2l.mjs.map} +1 -1
- package/dist/{tools-DQ1mPUw5.mjs → tools-DRYfLfKS.mjs} +2 -2
- package/dist/{tools-DQ1mPUw5.mjs.map → tools-DRYfLfKS.mjs.map} +1 -1
- package/dist/tune/index.d.mts +186 -0
- package/dist/tune/index.d.mts.map +1 -0
- package/dist/tune/index.mjs +297 -0
- package/dist/tune/index.mjs.map +1 -0
- package/dist/{types-BBDAW4MS.d.mts → types-BzbBDoaP.d.mts} +10 -2
- package/dist/types-BzbBDoaP.d.mts.map +1 -0
- package/dist/{types-Dqv4m0vo.d.mts → types-CIcNt_XS.d.mts} +1 -1
- package/dist/{types-Dqv4m0vo.d.mts.map → types-CIcNt_XS.d.mts.map} +1 -1
- package/dist/{utils-DKO55ZmZ.mjs → utils-CZBZ8dgR.mjs} +1 -1
- package/dist/{utils-DKO55ZmZ.mjs.map → utils-CZBZ8dgR.mjs.map} +1 -1
- package/dist/{vector-B0panuy6.mjs → vector-BJqdOci_.mjs} +1 -1
- package/dist/{vector-B0panuy6.mjs.map → vector-BJqdOci_.mjs.map} +1 -1
- package/package.json +5 -1
- package/dist/gerbil-BHPSiECS.d.mts.map +0 -1
- package/dist/gerbil-eX40N7oB.mjs +0 -4
- package/dist/gerbil-qJhKR8JR.mjs.map +0 -1
- package/dist/index-CZVIHkaH.d.mts.map +0 -1
- package/dist/indexeddb-store-ClH12Xnl.mjs +0 -4
- package/dist/memory-D1P7Tmda.mjs +0 -4
- package/dist/moonshine-stt-BFIm_Xxk.mjs.map +0 -1
- package/dist/moonshine-stt-D7BqG2pU.mjs +0 -4
- package/dist/repl-C_-ogJ09.mjs +0 -9
- package/dist/types-BBDAW4MS.d.mts.map +0 -1
- /package/dist/{auto-update-BVaLXcDE.mjs → auto-update-DIh0uiTw.mjs} +0 -0
- /package/dist/{chunk-B9cbKln6.mjs → chunk-BeNUM--j.mjs} +0 -0
- /package/dist/{microphone-Bqmoz9_K.mjs → microphone-CsUwVP2O.mjs} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as zodToJsonSchema } from "./utils-
|
|
1
|
+
import { n as zodToJsonSchema } from "./utils-CZBZ8dgR.mjs";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
|
|
4
4
|
//#region src/core/docs.ts
|
|
@@ -575,4 +575,4 @@ const docsTool = defineTool({
|
|
|
575
575
|
|
|
576
576
|
//#endregion
|
|
577
577
|
export { getToolDefinitions as a, setToolContext as c, getTool as i, executeToolCall as n, loadProjectTools as o, formatToolsForPrompt as r, parseToolCall as s, defineTool as t };
|
|
578
|
-
//# sourceMappingURL=tools-
|
|
578
|
+
//# sourceMappingURL=tools-DRYfLfKS.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools-DQ1mPUw5.mjs","names":["GERBIL_DOCS: Record<string, string>","matches: string[]","toolContext: ToolContext | null","results: LoadedToolResult[]"],"sources":["../src/core/docs.ts","../src/core/tools.ts"],"sourcesContent":["/**\n * Gerbil Documentation Content\n *\n * Documentation strings used by the gerbil_docs tool\n * for answering questions about Gerbil features.\n */\n\nexport const GERBIL_DOCS: Record<string, string> = {\n // Core\n quickstart: `Gerbil Quick Start:\n\\`\\`\\`typescript\nimport { Gerbil } from \"gerbil\";\n\nconst g = new Gerbil();\nawait g.loadModel(\"qwen3.5-0.8b\");\n\n// Generate text\nconst result = await g.generate(\"Write a haiku\");\nconsole.log(result.text);\n\n// Stream responses\nfor await (const chunk of g.stream(\"Tell me a story\")) {\n process.stdout.write(chunk);\n}\n\\`\\`\\``,\n\n generate: `Generate text with options:\n\\`\\`\\`typescript\nconst result = await gerbil.generate(prompt, {\n maxTokens: 256, // Max tokens to generate\n temperature: 0.7, // 0-2, higher = more creative\n topP: 0.9, // Nucleus sampling\n topK: 50, // Top-k sampling\n thinking: false, // Enable reasoning mode (Qwen3)\n system: \"You are...\", // System prompt\n});\n\n// Result contains:\nresult.text // Generated text\nresult.thinking // Reasoning (if thinking=true)\nresult.tokensGenerated\nresult.tokensPerSecond\nresult.totalTime\n\\`\\`\\``,\n\n stream: `Streaming responses:\n\\`\\`\\`typescript\nfor await (const chunk of gerbil.stream(\"Hello\")) {\n process.stdout.write(chunk);\n}\n\n// With options\nconst stream = gerbil.stream(\"Explain React\", {\n maxTokens: 500,\n onToken: (token) => console.log(token),\n});\n\\`\\`\\``,\n\n json: `Structured JSON output with Zod validation:\n\\`\\`\\`typescript\nimport { z } from \"zod\";\n\nconst PersonSchema = z.object({\n name: z.string(),\n age: z.number(),\n city: z.string(),\n});\n\nconst data = await gerbil.json(\"Extract: John is 32 and lives in NYC\", {\n schema: PersonSchema,\n retries: 3, // Retry on validation failure\n temperature: 0.3, // Lower = more consistent\n});\n// { name: \"John\", age: 32, city: \"NYC\" }\n\\`\\`\\``,\n\n \"generate-object\": `Structured object output with retries (generateObject):\n\\`\\`\\`typescript\nimport { generateObject } from \"@tryhamster/gerbil\";\n\n// Validate with a minimal { required } schema or a predicate; omit to accept any JSON.\nconst { object, attempts } = await generateObject(\n 'Extract {name, age} from: \"I am Sarah, 28\"',\n { schema: { required: [\"name\", \"age\"] }, maxRetries: 4 },\n);\n// object === { name: \"Sarah\", age: 28 }\n\\`\\`\\`\n\nAlso on the Gerbil class (g.generateObject), the engine\n(WebGPUEngine.generateObject), the useObject React hook, and the CLI\n(gerbil object \"...\" --schema person.json). Unlike json() (Zod), this uses a\npredicate/required-keys validator and retries with a corrective nudge.`,\n\n thinking: `Thinking mode (chain-of-thought reasoning) - Qwen3 models:\n\\`\\`\\`typescript\nconst result = await gerbil.generate(\"What is 127 × 43?\", {\n thinking: true,\n});\n\nconsole.log(result.thinking);\n// \"Let me calculate step by step: 127 × 43 = 127 × 40 + 127 × 3...\"\n\nconsole.log(result.text);\n// \"5461\"\n\\`\\`\\`\n\nEnable in chat with /no_think or /think commands, or enable_thinking parameter.`,\n\n embed: `Generate embeddings:\n\\`\\`\\`typescript\n// Single text\nconst { vector } = await gerbil.embed(\"Hello world\");\n// vector: number[] (384 dimensions by default)\n\n// Batch\nconst results = await gerbil.embedBatch([\"Hello\", \"World\"]);\n\\`\\`\\``,\n\n // Models\n models: `Available models (native WebGPU engine, INT4 on-device):\n\n| Model | Size | Best For |\n|-------|------|----------|\n| qwen3.5-0.8b | ~1.6GB | Default — fast, multimodal, 262k context, thinking mode |\n| qwen3.5-2b | ~4GB | Best quality, multimodal, 262k context, thinking mode |\n| lfm2.5-1.2b-thinking | ~2.4GB | Efficient reasoning, 128k context |\n\nLoad any HuggingFace model with a supported architecture (Qwen2/Qwen3/Qwen3.5/LFM2):\n\\`\\`\\`typescript\nawait gerbil.loadModel(\"hf:org/model\");\nawait gerbil.loadModel(\"Qwen/Qwen3.5-0.8B\");\n\\`\\`\\``,\n\n load: `Loading models:\n\\`\\`\\`typescript\nawait gerbil.loadModel(\"qwen3.5-0.8b\", {\n device: \"webgpu\", // native WebGPU engine (Dawn in Node, WebGPU in browser)\n onProgress: (p) => {\n console.log(p.status, p.progress, p.file);\n },\n});\n\\`\\`\\``,\n\n // AI SDK\n \"ai-sdk\": `Vercel AI SDK v5 integration:\n\\`\\`\\`typescript\nimport { generateText, streamText } from \"ai\";\nimport { gerbil } from \"gerbil/ai\";\n\n// Generate\nconst { text } = await generateText({\n model: gerbil(\"qwen3.5-0.8b\"),\n prompt: \"Write a commit message\",\n});\n\n// Stream\nconst stream = streamText({\n model: gerbil(\"qwen3.5-0.8b\"),\n prompt: \"Explain React hooks\",\n});\n\nfor await (const chunk of stream.textStream) {\n process.stdout.write(chunk);\n}\n\n// With thinking mode\nconst { text } = await generateText({\n model: gerbil(\"qwen3.5-0.8b\", { thinking: true }),\n prompt: \"What is 127 × 43?\",\n});\n\\`\\`\\``,\n\n // Frameworks\n next: `Next.js App Router integration:\n\\`\\`\\`typescript\n// app/api/chat/route.ts\nimport { gerbil } from \"gerbil/next\";\n\nexport const POST = gerbil.handler({ \n model: \"qwen3.5-0.8b\",\n maxTokens: 500,\n});\n\\`\\`\\``,\n\n express: `Express.js integration:\n\\`\\`\\`typescript\nimport express from \"express\";\nimport { gerbil } from \"gerbil/express\";\n\nconst app = express();\napp.use(\"/ai\", gerbil()());\n\n// Endpoints created:\n// POST /ai/generate - Generate text\n// POST /ai/stream - Stream text\n// POST /ai/json - Structured output\n// POST /ai/embed - Embeddings\n\\`\\`\\``,\n\n react: `React hooks:\n\\`\\`\\`typescript\nimport { useGerbil, useChat } from \"gerbil/react\";\n\nfunction Chat() {\n const { generate, isLoading } = useGerbil();\n \n const handleSubmit = async (prompt: string) => {\n const result = await generate(prompt);\n console.log(result.text);\n };\n}\n\\`\\`\\``,\n\n hono: `Hono integration:\n\\`\\`\\`typescript\nimport { Hono } from \"hono\";\nimport { gerbil } from \"gerbil/hono\";\n\nconst app = new Hono();\napp.route(\"/ai\", await gerbil()());\n\\`\\`\\``,\n\n langchain: `LangChain integration:\n\\`\\`\\`typescript\nimport { GerbilLLM, GerbilEmbeddings } from \"gerbil/langchain\";\n\nconst llm = new GerbilLLM({ model: \"qwen3.5-0.8b\" });\nconst embeddings = new GerbilEmbeddings();\n\n// Use with chains\nconst chain = new LLMChain({ llm, prompt: template });\n\\`\\`\\``,\n\n mcp: `MCP Server for Claude Desktop & Cursor:\n\\`\\`\\`bash\ngerbil serve --mcp\n\\`\\`\\`\n\nSkills are automatically exposed as MCP tools:\n- gerbil_generate\n- gerbil_commit\n- gerbil_summarize\n- gerbil_explain\n- etc.`,\n\n // Skills\n skills: `Skills are reusable AI tasks with Zod validation:\n\\`\\`\\`typescript\nimport { commit, summarize, explain, review } from \"gerbil/skills\";\n\n// Generate commit message\nconst msg = await commit({ type: \"conventional\", maxLength: 72 });\n\n// Summarize content\nconst summary = await summarize({ \n content: doc, \n length: \"short\",\n format: \"bullets\" \n});\n\n// Explain code\nconst explanation = await explain({ \n content: code, \n level: \"beginner\" \n});\n\n// Code review\nconst feedback = await review({ \n code, \n focus: [\"security\", \"performance\"] \n});\n\\`\\`\\`\n\nBuilt-in skills: commit, summarize, explain, review, test, translate, extract, title`,\n\n \"define-skill\": `Create custom skills:\n\\`\\`\\`typescript\nimport { defineSkill } from \"gerbil/skills\";\nimport { z } from \"zod\";\n\nexport const sentiment = defineSkill({\n name: \"sentiment\",\n description: \"Analyze text sentiment\",\n input: z.object({ \n text: z.string(),\n detailed: z.boolean().default(false)\n }),\n output: z.object({\n sentiment: z.enum([\"positive\", \"negative\", \"neutral\"]),\n confidence: z.number(),\n }),\n temperature: 0.3,\n maxTokens: 100,\n \n async run({ input, gerbil }) {\n return gerbil.json(\\`Sentiment of: \\${input.text}\\`, {\n schema: this.output,\n });\n },\n});\n\\`\\`\\``,\n\n \"load-skills\": `Load skills from files:\n\\`\\`\\`typescript\nimport { loadSkills, useSkill, listSkills } from \"gerbil/skills\";\n\n// Load from directory (*.skill.ts files)\nawait loadSkills(\"./skills\");\n\n// Use by name\nconst skill = useSkill(\"my-skill\");\nconst result = await skill({ text: \"Hello\" });\n\n// List all\nconsole.log(listSkills()); // [\"commit\", \"summarize\", ...]\n\\`\\`\\``,\n\n // CLI\n cli: `CLI commands:\n\\`\\`\\`bash\ngerbil \"Write a haiku\" # Generate text\ngerbil -m qwen3.5-0.8b \"prompt\" # Specify model\ngerbil --thinking \"127 × 43\" # Enable reasoning\n\ngerbil commit # Git commit message\ngerbil summarize README.md # Summarize file\ngerbil explain src/index.ts # Explain code\n\ngerbil chat # Interactive chat\ngerbil chat --thinking # With reasoning\n\ngerbil serve # HTTP server\ngerbil serve --mcp # MCP server\n\ngerbil models # List models\ngerbil cache # Show cached models\ngerbil bench # Benchmark\ngerbil info # System info\n\ngerbil repl # Interactive TUI\n\\`\\`\\``,\n\n // Tools/Agents\n tools: `Gerbil supports tool calling with Qwen3 models:\n\\`\\`\\`typescript\nimport { defineTool, executeToolCall } from \"gerbil\";\n\nconst weatherTool = defineTool({\n name: \"get_weather\",\n description: \"Get current weather for a city\",\n parameters: z.object({\n city: z.string(),\n }),\n execute: async ({ city }) => {\n return \\`Weather in \\${city}: 72°F, sunny\\`;\n },\n});\n\\`\\`\\`\n\nIn the REPL, use Agent mode (Tab → Agent) to enable tools.`,\n};\n\n/**\n * Get all available documentation topics\n */\nexport function getDocTopics(): string[] {\n return Object.keys(GERBIL_DOCS);\n}\n\n/**\n * Search documentation by topic\n */\nexport function searchDocs(query: string): string | null {\n const words = query.toLowerCase().split(/\\s+/);\n const keyWords = words.filter((w) => w.length > 2);\n\n // Direct key match first\n for (const [key, content] of Object.entries(GERBIL_DOCS)) {\n const keyNorm = key.replace(/-/g, \"\");\n for (const word of keyWords) {\n if (keyNorm === word || key === word || keyNorm.includes(word)) {\n return content;\n }\n }\n }\n\n // Try each word as a potential topic\n for (const word of keyWords) {\n for (const [key, content] of Object.entries(GERBIL_DOCS)) {\n if (key.includes(word) || word.includes(key.replace(/-/g, \"\"))) {\n return content;\n }\n }\n }\n\n // Fallback: search content\n const matches: string[] = [];\n for (const [key, content] of Object.entries(GERBIL_DOCS)) {\n for (const word of keyWords) {\n if (content.toLowerCase().includes(word)) {\n matches.push(`## ${key}\\n${content}`);\n break;\n }\n }\n }\n\n if (matches.length > 0) {\n return matches.slice(0, 2).join(\"\\n\\n---\\n\\n\");\n }\n\n return null;\n}\n","/**\n * Tool Calling System for Gerbil\n *\n * Enables LLMs to call functions/tools during generation.\n * Compatible with Qwen3's tool calling format.\n */\n\nimport { z } from \"zod\";\nimport { getDocTopics, searchDocs } from \"./docs.js\";\nimport { zodToJsonSchema } from \"./utils.js\";\n\n// ============================================\n// Tool Context (for passing gerbil to tools)\n// ============================================\n\nexport type ToolContext = {\n /** Generate text using the loaded model */\n generate: (prompt: string) => Promise<string>;\n};\n\nlet toolContext: ToolContext | null = null;\n\n/**\n * Set the tool context (call this after loading a model)\n */\nexport function setToolContext(ctx: ToolContext): void {\n toolContext = ctx;\n}\n\n/**\n * Get the tool context for use in tools\n */\nexport function getToolContext(): ToolContext | null {\n return toolContext;\n}\n\n// ============================================\n// Tool Definition\n// ============================================\n\nexport type ToolDefinition<TParams = unknown> = {\n /** Tool name (function name) */\n name: string;\n\n /** Description for the LLM */\n description: string;\n\n /** Zod schema for parameters */\n parameters?: z.ZodType<TParams>;\n\n /** The function to execute (receives params and optional context) */\n execute: (params: TParams, ctx?: ToolContext | null) => Promise<string>;\n};\n\nexport type Tool<TParams = unknown> = {\n definition: ToolDefinition<TParams>;\n (params: TParams): Promise<string>;\n};\n\n// ============================================\n// Tool Registry\n// ============================================\n\nconst toolRegistry = new Map<string, Tool<any>>();\n\n/**\n * Define a tool\n */\nexport function defineTool<TParams>(def: ToolDefinition<TParams>): Tool<TParams> {\n const tool = async (params: TParams): Promise<string> => {\n // Validate params if schema provided\n if (def.parameters) {\n const parsed = def.parameters.parse(params);\n return def.execute(parsed, toolContext);\n }\n return def.execute(params, toolContext);\n };\n\n (tool as Tool<TParams>).definition = def;\n\n // Auto-register\n toolRegistry.set(def.name, tool as Tool<any>);\n\n return tool as Tool<TParams>;\n}\n\n/**\n * Get tool by name\n */\nexport function getTool(name: string): Tool<any> | undefined {\n return toolRegistry.get(name);\n}\n\n/**\n * List all tools\n */\nexport function listTools(): string[] {\n return Array.from(toolRegistry.keys());\n}\n\n/**\n * Get all tool definitions for prompt injection\n */\nexport function getToolDefinitions(): ToolDefinition<any>[] {\n return Array.from(toolRegistry.values()).map((t) => t.definition);\n}\n\nexport type LoadedToolResult = {\n name: string;\n loaded: boolean;\n error?: string;\n path: string;\n};\n\n/**\n * Simple tool config (no imports needed in tool files)\n */\nexport type SimpleToolConfig = {\n name: string;\n description: string;\n execute: (params: Record<string, any>) => Promise<string> | string;\n};\n\n/**\n * Load tools from a directory\n * Tools are simple .ts files with a default export config object\n * No module imports - we eval the execute function directly\n */\nexport async function loadTools(dir: string): Promise<LoadedToolResult[]> {\n const fs = await import(\"node:fs\");\n const pathModule = await import(\"node:path\");\n\n if (!fs.existsSync(dir)) {\n return [];\n }\n\n const files = fs.readdirSync(dir).filter((f) => f.endsWith(\".tool.ts\") || f.endsWith(\".tool.js\"));\n const results: LoadedToolResult[] = [];\n\n for (const file of files) {\n const filePath = pathModule.join(dir, file);\n const toolName = file.replace(/\\.tool\\.(ts|js)$/, \"\");\n\n try {\n // Read file content directly\n const content = fs.readFileSync(filePath, \"utf-8\");\n\n // Extract the config object using simple parsing\n const config = parseToolFile(content, toolName);\n\n if (config) {\n // Register the tool\n defineTool({\n name: config.name,\n description: config.description,\n execute: async (params, ctx) => {\n // Project tools may or may not accept ctx parameter\n const executeFn = config.execute as any;\n const result = await executeFn(params, ctx);\n return typeof result === \"string\" ? result : String(result);\n },\n });\n results.push({ name: config.name, loaded: true, path: filePath });\n } else {\n results.push({\n name: toolName,\n loaded: false,\n error: \"Could not parse tool config\",\n path: filePath,\n });\n }\n } catch (e) {\n const errorMsg = e instanceof Error ? e.message.split(\"\\n\")[0] : String(e);\n results.push({ name: toolName, loaded: false, error: errorMsg, path: filePath });\n }\n }\n\n return results;\n}\n\n/**\n * Parse a simple tool file and extract the config\n * Expected format:\n * ```\n * export default {\n * name: \"tool_name\",\n * description: \"...\",\n * execute: async (params) => { ... }\n * }\n * ```\n */\nfunction parseToolFile(content: string, fallbackName: string): SimpleToolConfig | null {\n try {\n // Remove TypeScript type annotations for eval\n let cleaned = content\n .replace(/:\\s*\\{[^}]+\\}/g, \"\") // Remove type objects like : { theme?: string }\n .replace(/:\\s*Record<[^>]+>/g, \"\") // Remove Record types\n .replace(/:\\s*string\\b/g, \"\")\n .replace(/:\\s*number\\b/g, \"\")\n .replace(/:\\s*boolean\\b/g, \"\")\n .replace(/:\\s*any\\b/g, \"\")\n .replace(/:\\s*Promise<[^>]+>/g, \"\")\n .replace(/export\\s+default\\s+/, \"return \");\n\n // Remove comments\n cleaned = cleaned.replace(/\\/\\/.*$/gm, \"\").replace(/\\/\\*[\\s\\S]*?\\*\\//g, \"\");\n\n // Wrap in function and evaluate\n const fn = new Function(cleaned);\n const config = fn();\n\n if (config && typeof config === \"object\" && config.name && config.execute) {\n return {\n name: config.name || fallbackName,\n description: config.description || \"No description\",\n execute: config.execute,\n };\n }\n return null;\n } catch (_e) {\n return null;\n }\n}\n\n/**\n * Load tools from project .gerbil/tools directory\n */\nexport async function loadProjectTools(): Promise<LoadedToolResult[]> {\n const pathModule = await import(\"node:path\");\n const toolsDir = pathModule.join(process.cwd(), \".gerbil\", \"tools\");\n return loadTools(toolsDir);\n}\n\n// Re-export for backwards compatibility\nexport { zodToJsonSchema } from \"./utils.js\";\n\n/**\n * Format tools for Qwen3 prompt\n */\nexport function formatToolsForPrompt(tools: ToolDefinition<any>[]): string {\n if (tools.length === 0) {\n return \"\";\n }\n\n const toolDefs = tools\n .map((t) => {\n const params = t.parameters\n ? zodToJsonSchema(t.parameters)\n : { type: \"object\", properties: {} };\n return `## ${t.name}\n\nDescription: ${t.description}\nParameters: ${JSON.stringify(params, null, 2)}`;\n })\n .join(\"\\n\\n\");\n\n return `You are a helpful assistant with access to tools.\n\n# Tools\n\n${toolDefs}\n\n## How to call tools\n\nTo call a tool, use this exact format:\n<tool_call>\n{\"name\": \"tool_name\", \"arguments\": {\"param\": \"value\"}}\n</tool_call>\n\nWhen the user asks about Gerbil (documentation, features, how to use), call gerbil_docs with a topic like: tools, skills, ai-sdk, next, express, generate, stream, models, cli.\n\nFor other questions, respond normally without calling tools.`;\n}\n\n/**\n * Parse tool call from response\n */\nexport function parseToolCall(response: string): { tool: string; params: any } | null {\n try {\n // Try <tool_call> format first (Qwen style)\n const toolCallMatch = response.match(/<tool_call>\\s*([\\s\\S]*?)\\s*<\\/tool_call>/);\n if (toolCallMatch) {\n const parsed = JSON.parse(toolCallMatch[1]);\n if (parsed.name) {\n return { tool: parsed.name, params: parsed.arguments || {} };\n }\n }\n\n // Fallback: look for {\"name\": \"...\", \"arguments\": ...} pattern\n const nameArgsMatch = response.match(\n /\\{\\s*\"name\"\\s*:\\s*\"([^\"]+)\"[^}]*\"arguments\"\\s*:\\s*(\\{[^}]*\\})/,\n );\n if (nameArgsMatch) {\n return { tool: nameArgsMatch[1], params: JSON.parse(nameArgsMatch[2]) };\n }\n\n // Fallback: {\"tool\": \"...\", \"params\": ...} pattern\n const toolParamsMatch = response.match(\n /\\{\\s*\"tool\"\\s*:\\s*\"([^\"]+)\"[^}]*\"params\"\\s*:\\s*(\\{[^}]*\\})/,\n );\n if (toolParamsMatch) {\n return { tool: toolParamsMatch[1], params: JSON.parse(toolParamsMatch[2]) };\n }\n\n // Simple fallback\n const simpleMatch = response.match(/\\{\\s*\"(?:tool|name)\"\\s*:\\s*\"([^\"]+)\"/);\n if (simpleMatch) {\n return { tool: simpleMatch[1], params: {} };\n }\n } catch {}\n return null;\n}\n\n/**\n * Execute a tool call\n */\nexport async function executeToolCall(toolName: string, params: any): Promise<string> {\n const tool = getTool(toolName);\n if (!tool) {\n return `Error: Unknown tool \"${toolName}\"`;\n }\n\n try {\n return await tool(params);\n } catch (e) {\n return `Error executing ${toolName}: ${e}`;\n }\n}\n\n// ============================================\n// Built-in Tools\n// ============================================\n\n/**\n * Docs tool - searches gerbil documentation\n * Documentation content is in core/docs.ts\n */\nexport const docsTool = defineTool({\n name: \"gerbil_docs\",\n description:\n \"Search Gerbil documentation for information about features, API, integrations, skills, and usage examples\",\n parameters: z.object({\n query: z\n .string()\n .optional()\n .default(\"quickstart\")\n .describe(\n \"Topic: quickstart, generate, stream, json, thinking, embed, models, ai-sdk, next, express, react, skills, define-skill, cli, tools\",\n ),\n }),\n execute: async ({ query = \"quickstart\" }) => {\n const result = searchDocs(query);\n if (result) {\n return result;\n }\n const topics = getDocTopics().join(\", \");\n return `No documentation found for \"${query}\". Available topics: ${topics}`;\n },\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tools-DRYfLfKS.mjs","names":["GERBIL_DOCS: Record<string, string>","matches: string[]","toolContext: ToolContext | null","results: LoadedToolResult[]"],"sources":["../src/core/docs.ts","../src/core/tools.ts"],"sourcesContent":["/**\n * Gerbil Documentation Content\n *\n * Documentation strings used by the gerbil_docs tool\n * for answering questions about Gerbil features.\n */\n\nexport const GERBIL_DOCS: Record<string, string> = {\n // Core\n quickstart: `Gerbil Quick Start:\n\\`\\`\\`typescript\nimport { Gerbil } from \"gerbil\";\n\nconst g = new Gerbil();\nawait g.loadModel(\"qwen3.5-0.8b\");\n\n// Generate text\nconst result = await g.generate(\"Write a haiku\");\nconsole.log(result.text);\n\n// Stream responses\nfor await (const chunk of g.stream(\"Tell me a story\")) {\n process.stdout.write(chunk);\n}\n\\`\\`\\``,\n\n generate: `Generate text with options:\n\\`\\`\\`typescript\nconst result = await gerbil.generate(prompt, {\n maxTokens: 256, // Max tokens to generate\n temperature: 0.7, // 0-2, higher = more creative\n topP: 0.9, // Nucleus sampling\n topK: 50, // Top-k sampling\n thinking: false, // Enable reasoning mode (Qwen3)\n system: \"You are...\", // System prompt\n});\n\n// Result contains:\nresult.text // Generated text\nresult.thinking // Reasoning (if thinking=true)\nresult.tokensGenerated\nresult.tokensPerSecond\nresult.totalTime\n\\`\\`\\``,\n\n stream: `Streaming responses:\n\\`\\`\\`typescript\nfor await (const chunk of gerbil.stream(\"Hello\")) {\n process.stdout.write(chunk);\n}\n\n// With options\nconst stream = gerbil.stream(\"Explain React\", {\n maxTokens: 500,\n onToken: (token) => console.log(token),\n});\n\\`\\`\\``,\n\n json: `Structured JSON output with Zod validation:\n\\`\\`\\`typescript\nimport { z } from \"zod\";\n\nconst PersonSchema = z.object({\n name: z.string(),\n age: z.number(),\n city: z.string(),\n});\n\nconst data = await gerbil.json(\"Extract: John is 32 and lives in NYC\", {\n schema: PersonSchema,\n retries: 3, // Retry on validation failure\n temperature: 0.3, // Lower = more consistent\n});\n// { name: \"John\", age: 32, city: \"NYC\" }\n\\`\\`\\``,\n\n \"generate-object\": `Structured object output with retries (generateObject):\n\\`\\`\\`typescript\nimport { generateObject } from \"@tryhamster/gerbil\";\n\n// Validate with a minimal { required } schema or a predicate; omit to accept any JSON.\nconst { object, attempts } = await generateObject(\n 'Extract {name, age} from: \"I am Sarah, 28\"',\n { schema: { required: [\"name\", \"age\"] }, maxRetries: 4 },\n);\n// object === { name: \"Sarah\", age: 28 }\n\\`\\`\\`\n\nAlso on the Gerbil class (g.generateObject), the engine\n(WebGPUEngine.generateObject), the useObject React hook, and the CLI\n(gerbil object \"...\" --schema person.json). Unlike json() (Zod), this uses a\npredicate/required-keys validator and retries with a corrective nudge.`,\n\n thinking: `Thinking mode (chain-of-thought reasoning) - Qwen3 models:\n\\`\\`\\`typescript\nconst result = await gerbil.generate(\"What is 127 × 43?\", {\n thinking: true,\n});\n\nconsole.log(result.thinking);\n// \"Let me calculate step by step: 127 × 43 = 127 × 40 + 127 × 3...\"\n\nconsole.log(result.text);\n// \"5461\"\n\\`\\`\\`\n\nEnable in chat with /no_think or /think commands, or enable_thinking parameter.`,\n\n embed: `Generate embeddings:\n\\`\\`\\`typescript\n// Single text\nconst { vector } = await gerbil.embed(\"Hello world\");\n// vector: number[] (384 dimensions by default)\n\n// Batch\nconst results = await gerbil.embedBatch([\"Hello\", \"World\"]);\n\\`\\`\\``,\n\n // Models\n models: `Available models (native WebGPU engine, INT4 on-device):\n\n| Model | Size | Best For |\n|-------|------|----------|\n| qwen3.5-0.8b | ~1.6GB | Default — fast, multimodal, 262k context, thinking mode |\n| qwen3.5-2b | ~4GB | Best quality, multimodal, 262k context, thinking mode |\n| lfm2.5-1.2b-thinking | ~2.4GB | Efficient reasoning, 128k context |\n\nLoad any HuggingFace model with a supported architecture (Qwen2/Qwen3/Qwen3.5/LFM2):\n\\`\\`\\`typescript\nawait gerbil.loadModel(\"hf:org/model\");\nawait gerbil.loadModel(\"Qwen/Qwen3.5-0.8B\");\n\\`\\`\\``,\n\n load: `Loading models:\n\\`\\`\\`typescript\nawait gerbil.loadModel(\"qwen3.5-0.8b\", {\n device: \"webgpu\", // native WebGPU engine (Dawn in Node, WebGPU in browser)\n onProgress: (p) => {\n console.log(p.status, p.progress, p.file);\n },\n});\n\\`\\`\\``,\n\n // AI SDK\n \"ai-sdk\": `Vercel AI SDK v5 integration:\n\\`\\`\\`typescript\nimport { generateText, streamText } from \"ai\";\nimport { gerbil } from \"gerbil/ai\";\n\n// Generate\nconst { text } = await generateText({\n model: gerbil(\"qwen3.5-0.8b\"),\n prompt: \"Write a commit message\",\n});\n\n// Stream\nconst stream = streamText({\n model: gerbil(\"qwen3.5-0.8b\"),\n prompt: \"Explain React hooks\",\n});\n\nfor await (const chunk of stream.textStream) {\n process.stdout.write(chunk);\n}\n\n// With thinking mode\nconst { text } = await generateText({\n model: gerbil(\"qwen3.5-0.8b\", { thinking: true }),\n prompt: \"What is 127 × 43?\",\n});\n\\`\\`\\``,\n\n // Frameworks\n next: `Next.js App Router integration:\n\\`\\`\\`typescript\n// app/api/chat/route.ts\nimport { gerbil } from \"gerbil/next\";\n\nexport const POST = gerbil.handler({ \n model: \"qwen3.5-0.8b\",\n maxTokens: 500,\n});\n\\`\\`\\``,\n\n express: `Express.js integration:\n\\`\\`\\`typescript\nimport express from \"express\";\nimport { gerbil } from \"gerbil/express\";\n\nconst app = express();\napp.use(\"/ai\", gerbil()());\n\n// Endpoints created:\n// POST /ai/generate - Generate text\n// POST /ai/stream - Stream text\n// POST /ai/json - Structured output\n// POST /ai/embed - Embeddings\n\\`\\`\\``,\n\n react: `React hooks:\n\\`\\`\\`typescript\nimport { useGerbil, useChat } from \"gerbil/react\";\n\nfunction Chat() {\n const { generate, isLoading } = useGerbil();\n \n const handleSubmit = async (prompt: string) => {\n const result = await generate(prompt);\n console.log(result.text);\n };\n}\n\\`\\`\\``,\n\n hono: `Hono integration:\n\\`\\`\\`typescript\nimport { Hono } from \"hono\";\nimport { gerbil } from \"gerbil/hono\";\n\nconst app = new Hono();\napp.route(\"/ai\", await gerbil()());\n\\`\\`\\``,\n\n langchain: `LangChain integration:\n\\`\\`\\`typescript\nimport { GerbilLLM, GerbilEmbeddings } from \"gerbil/langchain\";\n\nconst llm = new GerbilLLM({ model: \"qwen3.5-0.8b\" });\nconst embeddings = new GerbilEmbeddings();\n\n// Use with chains\nconst chain = new LLMChain({ llm, prompt: template });\n\\`\\`\\``,\n\n mcp: `MCP Server for Claude Desktop & Cursor:\n\\`\\`\\`bash\ngerbil serve --mcp\n\\`\\`\\`\n\nSkills are automatically exposed as MCP tools:\n- gerbil_generate\n- gerbil_commit\n- gerbil_summarize\n- gerbil_explain\n- etc.`,\n\n // Skills\n skills: `Skills are reusable AI tasks with Zod validation:\n\\`\\`\\`typescript\nimport { commit, summarize, explain, review } from \"gerbil/skills\";\n\n// Generate commit message\nconst msg = await commit({ type: \"conventional\", maxLength: 72 });\n\n// Summarize content\nconst summary = await summarize({ \n content: doc, \n length: \"short\",\n format: \"bullets\" \n});\n\n// Explain code\nconst explanation = await explain({ \n content: code, \n level: \"beginner\" \n});\n\n// Code review\nconst feedback = await review({ \n code, \n focus: [\"security\", \"performance\"] \n});\n\\`\\`\\`\n\nBuilt-in skills: commit, summarize, explain, review, test, translate, extract, title`,\n\n \"define-skill\": `Create custom skills:\n\\`\\`\\`typescript\nimport { defineSkill } from \"gerbil/skills\";\nimport { z } from \"zod\";\n\nexport const sentiment = defineSkill({\n name: \"sentiment\",\n description: \"Analyze text sentiment\",\n input: z.object({ \n text: z.string(),\n detailed: z.boolean().default(false)\n }),\n output: z.object({\n sentiment: z.enum([\"positive\", \"negative\", \"neutral\"]),\n confidence: z.number(),\n }),\n temperature: 0.3,\n maxTokens: 100,\n \n async run({ input, gerbil }) {\n return gerbil.json(\\`Sentiment of: \\${input.text}\\`, {\n schema: this.output,\n });\n },\n});\n\\`\\`\\``,\n\n \"load-skills\": `Load skills from files:\n\\`\\`\\`typescript\nimport { loadSkills, useSkill, listSkills } from \"gerbil/skills\";\n\n// Load from directory (*.skill.ts files)\nawait loadSkills(\"./skills\");\n\n// Use by name\nconst skill = useSkill(\"my-skill\");\nconst result = await skill({ text: \"Hello\" });\n\n// List all\nconsole.log(listSkills()); // [\"commit\", \"summarize\", ...]\n\\`\\`\\``,\n\n // CLI\n cli: `CLI commands:\n\\`\\`\\`bash\ngerbil \"Write a haiku\" # Generate text\ngerbil -m qwen3.5-0.8b \"prompt\" # Specify model\ngerbil --thinking \"127 × 43\" # Enable reasoning\n\ngerbil commit # Git commit message\ngerbil summarize README.md # Summarize file\ngerbil explain src/index.ts # Explain code\n\ngerbil chat # Interactive chat\ngerbil chat --thinking # With reasoning\n\ngerbil serve # HTTP server\ngerbil serve --mcp # MCP server\n\ngerbil models # List models\ngerbil cache # Show cached models\ngerbil bench # Benchmark\ngerbil info # System info\n\ngerbil repl # Interactive TUI\n\\`\\`\\``,\n\n // Tools/Agents\n tools: `Gerbil supports tool calling with Qwen3 models:\n\\`\\`\\`typescript\nimport { defineTool, executeToolCall } from \"gerbil\";\n\nconst weatherTool = defineTool({\n name: \"get_weather\",\n description: \"Get current weather for a city\",\n parameters: z.object({\n city: z.string(),\n }),\n execute: async ({ city }) => {\n return \\`Weather in \\${city}: 72°F, sunny\\`;\n },\n});\n\\`\\`\\`\n\nIn the REPL, use Agent mode (Tab → Agent) to enable tools.`,\n};\n\n/**\n * Get all available documentation topics\n */\nexport function getDocTopics(): string[] {\n return Object.keys(GERBIL_DOCS);\n}\n\n/**\n * Search documentation by topic\n */\nexport function searchDocs(query: string): string | null {\n const words = query.toLowerCase().split(/\\s+/);\n const keyWords = words.filter((w) => w.length > 2);\n\n // Direct key match first\n for (const [key, content] of Object.entries(GERBIL_DOCS)) {\n const keyNorm = key.replace(/-/g, \"\");\n for (const word of keyWords) {\n if (keyNorm === word || key === word || keyNorm.includes(word)) {\n return content;\n }\n }\n }\n\n // Try each word as a potential topic\n for (const word of keyWords) {\n for (const [key, content] of Object.entries(GERBIL_DOCS)) {\n if (key.includes(word) || word.includes(key.replace(/-/g, \"\"))) {\n return content;\n }\n }\n }\n\n // Fallback: search content\n const matches: string[] = [];\n for (const [key, content] of Object.entries(GERBIL_DOCS)) {\n for (const word of keyWords) {\n if (content.toLowerCase().includes(word)) {\n matches.push(`## ${key}\\n${content}`);\n break;\n }\n }\n }\n\n if (matches.length > 0) {\n return matches.slice(0, 2).join(\"\\n\\n---\\n\\n\");\n }\n\n return null;\n}\n","/**\n * Tool Calling System for Gerbil\n *\n * Enables LLMs to call functions/tools during generation.\n * Compatible with Qwen3's tool calling format.\n */\n\nimport { z } from \"zod\";\nimport { getDocTopics, searchDocs } from \"./docs.js\";\nimport { zodToJsonSchema } from \"./utils.js\";\n\n// ============================================\n// Tool Context (for passing gerbil to tools)\n// ============================================\n\nexport type ToolContext = {\n /** Generate text using the loaded model */\n generate: (prompt: string) => Promise<string>;\n};\n\nlet toolContext: ToolContext | null = null;\n\n/**\n * Set the tool context (call this after loading a model)\n */\nexport function setToolContext(ctx: ToolContext): void {\n toolContext = ctx;\n}\n\n/**\n * Get the tool context for use in tools\n */\nexport function getToolContext(): ToolContext | null {\n return toolContext;\n}\n\n// ============================================\n// Tool Definition\n// ============================================\n\nexport type ToolDefinition<TParams = unknown> = {\n /** Tool name (function name) */\n name: string;\n\n /** Description for the LLM */\n description: string;\n\n /** Zod schema for parameters */\n parameters?: z.ZodType<TParams>;\n\n /** The function to execute (receives params and optional context) */\n execute: (params: TParams, ctx?: ToolContext | null) => Promise<string>;\n};\n\nexport type Tool<TParams = unknown> = {\n definition: ToolDefinition<TParams>;\n (params: TParams): Promise<string>;\n};\n\n// ============================================\n// Tool Registry\n// ============================================\n\nconst toolRegistry = new Map<string, Tool<any>>();\n\n/**\n * Define a tool\n */\nexport function defineTool<TParams>(def: ToolDefinition<TParams>): Tool<TParams> {\n const tool = async (params: TParams): Promise<string> => {\n // Validate params if schema provided\n if (def.parameters) {\n const parsed = def.parameters.parse(params);\n return def.execute(parsed, toolContext);\n }\n return def.execute(params, toolContext);\n };\n\n (tool as Tool<TParams>).definition = def;\n\n // Auto-register\n toolRegistry.set(def.name, tool as Tool<any>);\n\n return tool as Tool<TParams>;\n}\n\n/**\n * Get tool by name\n */\nexport function getTool(name: string): Tool<any> | undefined {\n return toolRegistry.get(name);\n}\n\n/**\n * List all tools\n */\nexport function listTools(): string[] {\n return Array.from(toolRegistry.keys());\n}\n\n/**\n * Get all tool definitions for prompt injection\n */\nexport function getToolDefinitions(): ToolDefinition<any>[] {\n return Array.from(toolRegistry.values()).map((t) => t.definition);\n}\n\nexport type LoadedToolResult = {\n name: string;\n loaded: boolean;\n error?: string;\n path: string;\n};\n\n/**\n * Simple tool config (no imports needed in tool files)\n */\nexport type SimpleToolConfig = {\n name: string;\n description: string;\n execute: (params: Record<string, any>) => Promise<string> | string;\n};\n\n/**\n * Load tools from a directory\n * Tools are simple .ts files with a default export config object\n * No module imports - we eval the execute function directly\n */\nexport async function loadTools(dir: string): Promise<LoadedToolResult[]> {\n const fs = await import(\"node:fs\");\n const pathModule = await import(\"node:path\");\n\n if (!fs.existsSync(dir)) {\n return [];\n }\n\n const files = fs.readdirSync(dir).filter((f) => f.endsWith(\".tool.ts\") || f.endsWith(\".tool.js\"));\n const results: LoadedToolResult[] = [];\n\n for (const file of files) {\n const filePath = pathModule.join(dir, file);\n const toolName = file.replace(/\\.tool\\.(ts|js)$/, \"\");\n\n try {\n // Read file content directly\n const content = fs.readFileSync(filePath, \"utf-8\");\n\n // Extract the config object using simple parsing\n const config = parseToolFile(content, toolName);\n\n if (config) {\n // Register the tool\n defineTool({\n name: config.name,\n description: config.description,\n execute: async (params, ctx) => {\n // Project tools may or may not accept ctx parameter\n const executeFn = config.execute as any;\n const result = await executeFn(params, ctx);\n return typeof result === \"string\" ? result : String(result);\n },\n });\n results.push({ name: config.name, loaded: true, path: filePath });\n } else {\n results.push({\n name: toolName,\n loaded: false,\n error: \"Could not parse tool config\",\n path: filePath,\n });\n }\n } catch (e) {\n const errorMsg = e instanceof Error ? e.message.split(\"\\n\")[0] : String(e);\n results.push({ name: toolName, loaded: false, error: errorMsg, path: filePath });\n }\n }\n\n return results;\n}\n\n/**\n * Parse a simple tool file and extract the config\n * Expected format:\n * ```\n * export default {\n * name: \"tool_name\",\n * description: \"...\",\n * execute: async (params) => { ... }\n * }\n * ```\n */\nfunction parseToolFile(content: string, fallbackName: string): SimpleToolConfig | null {\n try {\n // Remove TypeScript type annotations for eval\n let cleaned = content\n .replace(/:\\s*\\{[^}]+\\}/g, \"\") // Remove type objects like : { theme?: string }\n .replace(/:\\s*Record<[^>]+>/g, \"\") // Remove Record types\n .replace(/:\\s*string\\b/g, \"\")\n .replace(/:\\s*number\\b/g, \"\")\n .replace(/:\\s*boolean\\b/g, \"\")\n .replace(/:\\s*any\\b/g, \"\")\n .replace(/:\\s*Promise<[^>]+>/g, \"\")\n .replace(/export\\s+default\\s+/, \"return \");\n\n // Remove comments\n cleaned = cleaned.replace(/\\/\\/.*$/gm, \"\").replace(/\\/\\*[\\s\\S]*?\\*\\//g, \"\");\n\n // Wrap in function and evaluate\n const fn = new Function(cleaned);\n const config = fn();\n\n if (config && typeof config === \"object\" && config.name && config.execute) {\n return {\n name: config.name || fallbackName,\n description: config.description || \"No description\",\n execute: config.execute,\n };\n }\n return null;\n } catch (_e) {\n return null;\n }\n}\n\n/**\n * Load tools from project .gerbil/tools directory\n */\nexport async function loadProjectTools(): Promise<LoadedToolResult[]> {\n const pathModule = await import(\"node:path\");\n const toolsDir = pathModule.join(process.cwd(), \".gerbil\", \"tools\");\n return loadTools(toolsDir);\n}\n\n// Re-export for backwards compatibility\nexport { zodToJsonSchema } from \"./utils.js\";\n\n/**\n * Format tools for Qwen3 prompt\n */\nexport function formatToolsForPrompt(tools: ToolDefinition<any>[]): string {\n if (tools.length === 0) {\n return \"\";\n }\n\n const toolDefs = tools\n .map((t) => {\n const params = t.parameters\n ? zodToJsonSchema(t.parameters)\n : { type: \"object\", properties: {} };\n return `## ${t.name}\n\nDescription: ${t.description}\nParameters: ${JSON.stringify(params, null, 2)}`;\n })\n .join(\"\\n\\n\");\n\n return `You are a helpful assistant with access to tools.\n\n# Tools\n\n${toolDefs}\n\n## How to call tools\n\nTo call a tool, use this exact format:\n<tool_call>\n{\"name\": \"tool_name\", \"arguments\": {\"param\": \"value\"}}\n</tool_call>\n\nWhen the user asks about Gerbil (documentation, features, how to use), call gerbil_docs with a topic like: tools, skills, ai-sdk, next, express, generate, stream, models, cli.\n\nFor other questions, respond normally without calling tools.`;\n}\n\n/**\n * Parse tool call from response\n */\nexport function parseToolCall(response: string): { tool: string; params: any } | null {\n try {\n // Try <tool_call> format first (Qwen style)\n const toolCallMatch = response.match(/<tool_call>\\s*([\\s\\S]*?)\\s*<\\/tool_call>/);\n if (toolCallMatch) {\n const parsed = JSON.parse(toolCallMatch[1]);\n if (parsed.name) {\n return { tool: parsed.name, params: parsed.arguments || {} };\n }\n }\n\n // Fallback: look for {\"name\": \"...\", \"arguments\": ...} pattern\n const nameArgsMatch = response.match(\n /\\{\\s*\"name\"\\s*:\\s*\"([^\"]+)\"[^}]*\"arguments\"\\s*:\\s*(\\{[^}]*\\})/,\n );\n if (nameArgsMatch) {\n return { tool: nameArgsMatch[1], params: JSON.parse(nameArgsMatch[2]) };\n }\n\n // Fallback: {\"tool\": \"...\", \"params\": ...} pattern\n const toolParamsMatch = response.match(\n /\\{\\s*\"tool\"\\s*:\\s*\"([^\"]+)\"[^}]*\"params\"\\s*:\\s*(\\{[^}]*\\})/,\n );\n if (toolParamsMatch) {\n return { tool: toolParamsMatch[1], params: JSON.parse(toolParamsMatch[2]) };\n }\n\n // Simple fallback\n const simpleMatch = response.match(/\\{\\s*\"(?:tool|name)\"\\s*:\\s*\"([^\"]+)\"/);\n if (simpleMatch) {\n return { tool: simpleMatch[1], params: {} };\n }\n } catch {}\n return null;\n}\n\n/**\n * Execute a tool call\n */\nexport async function executeToolCall(toolName: string, params: any): Promise<string> {\n const tool = getTool(toolName);\n if (!tool) {\n return `Error: Unknown tool \"${toolName}\"`;\n }\n\n try {\n return await tool(params);\n } catch (e) {\n return `Error executing ${toolName}: ${e}`;\n }\n}\n\n// ============================================\n// Built-in Tools\n// ============================================\n\n/**\n * Docs tool - searches gerbil documentation\n * Documentation content is in core/docs.ts\n */\nexport const docsTool = defineTool({\n name: \"gerbil_docs\",\n description:\n \"Search Gerbil documentation for information about features, API, integrations, skills, and usage examples\",\n parameters: z.object({\n query: z\n .string()\n .optional()\n .default(\"quickstart\")\n .describe(\n \"Topic: quickstart, generate, stream, json, thinking, embed, models, ai-sdk, next, express, react, skills, define-skill, cli, tools\",\n ),\n }),\n execute: async ({ query = \"quickstart\" }) => {\n const result = searchDocs(query);\n if (result) {\n return result;\n }\n const topics = getDocTopics().join(\", \");\n return `No documentation found for \"${query}\". Available topics: ${topics}`;\n },\n});\n"],"mappings":"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"}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
//#region src/tune/capture.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Capture — turn any AI call-site into an organized, task-tagged dataset, LOCALLY.
|
|
4
|
+
*
|
|
5
|
+
* Nothing is sent anywhere. `capture`/`tap` append your model inputs+outputs to a
|
|
6
|
+
* local store (a JSONL file per task in Node, or memory/your-own-sink elsewhere),
|
|
7
|
+
* so scattered inference calls become clean, growing datasets you own. Feed a task
|
|
8
|
+
* to `tune()` when you want to train — that's the only step that sends anything,
|
|
9
|
+
* and only the compiled set for that one task.
|
|
10
|
+
*/
|
|
11
|
+
interface CaptureRow {
|
|
12
|
+
/** The call-site tag = the dataset bucket. */
|
|
13
|
+
task: string;
|
|
14
|
+
input: string;
|
|
15
|
+
output: string;
|
|
16
|
+
/** Source model (e.g. "gpt-5"), for provenance. */
|
|
17
|
+
model?: string;
|
|
18
|
+
/** Freeform origin (a route, a component). */
|
|
19
|
+
source?: string;
|
|
20
|
+
meta?: Record<string, unknown>;
|
|
21
|
+
/** ISO timestamp. */
|
|
22
|
+
ts: string;
|
|
23
|
+
}
|
|
24
|
+
/** A row to append (everything but the timestamp, which is stamped for you). */
|
|
25
|
+
type CapturePair = Omit<CaptureRow, "task" | "ts">;
|
|
26
|
+
/** Where captured rows go. Return void; throw to signal a write failure. */
|
|
27
|
+
type CaptureSink = (row: CaptureRow) => void | Promise<void>;
|
|
28
|
+
interface CaptureConfig {
|
|
29
|
+
/** Directory for the per-task JSONL files (Node default sink). */
|
|
30
|
+
dir: string;
|
|
31
|
+
/** Custom sink — overrides the file/memory default (e.g. write to your DB). */
|
|
32
|
+
sink?: CaptureSink;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Configure where captured rows land. Defaults to `./gerbil-data/<task>.jsonl`
|
|
36
|
+
* in Node. Pass `{ sink }` to route rows to your own store instead.
|
|
37
|
+
*
|
|
38
|
+
* ```ts
|
|
39
|
+
* configureCapture({ dir: "./data" }); // local files
|
|
40
|
+
* configureCapture({ sink: (row) => db.insert(row) }); // your warehouse
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
declare function configureCapture(opts: Partial<CaptureConfig>): void;
|
|
44
|
+
/**
|
|
45
|
+
* Log one production call to a task's dataset.
|
|
46
|
+
*
|
|
47
|
+
* ```ts
|
|
48
|
+
* capture("title-gen", { input: draft, output: title, model: "gpt-5" });
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
declare function capture(task: string, pair: CapturePair): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Wrap a model call you already make: runs it, returns its result untouched, and
|
|
54
|
+
* captures the input+output to the task's dataset. Provide `input` (you have it);
|
|
55
|
+
* the output is pulled from the result (override with `extract`).
|
|
56
|
+
*
|
|
57
|
+
* ```ts
|
|
58
|
+
* const title = await tap(
|
|
59
|
+
* { task: "title-gen", input: draft, model: "gpt-5" },
|
|
60
|
+
* () => openai.chat.completions.create({ ... }),
|
|
61
|
+
* );
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
declare function tap<T>(opts: {
|
|
65
|
+
task: string;
|
|
66
|
+
input: string;
|
|
67
|
+
model?: string;
|
|
68
|
+
source?: string;
|
|
69
|
+
meta?: Record<string, unknown>;
|
|
70
|
+
/** Override how the output text is pulled from the call's result. */
|
|
71
|
+
extract?: (result: T) => string;
|
|
72
|
+
}, call: () => Promise<T>): Promise<T>;
|
|
73
|
+
/** A handle to one task's accumulated dataset. */
|
|
74
|
+
interface Dataset {
|
|
75
|
+
task: string;
|
|
76
|
+
rows(): Promise<CaptureRow[]>;
|
|
77
|
+
count(): Promise<number>;
|
|
78
|
+
/** Compile to JSONL (the format tune() and the upload accept). */
|
|
79
|
+
toJSONL(): Promise<string>;
|
|
80
|
+
}
|
|
81
|
+
/** Read back a task's dataset (from the file/memory store). */
|
|
82
|
+
declare function dataset(task: string): Dataset;
|
|
83
|
+
/** List the task datasets that exist, with their row counts. */
|
|
84
|
+
declare function datasets(): Promise<Record<string, number>>;
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/tune/client.d.ts
|
|
87
|
+
/**
|
|
88
|
+
* tune() — train (or retrain) a model from your own code.
|
|
89
|
+
*
|
|
90
|
+
* Compiles a captured task dataset (or raw JSONL you pass) and hands it to the
|
|
91
|
+
* Gerbil Tune API with your API key. The only step that sends anything: only the
|
|
92
|
+
* compiled set for that one task leaves, when you call this. Priced by dataset
|
|
93
|
+
* size; charged to the payment method on your account (off-session).
|
|
94
|
+
*
|
|
95
|
+
* ```ts
|
|
96
|
+
* import { tune } from "@tryhamster/gerbil/tune";
|
|
97
|
+
*
|
|
98
|
+
* const model = await tune({ task: "support-triage", base: "qwen3.5-0.8b" });
|
|
99
|
+
* await model.until("completed");
|
|
100
|
+
* console.log(model.outputRepo); // load it with the engine, anywhere
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
interface TuneModel {
|
|
104
|
+
/** The job/model id. */
|
|
105
|
+
id: string;
|
|
106
|
+
status: string;
|
|
107
|
+
/** HF repo of the merged model once training completes. */
|
|
108
|
+
outputRepo: string | null;
|
|
109
|
+
error: string | null;
|
|
110
|
+
/** Re-fetch the latest status. */
|
|
111
|
+
refresh(): Promise<TuneModel>;
|
|
112
|
+
/**
|
|
113
|
+
* Poll until the job reaches `target` (default "completed"/"ready"). Rejects if
|
|
114
|
+
* the job fails or the timeout elapses.
|
|
115
|
+
*/
|
|
116
|
+
until(target?: string, opts?: {
|
|
117
|
+
intervalMs?: number;
|
|
118
|
+
timeoutMs?: number;
|
|
119
|
+
}): Promise<TuneModel>;
|
|
120
|
+
}
|
|
121
|
+
interface TuneOptions {
|
|
122
|
+
/** Train from a captured task's dataset (compiled to JSONL). */
|
|
123
|
+
task?: string;
|
|
124
|
+
/** Or pass JSONL directly (one {input,output} / {prompt,completion} per line). */
|
|
125
|
+
data?: string;
|
|
126
|
+
/** Base model to specialize (defaults server-side). */
|
|
127
|
+
base?: string;
|
|
128
|
+
/** Training epochs. */
|
|
129
|
+
epochs?: number;
|
|
130
|
+
/** API key. Defaults to process.env.GERBIL_API_KEY. */
|
|
131
|
+
apiKey?: string;
|
|
132
|
+
/** API base URL. Defaults to GERBIL_API_URL, then the Gerbil Tune API. */
|
|
133
|
+
baseUrl?: string;
|
|
134
|
+
}
|
|
135
|
+
/** Train (or retrain) a model from your own code. See the module docs. */
|
|
136
|
+
declare function tune(opts: TuneOptions): Promise<TuneModel>;
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/tune/labeler.d.ts
|
|
139
|
+
/**
|
|
140
|
+
* createLabeler — run a classifier you tuned, LOCALLY, to label your own data.
|
|
141
|
+
*
|
|
142
|
+
* The labeler is a small model you trained on us (paid Tune); this just *runs* it
|
|
143
|
+
* on-device (browser via WebGPU, or Node via Dawn — fast on a GPU server, CPU
|
|
144
|
+
* fallback otherwise). It wraps the engine's retrying `generateObject` with a
|
|
145
|
+
* predicate that enforces your label set, so every result is a clean, known label
|
|
146
|
+
* — and your raw data never leaves the machine.
|
|
147
|
+
*/
|
|
148
|
+
interface Labeler {
|
|
149
|
+
/** Label one input → `{ label }` (guaranteed to be one of your labels). */
|
|
150
|
+
(input: string): Promise<{
|
|
151
|
+
label: string;
|
|
152
|
+
}>;
|
|
153
|
+
/** Label many inputs, sequentially (one GPU). */
|
|
154
|
+
many(inputs: string[]): Promise<Array<{
|
|
155
|
+
input: string;
|
|
156
|
+
label: string;
|
|
157
|
+
}>>;
|
|
158
|
+
/** Label inputs and capture them straight into a task's dataset. */
|
|
159
|
+
labelInto(task: string, inputs: string[]): Promise<Array<{
|
|
160
|
+
input: string;
|
|
161
|
+
label: string;
|
|
162
|
+
}>>;
|
|
163
|
+
/** Free the underlying engine. */
|
|
164
|
+
dispose(): Promise<void>;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* ```ts
|
|
168
|
+
* const labeler = await createLabeler({
|
|
169
|
+
* model: "your-org/support-triage",
|
|
170
|
+
* labels: ["billing", "bug", "feature-request", "other"],
|
|
171
|
+
* });
|
|
172
|
+
* await labeler("My card was charged twice"); // → { label: "billing" }
|
|
173
|
+
* await labeler.labelInto("triage", rawInputs); // labels + captures locally
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
declare function createLabeler(opts: {
|
|
177
|
+
/** The tuned classifier's model repo/id. */
|
|
178
|
+
model: string;
|
|
179
|
+
/** The allowed labels — enforced on every result. */
|
|
180
|
+
labels: string[];
|
|
181
|
+
/** Override the system prompt (defaults to a JSON-label instruction). */
|
|
182
|
+
system?: string;
|
|
183
|
+
}): Promise<Labeler>;
|
|
184
|
+
//#endregion
|
|
185
|
+
export { type CapturePair, type CaptureRow, type CaptureSink, type Dataset, type Labeler, type TuneModel, type TuneOptions, capture, configureCapture, createLabeler, dataset, datasets, tap, tune };
|
|
186
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/tune/capture.ts","../../src/tune/client.ts","../../src/tune/labeler.ts"],"sourcesContent":[],"mappings":";;AAUA;AAeA;AAGA;AAAoE;AA8BpE;AAkCA;AAgCA;;AAQuB,UA1HN,UAAA,CA0HM;EAED;EAAR,IAAA,EAAA,MAAA;EACH,KAAA,EAAA,MAAA;EAAR,MAAA,EAAA,MAAA;EAAO;EAcO,KAAA,CAAA,EAAA,MAAO;EAEN;EAAR,MAAA,CAAA,EAAA,MAAA;EACC,IAAA,CAAA,EArIF,MAqIE,CAAA,MAAA,EAAA,OAAA,CAAA;EAEE;EAAO,EAAA,EAAA,MAAA;AAuBpB;AAaA;KArKY,WAAA,GAAc,KAAK;;KAGnB,WAAA,SAAoB,sBAAsB;ACMtD,UDJU,aAAA,CCIgB;EAQL;EAAR,GAAA,EAAA,MAAA;EAKyE;EAAR,IAAA,CAAA,EDbrE,WCaqE;;AAG9E;AA8EA;;;;;;;;ACnHiB,iBF6CD,gBAAA,CE7CQ,IAAA,EF6Ce,OE7Cf,CF6CuB,aE7CvB,CAAA,CAAA,EAAA,IAAA;;;;;;;;AAqBF,iBF0DA,OAAA,CE1Da,IAOvB,EAAA,MAAR,EAAA,IAAO,EFmDuC,WEnDvC,CAAA,EFmDqD,OEnDrD,CAAA,IAAA,CAAA;;;;;;;;;;;;;iBFmFW;;;;;SAMX;;qBAEY;eAET,QAAQ,KACnB,QAAQ;;UAcM,OAAA;;UAEP,QAAQ;WACP;;aAEE;;;iBAuBG,OAAA,gBAAuB;;iBAajB,QAAA,CAAA,GAAY,QAAQ;;;;AApL1C;AAeA;AAGA;AAAoE;AA8BpE;AAkCA;AAgCA;;;;;;;;AAyBA;AAEkB,UCrHD,SAAA,CDqHC;EAAR;EACC,EAAA,EAAA,MAAA;EAEE,MAAA,EAAA,MAAA;EAAO;EAuBJ,UAAO,EAAA,MAAA,GAAA,IAAgB;EAajB,KAAA,EAAA,MAAQ,GAAA,IAAA;;aCpJjB,QAAQ;;AARrB;;;EAasF,KAAA,CAAA,MAAA,CAAA,EAAA,MAAA,EAAA,IAGrE,CAHqE,EAAA;IAAR,UAAA,CAAA,EAAA,MAAA;IAAO,SAAA,CAAA,EAAA,MAAA;EAGpE,CAAA,CAAA,EAH6D,OAG7D,CAHqE,SAG1D,CAAA;AA8E5B;AAAiC,UA9EhB,WAAA,CA8EgB;EAAsB;EAAR,IAAA,CAAA,EAAA,MAAA;EAAO;;;;ECnHrC;EAEE,MAAA,CAAA,EAAA,MAAA;EAEe;EAAR,MAAA,CAAA,EAAA,MAAA;EAE2B;EAAR,OAAA,CAAA,EAAA,MAAA;;;AAevB,iBD8FA,IAAA,CC9Fa,IAOvB,EDuFqB,WCvF7B,CAAA,EDuF2C,OCvFpC,CDuF4C,SCvF5C,CAAA;;;;AF/BX;AAeA;AAGA;AAAoE;AA8BpE;AAkCA;AAgCA;;AAQuB,UEvHN,OAAA,CFuHM;EAED;EAAR,CAAA,KAAA,EAAA,MAAA,CAAA,EEvHK,OFuHL,CAAA;IACH,KAAA,EAAA,MAAA;EAAR,CAAA,CAAA;EAAO;EAcO,IAAA,CAAA,MAAO,EAAA,MAAA,EAAA,CAAA,EEpIE,OFoIF,CEpIU,KFoIV,CAAA;IAEN,KAAA,EAAA,MAAA;IAAR,KAAA,EAAA,MAAA;EACC,CAAA,CAAA,CAAA;EAEE;EAAO,SAAA,CAAA,IAAA,EAAA,MAAA,EAAA,MAAA,EAAA,MAAA,EAAA,CAAA,EEvIyB,OFuIzB,CEvIiC,KFuIjC,CAAA;IAuBJ,KAAA,EAAO,MAAA;IAaD,KAAA,EAAQ,MAAA;;;aEzKjB;ADab;;;;;;AAgBA;AA8EA;;;;AAAsD,iBC9FhC,aAAA,CD8FgC,IAAA,EAAA;;;;ECnHrC,MAAA,EAAA,MAAO,EAAA;EAEL;EAEe,MAAA,CAAA,EAAA,MAAA;CAAR,CAAA,EAwBtB,OAxBsB,CAwBd,OAxBc,CAAA"}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { t as Gerbil } from "../gerbil-Cgmb4Dit.mjs";
|
|
2
|
+
import "../utils-CZBZ8dgR.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/tune/capture.ts
|
|
5
|
+
const config = { dir: "./gerbil-data" };
|
|
6
|
+
const memory = /* @__PURE__ */ new Map();
|
|
7
|
+
function isNode() {
|
|
8
|
+
return typeof process !== "undefined" && !!process.versions && !!process.versions.node;
|
|
9
|
+
}
|
|
10
|
+
function sanitize(task) {
|
|
11
|
+
return task.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "task";
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Configure where captured rows land. Defaults to `./gerbil-data/<task>.jsonl`
|
|
15
|
+
* in Node. Pass `{ sink }` to route rows to your own store instead.
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* configureCapture({ dir: "./data" }); // local files
|
|
19
|
+
* configureCapture({ sink: (row) => db.insert(row) }); // your warehouse
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
function configureCapture(opts) {
|
|
23
|
+
if (opts.dir !== void 0) config.dir = opts.dir;
|
|
24
|
+
if (opts.sink !== void 0) config.sink = opts.sink;
|
|
25
|
+
}
|
|
26
|
+
async function fileFor(task) {
|
|
27
|
+
const fs = await import("node:fs");
|
|
28
|
+
const path = await import("node:path");
|
|
29
|
+
fs.mkdirSync(config.dir, { recursive: true });
|
|
30
|
+
return {
|
|
31
|
+
fs,
|
|
32
|
+
file: path.join(config.dir, `${sanitize(task)}.jsonl`)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
async function writeRow(row) {
|
|
36
|
+
if (config.sink) {
|
|
37
|
+
await config.sink(row);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (isNode()) {
|
|
41
|
+
const { fs, file } = await fileFor(row.task);
|
|
42
|
+
fs.appendFileSync(file, `${JSON.stringify(row)}\n`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const arr = memory.get(row.task) ?? [];
|
|
46
|
+
arr.push(row);
|
|
47
|
+
memory.set(row.task, arr);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Log one production call to a task's dataset.
|
|
51
|
+
*
|
|
52
|
+
* ```ts
|
|
53
|
+
* capture("title-gen", { input: draft, output: title, model: "gpt-5" });
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
async function capture(task, pair) {
|
|
57
|
+
await writeRow({
|
|
58
|
+
task,
|
|
59
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
60
|
+
...pair
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/** Pull readable text out of a common LLM result shape (OpenAI/Anthropic/AI SDK/string). */
|
|
64
|
+
function defaultExtractOutput(result) {
|
|
65
|
+
if (typeof result === "string") return result;
|
|
66
|
+
const r = result;
|
|
67
|
+
const choices = r?.choices;
|
|
68
|
+
if (choices?.[0]?.message?.content) return choices[0].message.content;
|
|
69
|
+
const content = r?.content;
|
|
70
|
+
if (Array.isArray(content) && content[0]?.text) return content[0].text;
|
|
71
|
+
if (typeof r?.text === "string") return r.text;
|
|
72
|
+
return JSON.stringify(result);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Wrap a model call you already make: runs it, returns its result untouched, and
|
|
76
|
+
* captures the input+output to the task's dataset. Provide `input` (you have it);
|
|
77
|
+
* the output is pulled from the result (override with `extract`).
|
|
78
|
+
*
|
|
79
|
+
* ```ts
|
|
80
|
+
* const title = await tap(
|
|
81
|
+
* { task: "title-gen", input: draft, model: "gpt-5" },
|
|
82
|
+
* () => openai.chat.completions.create({ ... }),
|
|
83
|
+
* );
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
async function tap(opts, call) {
|
|
87
|
+
const result = await call();
|
|
88
|
+
const output = opts.extract ? opts.extract(result) : defaultExtractOutput(result);
|
|
89
|
+
await capture(opts.task, {
|
|
90
|
+
input: opts.input,
|
|
91
|
+
output,
|
|
92
|
+
model: opts.model,
|
|
93
|
+
source: opts.source,
|
|
94
|
+
meta: opts.meta
|
|
95
|
+
});
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
async function readRows(task) {
|
|
99
|
+
if (config.sink) throw new Error("dataset() reads the built-in file/memory store; a custom sink owns its own reads.");
|
|
100
|
+
if (isNode()) {
|
|
101
|
+
const fs = await import("node:fs");
|
|
102
|
+
const { file } = await fileFor(task);
|
|
103
|
+
if (!fs.existsSync(file)) return [];
|
|
104
|
+
return fs.readFileSync(file, "utf8").split("\n").filter((l) => l.trim()).map((l) => JSON.parse(l));
|
|
105
|
+
}
|
|
106
|
+
return memory.get(task) ?? [];
|
|
107
|
+
}
|
|
108
|
+
/** Read back a task's dataset (from the file/memory store). */
|
|
109
|
+
function dataset(task) {
|
|
110
|
+
return {
|
|
111
|
+
task,
|
|
112
|
+
rows: () => readRows(task),
|
|
113
|
+
count: async () => (await readRows(task)).length,
|
|
114
|
+
toJSONL: async () => (await readRows(task)).map((r) => JSON.stringify({
|
|
115
|
+
input: r.input,
|
|
116
|
+
output: r.output
|
|
117
|
+
})).join("\n")
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/** List the task datasets that exist, with their row counts. */
|
|
121
|
+
async function datasets() {
|
|
122
|
+
if (config.sink) return {};
|
|
123
|
+
if (isNode()) {
|
|
124
|
+
const fs = await import("node:fs");
|
|
125
|
+
if (!fs.existsSync(config.dir)) return {};
|
|
126
|
+
const out$1 = {};
|
|
127
|
+
for (const name of fs.readdirSync(config.dir)) {
|
|
128
|
+
if (!name.endsWith(".jsonl")) continue;
|
|
129
|
+
const task = name.replace(/\.jsonl$/, "");
|
|
130
|
+
out$1[task] = await dataset(task).count();
|
|
131
|
+
}
|
|
132
|
+
return out$1;
|
|
133
|
+
}
|
|
134
|
+
const out = {};
|
|
135
|
+
for (const [task, rows] of memory) out[task] = rows.length;
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
//#endregion
|
|
140
|
+
//#region src/tune/client.ts
|
|
141
|
+
/**
|
|
142
|
+
* tune() — train (or retrain) a model from your own code.
|
|
143
|
+
*
|
|
144
|
+
* Compiles a captured task dataset (or raw JSONL you pass) and hands it to the
|
|
145
|
+
* Gerbil Tune API with your API key. The only step that sends anything: only the
|
|
146
|
+
* compiled set for that one task leaves, when you call this. Priced by dataset
|
|
147
|
+
* size; charged to the payment method on your account (off-session).
|
|
148
|
+
*
|
|
149
|
+
* ```ts
|
|
150
|
+
* import { tune } from "@tryhamster/gerbil/tune";
|
|
151
|
+
*
|
|
152
|
+
* const model = await tune({ task: "support-triage", base: "qwen3.5-0.8b" });
|
|
153
|
+
* await model.until("completed");
|
|
154
|
+
* console.log(model.outputRepo); // load it with the engine, anywhere
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
const DEFAULT_API = "https://gerbilsdk.com";
|
|
158
|
+
const LIFECYCLE = [
|
|
159
|
+
"awaiting_payment",
|
|
160
|
+
"paid",
|
|
161
|
+
"generating",
|
|
162
|
+
"queued",
|
|
163
|
+
"training",
|
|
164
|
+
"merging",
|
|
165
|
+
"exporting",
|
|
166
|
+
"pushing",
|
|
167
|
+
"completed"
|
|
168
|
+
];
|
|
169
|
+
function resolveApiKey(explicit) {
|
|
170
|
+
const key = explicit ?? (typeof process !== "undefined" ? process.env?.GERBIL_API_KEY : void 0);
|
|
171
|
+
if (!key) throw new Error("tune() needs an API key — pass { apiKey } or set GERBIL_API_KEY. Create one in the dashboard.");
|
|
172
|
+
return key;
|
|
173
|
+
}
|
|
174
|
+
function makeModel(job, apiKey, baseUrl) {
|
|
175
|
+
const model = {
|
|
176
|
+
id: job.id,
|
|
177
|
+
status: job.status,
|
|
178
|
+
outputRepo: job.output_repo ?? null,
|
|
179
|
+
error: job.error ?? null,
|
|
180
|
+
async refresh() {
|
|
181
|
+
const res = await fetch(`${baseUrl}/api/tune/from-code?job=${encodeURIComponent(model.id)}`, { headers: { Authorization: `Bearer ${apiKey}` } });
|
|
182
|
+
const data = await res.json().catch(() => ({}));
|
|
183
|
+
if (!res.ok || !data.job) throw new Error(data.error ?? `tune status failed: ${res.status}`);
|
|
184
|
+
model.status = data.job.status;
|
|
185
|
+
model.outputRepo = data.job.output_repo ?? null;
|
|
186
|
+
model.error = data.job.error ?? null;
|
|
187
|
+
return model;
|
|
188
|
+
},
|
|
189
|
+
async until(target = "completed", opts = {}) {
|
|
190
|
+
const goal = target === "ready" ? "completed" : target;
|
|
191
|
+
const goalIdx = LIFECYCLE.indexOf(goal);
|
|
192
|
+
const intervalMs = opts.intervalMs ?? 5e3;
|
|
193
|
+
const timeoutMs = opts.timeoutMs ?? 30 * 6e4;
|
|
194
|
+
const start = Date.now();
|
|
195
|
+
while (Date.now() - start < timeoutMs) {
|
|
196
|
+
await model.refresh();
|
|
197
|
+
if (model.status === "failed") throw new Error(`tune job failed: ${model.error ?? "unknown error"}`);
|
|
198
|
+
const curIdx = LIFECYCLE.indexOf(model.status);
|
|
199
|
+
if (model.status === goal || goalIdx >= 0 && curIdx >= 0 && curIdx >= goalIdx) return model;
|
|
200
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
201
|
+
}
|
|
202
|
+
throw new Error(`tune().until(${target}) timed out after ${timeoutMs}ms`);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
return model;
|
|
206
|
+
}
|
|
207
|
+
/** Train (or retrain) a model from your own code. See the module docs. */
|
|
208
|
+
async function tune(opts) {
|
|
209
|
+
const apiKey = resolveApiKey(opts.apiKey);
|
|
210
|
+
const envUrl = typeof process !== "undefined" ? process.env?.GERBIL_API_URL : void 0;
|
|
211
|
+
const baseUrl = (opts.baseUrl ?? envUrl ?? DEFAULT_API).replace(/\/+$/, "");
|
|
212
|
+
let jsonl = opts.data;
|
|
213
|
+
if (!jsonl && opts.task) jsonl = await dataset(opts.task).toJSONL();
|
|
214
|
+
if (!jsonl || !jsonl.trim()) throw new Error("tune() needs data — pass { task } (a captured dataset) or { data } (JSONL).");
|
|
215
|
+
const res = await fetch(`${baseUrl}/api/tune/from-code`, {
|
|
216
|
+
method: "POST",
|
|
217
|
+
headers: {
|
|
218
|
+
Authorization: `Bearer ${apiKey}`,
|
|
219
|
+
"Content-Type": "application/json"
|
|
220
|
+
},
|
|
221
|
+
body: JSON.stringify({
|
|
222
|
+
base: opts.base,
|
|
223
|
+
epochs: opts.epochs,
|
|
224
|
+
jsonl
|
|
225
|
+
})
|
|
226
|
+
});
|
|
227
|
+
const data = await res.json().catch(() => ({}));
|
|
228
|
+
if (!res.ok || !data.job) throw new Error(data.error ?? `tune() failed: ${res.status}`);
|
|
229
|
+
return makeModel(data.job, apiKey, baseUrl);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
//#endregion
|
|
233
|
+
//#region src/tune/labeler.ts
|
|
234
|
+
/**
|
|
235
|
+
* createLabeler — run a classifier you tuned, LOCALLY, to label your own data.
|
|
236
|
+
*
|
|
237
|
+
* The labeler is a small model you trained on us (paid Tune); this just *runs* it
|
|
238
|
+
* on-device (browser via WebGPU, or Node via Dawn — fast on a GPU server, CPU
|
|
239
|
+
* fallback otherwise). It wraps the engine's retrying `generateObject` with a
|
|
240
|
+
* predicate that enforces your label set, so every result is a clean, known label
|
|
241
|
+
* — and your raw data never leaves the machine.
|
|
242
|
+
*/
|
|
243
|
+
/**
|
|
244
|
+
* ```ts
|
|
245
|
+
* const labeler = await createLabeler({
|
|
246
|
+
* model: "your-org/support-triage",
|
|
247
|
+
* labels: ["billing", "bug", "feature-request", "other"],
|
|
248
|
+
* });
|
|
249
|
+
* await labeler("My card was charged twice"); // → { label: "billing" }
|
|
250
|
+
* await labeler.labelInto("triage", rawInputs); // labels + captures locally
|
|
251
|
+
* ```
|
|
252
|
+
*/
|
|
253
|
+
async function createLabeler(opts) {
|
|
254
|
+
const g = new Gerbil();
|
|
255
|
+
await g.loadModel(opts.model);
|
|
256
|
+
const allowed = new Set(opts.labels);
|
|
257
|
+
const system = opts.system ?? `Classify the input into exactly one category from: ${opts.labels.join(", ")}. Reply with ONLY JSON: {"label": "<category>"}.`;
|
|
258
|
+
const one = async (input) => {
|
|
259
|
+
const { object } = await g.generateObject(input, {
|
|
260
|
+
schema: (o) => {
|
|
261
|
+
const l = o?.label;
|
|
262
|
+
return typeof l === "string" && allowed.has(l);
|
|
263
|
+
},
|
|
264
|
+
systemPrompt: system
|
|
265
|
+
});
|
|
266
|
+
return { label: object.label };
|
|
267
|
+
};
|
|
268
|
+
const many = async (inputs) => {
|
|
269
|
+
const out = [];
|
|
270
|
+
for (const input of inputs) {
|
|
271
|
+
const { label } = await one(input);
|
|
272
|
+
out.push({
|
|
273
|
+
input,
|
|
274
|
+
label
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
return out;
|
|
278
|
+
};
|
|
279
|
+
const labelInto = async (task, inputs) => {
|
|
280
|
+
const labeled = await many(inputs);
|
|
281
|
+
for (const row of labeled) await capture(task, {
|
|
282
|
+
input: row.input,
|
|
283
|
+
output: row.label,
|
|
284
|
+
model: opts.model
|
|
285
|
+
});
|
|
286
|
+
return labeled;
|
|
287
|
+
};
|
|
288
|
+
return Object.assign(one, {
|
|
289
|
+
many,
|
|
290
|
+
labelInto,
|
|
291
|
+
dispose: () => g.dispose()
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
//#endregion
|
|
296
|
+
export { capture, configureCapture, createLabeler, dataset, datasets, tap, tune };
|
|
297
|
+
//# sourceMappingURL=index.mjs.map
|