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
This commit is contained in:
Rob Colbert 2026-05-06 22:58:03 -04:00
parent 819654e20a
commit f8dbb2e08a
16 changed files with 715 additions and 20 deletions

View File

@ -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 || "",

View File

@ -2,6 +2,16 @@
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
// 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,

View File

@ -37,7 +37,7 @@ async function readJsonFile<T>(filePath: string): Promise<T> {
/* 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<typeof PackageJson>(
@ -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,

View File

@ -2,8 +2,10 @@
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
// 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);
}
}

View File

@ -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"
}
}
}

View File

@ -1,6 +1,9 @@
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
// 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();
}

View File

@ -0,0 +1,12 @@
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
// Licensed under the Apache License, Version 2.0
export interface IAiEnvironment {
NODE_ENV: string;
google: {
cse: {
apiKey: string | undefined;
engineId: string | undefined;
};
};
}

View File

@ -1,6 +1,8 @@
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
// 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}`);
}

View File

@ -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"),
};

View File

@ -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,
});

View File

@ -0,0 +1,76 @@
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
// Licensed under the Apache License, Version 2.0
import { IAiEnvironment } from "./config/env.ts";
import { AiTool } from "./tools/tool.ts";
export type ToolMap = Map<string, AiTool>;
export type ToolSet = Set<AiTool>;
/**
* 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<string, AiTool>();
private modeSets: Map<string, ToolSet> = new Map<string, Set<AiTool>>();
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<AiTool>();
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);
}
}

View File

@ -0,0 +1,273 @@
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
// 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<string> {
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<ISearchResult[]> {
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",
});
}
}

View File

@ -0,0 +1,50 @@
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
// 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");
}

View File

@ -0,0 +1,35 @@
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
// 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<string>;
}

View File

@ -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;

View File

@ -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: {}