From f8dbb2e08aac902a2302010342b166d8dc38f0e1 Mon Sep 17 00:00:00 2001 From: Rob Colbert Date: Wed, 6 May 2026 22:58:03 -0400 Subject: [PATCH] agent tool and toolbox - created AiTool and AiToolbox for representing tools in the API - add googleapis dependency - integrate Google Search tool as first agent tool - created IAiEnvironment to communicate AI environment vars around the platform --- gadget-code/src/config/env.ts | 6 + gadget-code/src/web-cli.ts | 13 +- gadget-drone/src/config/env.ts | 8 +- gadget-drone/src/services/ai.ts | 19 +- packages/ai/package.json | 11 +- packages/ai/src/api.ts | 8 +- packages/ai/src/config/env.ts | 12 ++ packages/ai/src/index.ts | 13 +- packages/ai/src/ollama.ts | 8 +- packages/ai/src/openai.ts | 33 ++- packages/ai/src/toolbox.ts | 76 +++++++ packages/ai/src/tools/search/google.ts | 273 +++++++++++++++++++++++++ packages/ai/src/tools/tool-error.ts | 50 +++++ packages/ai/src/tools/tool.ts | 35 ++++ packages/config/src/types.ts | 12 ++ pnpm-lock.yaml | 158 ++++++++++++++ 16 files changed, 715 insertions(+), 20 deletions(-) create mode 100644 packages/ai/src/config/env.ts create mode 100644 packages/ai/src/toolbox.ts create mode 100644 packages/ai/src/tools/search/google.ts create mode 100644 packages/ai/src/tools/tool-error.ts create mode 100644 packages/ai/src/tools/tool.ts diff --git a/gadget-code/src/config/env.ts b/gadget-code/src/config/env.ts index b6280e0..d0f9cb1 100644 --- a/gadget-code/src/config/env.ts +++ b/gadget-code/src/config/env.ts @@ -94,6 +94,12 @@ export default { sameSite: yamlConfig.session?.cookie?.sameSite || false, }, }, + google: { + cse: { + apiKey: yamlConfig.google.cse.apiKey, + engineId: yamlConfig.google.cse.engineId, + }, + }, mongodb: { host: yamlConfig.mongodb?.host || "localhost", database: yamlConfig.mongodb?.database || "", diff --git a/gadget-code/src/web-cli.ts b/gadget-code/src/web-cli.ts index ea3e496..877964d 100644 --- a/gadget-code/src/web-cli.ts +++ b/gadget-code/src/web-cli.ts @@ -2,6 +2,16 @@ // Copyright (C) 2026 Robert Colbert // All Rights Reserved +import env from "./config/env.js"; +const aiEnv: IAiEnvironment = { + NODE_ENV: env.NODE_ENV || "develop", + google: { + cse: { + apiKey: env.google.cse.apiKey, + engineId: env.google.cse.engineId, + }, + }, +}; import { v4 as uuidv4 } from "uuid"; import "./lib/db.js"; @@ -30,7 +40,7 @@ import { * App Logic */ -import { createAiApi, type IAiLogger } from "@gadget/ai"; +import { createAiApi, IAiEnvironment, type IAiLogger } from "@gadget/ai"; import { IUser, @@ -551,6 +561,7 @@ class DtpWebCli extends DtpProcess { }; const api = createAiApi( + aiEnv, { _id: provider._id, name: provider.name, diff --git a/gadget-drone/src/config/env.ts b/gadget-drone/src/config/env.ts index a6e1be2..fe71ab4 100644 --- a/gadget-drone/src/config/env.ts +++ b/gadget-drone/src/config/env.ts @@ -37,7 +37,7 @@ async function readJsonFile(filePath: string): Promise { /* eslint-disable no-process-env */ export default { - NODE_ENV: process.env.NODE_ENV, + NODE_ENV: process.env.NODE_ENV || "develop", timezone: yamlConfig.timezone || "America/New_York", installDir: INSTALL_DIR, pkg: await readJsonFile( @@ -47,6 +47,12 @@ export default { baseUrl: yamlConfig.platform.baseUrl, gadgetKey: yamlConfig.platform.gadgetKey, }, + google: { + cse: { + apiKey: yamlConfig.google?.cse?.apiKey, + engineId: yamlConfig.google?.cse?.engineId, + }, + }, log: { console: { enabled: yamlConfig.logging?.console?.enabled === true, diff --git a/gadget-drone/src/services/ai.ts b/gadget-drone/src/services/ai.ts index 6f575c1..eb73a26 100644 --- a/gadget-drone/src/services/ai.ts +++ b/gadget-drone/src/services/ai.ts @@ -2,8 +2,10 @@ // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 -import { IAiProvider as DbAiProvider } from "@gadget/api"; -import { GadgetService } from "../lib/service.ts"; +import env from "../config/env.ts"; + +import { IAiProvider as DbAiProvider, GadgetId } from "@gadget/api"; +import { GadgetService } from "../lib/service.js"; import { type IAiChatOptions, type IAiChatResponse, @@ -13,8 +15,8 @@ import { type IAiProvider as AiProviderConfig, type IAiResponseStreamFn, createAiApi, + IAiEnvironment, } from "@gadget/ai"; -import { GadgetId } from "../../../packages/api/dist/lib/gadget-id.js"; /** * Drone-specific model config that accepts the database provider type. @@ -126,7 +128,16 @@ class AiService extends GadgetService { } getApi(provider: AiProviderConfig) { - return createAiApi(provider, this.log); + const aiEnv: IAiEnvironment = { + NODE_ENV: env.NODE_ENV, + google: { + cse: { + apiKey: env.google.cse.apiKey, + engineId: env.google.cse.engineId, + }, + }, + }; + return createAiApi(aiEnv, provider, this.log); } } diff --git a/packages/ai/package.json b/packages/ai/package.json index a290381..c0a3e3b 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -15,10 +15,17 @@ "build": "tsc", "dev": "tsc --watch" }, - "keywords": ["gadget", "ai", "ollama", "openai", "abstraction"], + "keywords": [ + "gadget", + "ai", + "agent", + "ollama", + "openai" + ], "author": "Rob Colbert", "license": "Apache-2.0", "dependencies": { + "googleapis": "^171.4.0", "numeral": "^2.0.6", "ollama": "^0.6.3", "openai": "^6.34.0" @@ -28,4 +35,4 @@ "@types/numeral": "^2.0.5", "typescript": "^6.0.3" } -} \ No newline at end of file +} diff --git a/packages/ai/src/api.ts b/packages/ai/src/api.ts index ed9c356..1a0151d 100644 --- a/packages/ai/src/api.ts +++ b/packages/ai/src/api.ts @@ -1,6 +1,9 @@ // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 +import { IAiEnvironment } from "./config/env.ts"; +import { AiTool } from "./tools/tool.ts"; + export type AiSdkType = "ollama" | "openai"; export interface IAiProvider { @@ -63,6 +66,7 @@ export interface IAiChatOptions { systemPrompt?: string; userPrompt?: string; context?: IContextChatMessage[]; + tools?: AiTool[]; } export interface IToolCall { @@ -133,10 +137,12 @@ export interface IAiModelProbeResult { } export abstract class AiApi { + protected env: IAiEnvironment; protected provider: IAiProvider; protected log: IAiLogger; - constructor(provider: IAiProvider, logger?: IAiLogger) { + constructor(env: IAiEnvironment, provider: IAiProvider, logger?: IAiLogger) { + this.env = env; this.provider = provider; this.log = logger ?? defaultLogger(); } diff --git a/packages/ai/src/config/env.ts b/packages/ai/src/config/env.ts new file mode 100644 index 0000000..7e1057d --- /dev/null +++ b/packages/ai/src/config/env.ts @@ -0,0 +1,12 @@ +// Copyright (C) 2026 Rob Colbert +// Licensed under the Apache License, Version 2.0 + +export interface IAiEnvironment { + NODE_ENV: string; + google: { + cse: { + apiKey: string | undefined; + engineId: string | undefined; + }; + }; +} diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index c479fee..e4492bd 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -1,6 +1,8 @@ // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 +export { IAiEnvironment } from "./config/env.ts"; + export { type AiSdkType, type IAiProvider, @@ -29,13 +31,18 @@ import { OpenAiApi } from "./openai.js"; import type { IAiProvider } from "./api.js"; import type { IAiLogger } from "./api.js"; import type { AiApi } from "./api.js"; +import { IAiEnvironment } from "./config/env.ts"; -export function createAiApi(provider: IAiProvider, logger?: IAiLogger): AiApi { +export function createAiApi( + env: IAiEnvironment, + provider: IAiProvider, + logger?: IAiLogger, +): AiApi { switch (provider.sdk) { case "ollama": - return new OllamaAiApi(provider, logger); + return new OllamaAiApi(env, provider, logger); case "openai": - return new OpenAiApi(provider, logger); + return new OpenAiApi(env, provider, logger); default: throw new Error(`Unknown AI SDK: ${(provider as IAiProvider).sdk}`); } diff --git a/packages/ai/src/ollama.ts b/packages/ai/src/ollama.ts index 69f19ad..4d44828 100644 --- a/packages/ai/src/ollama.ts +++ b/packages/ai/src/ollama.ts @@ -19,12 +19,13 @@ import { IAiProvider, IAiResponseStreamFn, } from "./api.js"; +import { IAiEnvironment } from "./config/env.ts"; export class OllamaAiApi extends AiApi { protected client: Ollama; - constructor(provider: IAiProvider, logger?: IAiLogger) { - super(provider, logger); + constructor(env: IAiEnvironment, provider: IAiProvider, logger?: IAiLogger) { + super(env, provider, logger); this.client = new Ollama({ host: this.provider.baseUrl, headers: { Authorization: `Bearer ${this.provider.apiKey}` }, @@ -105,7 +106,8 @@ export class OllamaAiApi extends AiApi { !!modelInfo?.["clip"], hasEmbedding: capabilities.includes("embeddings"), hasThinking: capabilities.includes("reasoning"), - isInstructTuned: modelId.toLowerCase().includes("instruct") || + isInstructTuned: + modelId.toLowerCase().includes("instruct") || modelId.toLowerCase().includes("chat") || modelId.toLowerCase().includes("-it"), }; diff --git a/packages/ai/src/openai.ts b/packages/ai/src/openai.ts index feee555..304b5ba 100644 --- a/packages/ai/src/openai.ts +++ b/packages/ai/src/openai.ts @@ -17,6 +17,11 @@ import { IAiProvider, IAiResponseStreamFn, } from "./api.js"; +import { + ChatCompletionFunctionTool, + ChatCompletionTool, +} from "openai/resources"; +import { IAiEnvironment } from "./config/env.ts"; interface GabAiCapabilities { text?: boolean; @@ -50,8 +55,8 @@ interface OpenAIModelInfo { export class OpenAiApi extends AiApi { protected client: OpenAI; - constructor(provider: IAiProvider, logger?: IAiLogger) { - super(provider, logger); + constructor(env: IAiEnvironment, provider: IAiProvider, logger?: IAiLogger) { + super(env, provider, logger); this.client = new OpenAI({ baseURL: provider.baseUrl, apiKey: provider.apiKey, @@ -127,12 +132,15 @@ export class OpenAiApi extends AiApi { return { capabilities: { canCallTools: modelId.toLowerCase().includes("gpt"), - hasVision: modelId.toLowerCase().includes("vision") || + hasVision: + modelId.toLowerCase().includes("vision") || modelId.toLowerCase().includes("4o") || modelId.toLowerCase().includes("image"), - hasEmbedding: modelId.toLowerCase().includes("embedding") || + hasEmbedding: + modelId.toLowerCase().includes("embedding") || modelId.toLowerCase().includes("embed"), - hasThinking: modelId.toLowerCase().includes("o1") || + hasThinking: + modelId.toLowerCase().includes("o1") || modelId.toLowerCase().includes("o3") || modelId.toLowerCase().includes("reasoning"), isInstructTuned: true, @@ -242,9 +250,24 @@ export class OpenAiApi extends AiApi { messages.push({ role: "user" as const, content: options.userPrompt }); } + const tools: ChatCompletionTool[] = options.tools + ? options.tools.map((tool) => { + const openaiTool: ChatCompletionFunctionTool = { + type: tool.definition.type, + function: { + name: tool.definition.function.name, + description: tool.definition.function.description, + parameters: tool.definition.function.parameters, + }, + }; + return openaiTool; + }) + : []; + const response = await this.client.chat.completions.create({ model: model.modelId, messages, + tools, stream: false, }); diff --git a/packages/ai/src/toolbox.ts b/packages/ai/src/toolbox.ts new file mode 100644 index 0000000..98e09f4 --- /dev/null +++ b/packages/ai/src/toolbox.ts @@ -0,0 +1,76 @@ +// Copyright (C) 2026 Rob Colbert +// Licensed under the Apache License, Version 2.0 + +import { IAiEnvironment } from "./config/env.ts"; +import { AiTool } from "./tools/tool.ts"; + +export type ToolMap = Map; +export type ToolSet = Set; + +/** + * No. I don't want to create an "MCP" server. I just want an in-process + * toolbox that the agents can use in their daily work. That's not too much to + * ask, dammit. + */ +export class AiToolbox { + private _env: IAiEnvironment; + get env(): IAiEnvironment { + return this._env; + } + + private tools: ToolMap = new Map(); + private modeSets: Map = new Map>(); + + constructor(env: IAiEnvironment) { + this._env = env; + } + + /** + * Registers an AiTool instance for use by the platform. If no ChatSessionMode + * modes are specified, you are registering a system tool - such as the chat + * session auto-naming tool - which are not called by agents (they are called) + * by the platform itself, deterministically. + * @param tool the tool being registered for use by the platform + * @param modes the optional name(s) of the mode for which the tool is being + * registered + */ + register(tool: AiTool, modes?: string[]): void { + if (this.tools.has(tool.name)) { + throw new Error(`tool already registered: ${tool.name}`); + } + this.tools.set(tool.name, tool); + + if (!modes) { + return; // system tools aren't listed in the modes for agent use + } + + for (const mode of modes) { + let set = this.modeSets.get(mode); + if (!set) { + set = new Set(); + this.modeSets.set(mode, set); + } + set.add(tool); + } + } + + /** + * Retrieve a tool instance from the toolbox by name, ignoring mode(s). This + * is how the system fetches system tools for use. + * @param name the name of the tool to be retrieved + * @returns the tool, or undefined if the tool is not registered + */ + getTool(name: string): AiTool | undefined { + return this.tools.get(name); + } + + /** + * Retrieves the set of tools registered for use in a given ChatSessionMode. + * @param mode the ChatSessionMode for which a set of tools is being requested + * @returns the set of tools, or undefined if there is not set for the mode + * @todo the mode parameter should be the ChatSessionMode enum + */ + getModeSet(mode: string): ToolSet | undefined { + return this.modeSets.get(mode); + } +} diff --git a/packages/ai/src/tools/search/google.ts b/packages/ai/src/tools/search/google.ts new file mode 100644 index 0000000..b8584fd --- /dev/null +++ b/packages/ai/src/tools/search/google.ts @@ -0,0 +1,273 @@ +// Copyright (C) 2026 Rob Colbert +// Licensed under the Apache License, Version 2.0 + +import { IAiLogger } from "../../api.ts"; +import { formatError } from "../tool-error.ts"; +import { AiTool, IToolArguments, IToolDefinition } from "../tool.ts"; + +import { google } from "googleapis"; +// import SearchService, { SearchServiceError } from "../../services/search.js"; + +export interface ISearchResult { + title: string; + link: string; + snippet: string; + image?: string; + position?: number; + displayLink?: string; +} + +export interface ISearchOptions { + num?: number; + siteSearch?: string; + dateRestrict?: string; + fileType?: string; + safe?: "active" | "off"; + sort?: "relevance" | "date"; + start?: number; +} + +export class GoogleSearchTool extends AiTool { + get name(): string { + return "search_google"; + } + + get category(): string { + return "search"; + } + + public definition: IToolDefinition = { + type: "function", + function: { + name: this.name, + description: + "Perform a Google search for relevant information on the web.", + parameters: { + type: "object", + properties: { + query: { + type: "string", + description: "The search query string.", + }, + num_results: { + type: "number", + description: + "Number of search results to return (default: 10, max: 10).", + }, + siteSearch: { + type: "string", + description: + "Optional site to restrict the search to (e.g. github.com).", + }, + dateRestrict: { + type: "string", + description: + "Restricts results to documents based on a date range. Examples: d1 (last day), d7 (last week), d30 (last month), d365 (last year).", + }, + fileType: { + type: "string", + description: + "Restricts results to files of a specified extension. Examples: pdf, doc, xls, ppt.", + }, + sort: { + type: "string", + description: + "Sort order for results. Values: 'relevance' (default) or 'date'.", + enum: ["relevance", "date"], + }, + start: { + type: "number", + description: + "The index of the first result to return (for pagination). Default: 1.", + }, + }, + required: ["query"], + }, + }, + }; + + public async execute( + args: IToolArguments, + logger: IAiLogger, + ): Promise { + const { query } = args; + + if (!query || typeof query !== "string" || query.trim().length === 0) { + return formatError({ + code: "MISSING_PARAMETER", + message: "The 'query' parameter is required.", + parameter: "query", + expected: "A non-empty string containing the search query.", + example: 'search_google(query: "latest AI news")', + recoveryHint: + "Provide a 'query' parameter with your search terms and try again.", + }); + } + + logger.debug("performing Google search for user", { args }); + + try { + const { + num_results = 10, + siteSearch, + dateRestrict, + fileType, + sort, + start, + } = args; + + const results = await this.search(query, { + num: Math.min(num_results as number, 10), + siteSearch: siteSearch as string | undefined, + dateRestrict: dateRestrict as string | undefined, + fileType: fileType as string | undefined, + safe: "active", + sort: sort as "relevance" | "date" | undefined, + start: start as number | undefined, + }); + + logger.debug("Google search results", { results }); + + let content = ""; + if (results && results.length) { + content += `Here are some relevant search results I found:\n\n`; + for (const result of results) { + const title = JSON.stringify(result.title || "").slice(1, -1); + const link = JSON.stringify(result.link || "").slice(1, -1); + const snippet = JSON.stringify(result.snippet || "").slice(1, -1); + const displayLink = result.displayLink + ? JSON.stringify(result.displayLink).slice(1, -1) + : ""; + + content += `Title: ${title}\n`; + content += `Link: ${link}\n`; + if (displayLink) { + content += `Source: ${displayLink}\n`; + } + content += `Snippet: ${snippet}\n\n`; + } + } else { + content += "No relevant search results found."; + } + + return content; + } catch (error: any) { + // Generic error handling + return formatError({ + code: "OPERATION_FAILED", + message: `Failed to perform search: ${error.message}`, + recoveryHint: "Please try again or check your search query.", + }); + } + } + + async search( + query: string, + options: ISearchOptions, + ): Promise { + const customSearch = google.customsearch({ + version: "v1", + auth: this.toolbox.env.google.cse.apiKey, + }); + + const params: any = { + q: query, + cx: this.toolbox.env.google.cse.engineId, + num: options.num || 10, + }; + + if (options.siteSearch) { + params.siteSearch = options.siteSearch; + } + if (options.dateRestrict) { + params.dateRestrict = options.dateRestrict; + } + if (options.fileType) { + params.fileType = options.fileType; + } + if (options.safe) { + params.safe = options.safe; + } + if (options.sort) { + params.sort = options.sort; + } + if (options.start) { + params.start = options.start; + } + + const response = await customSearch.cse.list(params); + const results: ISearchResult[] = []; + + if (response.data.items) { + response.data.items.forEach((item: any, index: number) => { + const result: ISearchResult = { + title: item.title || "", + link: item.link || "", + snippet: item.snippet || "", + position: item.position || index + 1, + displayLink: item.displayLink || "", + }; + + // Extract thumbnail if available + if (item.pagemap?.cse_thumbnail?.[0]?.src) { + result.image = item.pagemap.cse_thumbnail[0].src; + } + + results.push(result); + }); + } + + return results; + } + + /** + * Parse Google CSE API errors and provide meaningful messages + */ + private parseCseError(error: any): string { + // Handle Google API error structure + if (error.response?.data?.error) { + const apiError = error.response.data.error; + const statusCode = error.response.status; + + switch (statusCode) { + case 401: + formatError({ + code: "UNAUTHORIZED", + message: `401 Unauthorized - Invalid API key. ${apiError.message || ""}`, + }); + case 403: + return formatError({ + code: "FORBIDDEN", + message: `403 Forbidden - ${apiError.message || "Access denied"}. Check your Engine ID and API key permissions.`, + }); + case 429: + const retryAfter = error.response.headers?.["retry-after"]; + return formatError({ + code: "RATE_LIMIT_EXCEEDED", + message: `429 Too Many Requests - Rate limit exceeded. ${retryAfter ? `Retry after: ${retryAfter} seconds.` : ""} ${apiError.message || ""}`, + }); + default: + break; + } + + return formatError({ + code: "TOOL_EXECUTION_FAILED", + message: `HTTP ${statusCode} - ${apiError.message || "Unknown error"}`, + }); + } + + // Handle network errors + if (error.code === "ENOTFOUND") { + return formatError({ + code: "NETWORK_ERROR", + message: `Network error (code: ${error.code}) - Unable to reach Google API`, + }); + } + + // Generic error + return formatError({ + code: error.code || "UNKNOWN_ERROR", + message: error.message || "An unknown error occurred", + }); + } +} diff --git a/packages/ai/src/tools/tool-error.ts b/packages/ai/src/tools/tool-error.ts new file mode 100644 index 0000000..262c993 --- /dev/null +++ b/packages/ai/src/tools/tool-error.ts @@ -0,0 +1,50 @@ +// Copyright (C) 2026 Rob Colbert +// Licensed under the Apache License, Version 2.0 + +export type ToolErrorCode = + | "MISSING_PARAMETER" + | "INVALID_PARAMETER" + | "NOT_FOUND" + | "PERMISSION_DENIED" + | "OPERATION_FAILED" + | "OPERATION_NOT_ALLOWED" + | "VALIDATION_ERROR" + | "RATE_LIMITED" + | "LIMIT_EXCEEDED" + | "INVALID_CRON_SPEC" + | "INVALID_OPERATION" + | "TIMEOUT" + | "INVALID_TOOL_ARGUMENTS" + | "SUBAGENT_FAILED" + | "TOOL_EXECUTION_FAILED" + | "UNAUTHORIZED" + | "FORBIDDEN" + | "RATE_LIMIT_EXCEEDED" + | "NETWORK_ERROR" + | "SECURITY_VIOLATION"; + +export interface IToolError { + code: ToolErrorCode; + message: string; + parameter?: string; + expected?: string; + example?: string; + recoveryHint?: string; +} + +export function formatError(error: IToolError): string { + const components: string[] = [`TOOL ERROR: ${error.code}`, error.message]; + if (error.parameter) { + components.push(`PARAMETER: ${error.parameter}`); + } + if (error.expected) { + components.push(`EXPECTED: ${error.expected}`); + } + if (error.example) { + components.push(`EXAMPLE: ${error.example}`); + } + if (error.recoveryHint) { + components.push(`RECOVERY HINT: ${error.recoveryHint}`); + } + return components.join("\n"); +} diff --git a/packages/ai/src/tools/tool.ts b/packages/ai/src/tools/tool.ts new file mode 100644 index 0000000..118641f --- /dev/null +++ b/packages/ai/src/tools/tool.ts @@ -0,0 +1,35 @@ +// Copyright (C) 2026 Rob Colbert +// Licensed under the Apache License, Version 2.0 + +import { IAiLogger } from "../api.ts"; +import { AiToolbox } from "../toolbox.ts"; + +export interface IToolArguments { + [key: string]: unknown; +} + +export interface IToolDefinition { + type: "function"; + function: { + name: string; + description: string; + parameters: IToolArguments; + }; +} + +export abstract class AiTool { + protected _toolbox: AiToolbox; + get toolbox(): AiToolbox { + return this._toolbox; + } + + constructor(toolbox: AiToolbox) { + this._toolbox = toolbox; + } + + abstract get name(): string; + abstract get category(): string; + abstract get definition(): IToolDefinition; + + abstract execute(args: IToolArguments, logger: IAiLogger): Promise; +} diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts index 796ea95..5764162 100644 --- a/packages/config/src/types.ts +++ b/packages/config/src/types.ts @@ -30,6 +30,12 @@ export interface GadgetCodeConfig { sameSite?: boolean | "lax" | "strict" | "none"; }; }; + google: { + cse: { + apiKey: string; + engineId: string; + }; + }; mongodb: { host: string; database: string; @@ -120,6 +126,12 @@ export interface GadgetDroneConfig { baseUrl: string; gadgetKey: string; }; + google?: { + cse?: { + apiKey: string; + engineId: string; + }; + }; logging?: { console?: { enabled?: boolean; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 43d6047..23cbbaf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -315,6 +315,9 @@ importers: packages/ai: dependencies: + googleapis: + specifier: ^171.4.0 + version: 171.4.0 numeral: specifier: ^2.0.6 version: 2.0.6 @@ -1502,6 +1505,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1594,6 +1601,9 @@ packages: bidi-js@1.0.3: resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -1832,6 +1842,10 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + data-urls@7.0.0: resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -2047,6 +2061,9 @@ packages: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -2082,6 +2099,10 @@ packages: picomatch: optional: true + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + fflate@0.6.10: resolution: {integrity: sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==} @@ -2113,6 +2134,10 @@ packages: debug: optional: true + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -2147,6 +2172,14 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gaxios@7.1.4: + resolution: {integrity: sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==} + engines: {node: '>=18'} + + gcp-metadata@8.1.2: + resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} + engines: {node: '>=18'} + geoip-lite@1.4.10: resolution: {integrity: sha512-4N69uhpS3KFd97m00wiFEefwa+L+HT5xZbzPhwu+sDawStg6UN/dPwWtUfkQuZkGIY1Cj7wDVp80IsqNtGMi2w==} engines: {node: '>=10.3.0'} @@ -2189,6 +2222,22 @@ packages: glsl-noise@0.0.0: resolution: {integrity: sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==} + google-auth-library@10.6.2: + resolution: {integrity: sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==} + engines: {node: '>=18'} + + google-logging-utils@1.1.3: + resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} + engines: {node: '>=14'} + + googleapis-common@8.0.1: + resolution: {integrity: sha512-eCzNACUXPb1PW5l0ULTzMHaL/ltPRADoPgjBlT8jWsTbxkCp6siv+qKJ/1ldaybCthGwsYFYallF7u9AkU4L+A==} + engines: {node: '>=18.0.0'} + + googleapis@171.4.0: + resolution: {integrity: sha512-xybFL2SmmUgIifgsbsRQYRdNrSAYwxWZDmkZTGjUIaRnX5jPqR8el/cEvo6rCqh7iaZx6MfEPS/lrDgZ0bymkg==} + engines: {node: '>=18'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -2235,6 +2284,10 @@ packages: resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} engines: {node: '>=8.0.0'} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -2381,6 +2434,9 @@ packages: canvas: optional: true + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + jsonfile@3.0.1: resolution: {integrity: sha512-oBko6ZHlubVB5mRFkur5vgYR1UyqX+S6Y/oCfLhqNdcc2fYFlDpIoNc7AfKS1KOGcnNAkvsr0grLck9ANM815w==} @@ -2732,6 +2788,15 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-gyp-build-optional-packages@5.2.2: resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} hasBin: true @@ -3477,6 +3542,9 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + url-template@2.0.8: + resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==} + use-sync-external-store@1.6.0: resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: @@ -3597,6 +3665,10 @@ packages: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + webgl-constants@1.1.1: resolution: {integrity: sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==} @@ -4635,6 +4707,8 @@ snapshots: acorn@7.4.1: {} + agent-base@7.1.4: {} + ansi-regex@5.0.1: {} ansi-styles@4.3.0: @@ -4707,6 +4781,8 @@ snapshots: dependencies: require-from-string: 2.0.2 + bignumber.js@9.3.1: {} + binary-extensions@2.3.0: {} block-stream2@2.1.0: @@ -5005,6 +5081,8 @@ snapshots: csstype@3.2.3: {} + data-uri-to-buffer@4.0.1: {} + data-urls@7.0.0: dependencies: whatwg-mimetype: 5.0.0 @@ -5276,6 +5354,8 @@ snapshots: transitivePeerDependencies: - supports-color + extend@3.0.2: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -5317,6 +5397,11 @@ snapshots: optionalDependencies: picomatch: 4.0.4 + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + fflate@0.6.10: {} fflate@0.8.2: {} @@ -5352,6 +5437,10 @@ snapshots: follow-redirects@1.16.0: {} + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + forwarded@0.2.0: {} fraction.js@5.3.4: {} @@ -5376,6 +5465,22 @@ snapshots: function-bind@1.1.2: {} + gaxios@7.1.4: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + transitivePeerDependencies: + - supports-color + + gcp-metadata@8.1.2: + dependencies: + gaxios: 7.1.4 + google-logging-utils: 1.1.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + geoip-lite@1.4.10: dependencies: async: 2.6.4 @@ -5438,6 +5543,36 @@ snapshots: glsl-noise@0.0.0: {} + google-auth-library@10.6.2: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 7.1.4 + gcp-metadata: 8.1.2 + google-logging-utils: 1.1.3 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + + google-logging-utils@1.1.3: {} + + googleapis-common@8.0.1: + dependencies: + extend: 3.0.2 + gaxios: 7.1.4 + google-auth-library: 10.6.2 + qs: 6.15.1 + url-template: 2.0.8 + transitivePeerDependencies: + - supports-color + + googleapis@171.4.0: + dependencies: + google-auth-library: 10.6.2 + googleapis-common: 8.0.1 + transitivePeerDependencies: + - supports-color + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -5488,6 +5623,13 @@ snapshots: transitivePeerDependencies: - debug + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 @@ -5637,6 +5779,10 @@ snapshots: transitivePeerDependencies: - '@noble/hashes' + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + jsonfile@3.0.1: optionalDependencies: graceful-fs: 4.2.11 @@ -5956,6 +6102,14 @@ snapshots: negotiator@1.0.0: {} + node-domexception@1.0.0: {} + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + node-gyp-build-optional-packages@5.2.2: dependencies: detect-libc: 2.1.2 @@ -6738,6 +6892,8 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + url-template@2.0.8: {} + use-sync-external-store@1.6.0(react@19.2.5): dependencies: react: 19.2.5 @@ -6803,6 +6959,8 @@ snapshots: dependencies: xml-name-validator: 5.0.0 + web-streams-polyfill@3.3.3: {} + webgl-constants@1.1.1: {} webgl-sdf-generator@1.1.1: {}