{"version":3,"file":"user-tool-service.mjs","names":[],"sources":["../../../src/services/user-tool-service.ts"],"sourcesContent":["/**\n * User-Defined Tool Service — CRUD for user-created tools with persistence.\n *\n * Users define tools in plain English or structured JSON. The service:\n * 1. Stores tool definitions to disk (~/.openclawnch/user-tools/)\n * 2. Compiles definitions into executable AnyAgentTool-compatible objects\n * 3. Manages lifecycle: create, update, disable, delete, list\n * 4. Provides a registry that the plugin can query for dynamic tool loading\n *\n * Tool types:\n * - api_connector: wraps an HTTP API endpoint as a tool\n * - composed: chains existing tools into a higher-level operation\n * - custom: user-defined logic via natural language description (LLM-interpreted)\n *\n * All user tools run through the SandboxRuntime for safety.\n */\n\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';\nimport { join } from 'node:path';\n\n// ─── Types ──────────────────────────────────────────────────────────────\n\nexport type UserToolType = 'api_connector' | 'composed' | 'custom';\n\nexport interface UserToolParam {\n  name: string;\n  type: 'string' | 'number' | 'boolean';\n  description: string;\n  required: boolean;\n  default?: string | number | boolean;\n}\n\n/** API connector definition: wraps an HTTP endpoint as a tool. */\nexport interface ApiConnectorDef {\n  type: 'api_connector';\n  /** Base URL (e.g. \"https://api.example.com/v1\"). */\n  baseUrl: string;\n  /** HTTP method. */\n  method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';\n  /** URL path template (e.g. \"/users/{{userId}}/balance\"). */\n  path: string;\n  /** Request headers (values can reference secrets: \"$SECRET:my_api_key\"). */\n  headers?: Record<string, string>;\n  /** Body template (for POST/PUT/PATCH). JSON string with {{param}} placeholders. */\n  bodyTemplate?: string;\n  /** How to extract the result from the response. JSONPath-like (e.g. \"data.balance\"). */\n  resultPath?: string;\n  /** Timeout in ms. Default: 15000. */\n  timeoutMs?: number;\n}\n\n/** Composed tool: chains existing tools in sequence. */\nexport interface ComposedToolDef {\n  type: 'composed';\n  /** Steps to execute in order. Each step calls an existing tool. */\n  steps: ComposedStep[];\n}\n\nexport interface ComposedStep {\n  /** Step label for display. */\n  label: string;\n  /** Tool name to call. */\n  tool: string;\n  /** Arguments to pass. Values can reference prior step outputs: \"$step.0.balance\". */\n  args: Record<string, string | number | boolean>;\n  /** If true, stop the chain on failure. Default: true. */\n  stopOnFailure?: boolean;\n}\n\n/** Custom tool: LLM-interpreted behavior from natural language. */\nexport interface CustomToolDef {\n  type: 'custom';\n  /** Natural language description of what the tool does. */\n  behavior: string;\n  /** Which existing tools this custom tool is allowed to invoke. */\n  allowedTools: string[];\n  /** Max number of internal tool calls per execution. Default: 5. */\n  maxCalls?: number;\n}\n\nexport type UserToolDefinition = ApiConnectorDef | ComposedToolDef | CustomToolDef;\n\nexport interface UserTool {\n  /** Unique ID. */\n  id: string;\n  /** Tool name (snake_case, must not conflict with built-in tools). */\n  name: string;\n  /** Human-readable label. */\n  label: string;\n  /** LLM-facing description. */\n  description: string;\n  /** Who created this tool. */\n  createdBy: string;\n  /** Tool parameters the user can pass. */\n  params: UserToolParam[];\n  /** The tool definition (api_connector, composed, or custom). */\n  definition: UserToolDefinition;\n  /** Whether this tool can write (affects readonly gate). */\n  isWrite: boolean;\n  /** Whether this tool is currently enabled. */\n  enabled: boolean;\n  /** Usage count (how many times executed). */\n  usageCount: number;\n  /** Max budget per execution in USD (for sandboxing). Default: 1. */\n  maxBudgetUsd: number;\n  /** Tags for organization. */\n  tags: string[];\n  createdAt: number;\n  updatedAt: number;\n}\n\n// ─── Reserved Names (cannot conflict with built-in tools) ───────────────\n\nconst RESERVED_PREFIXES = [\n  'defi_', 'clawnch', 'bankr_', 'compound_', 'agent_', 'skill_', 'session_',\n  'fiat_', 'market_', 'herd_', 'crypto_', 'watch_', 'manage_',\n];\n\nconst RESERVED_NAMES = new Set([\n  'transfer', 'bridge', 'permit2', 'cost_basis', 'analytics', 'block_explorer',\n  'liquidity', 'wayfinder', 'molten', 'hummingbot', 'privacy', 'browser',\n  'governance', 'farcaster', 'safe', 'airdrop', 'nft', 'yield', 'approvals',\n]);\n\nfunction isNameReserved(name: string): boolean {\n  if (RESERVED_NAMES.has(name)) return true;\n  for (const prefix of RESERVED_PREFIXES) {\n    if (name.startsWith(prefix)) return true;\n  }\n  return false;\n}\n\nfunction isValidToolName(name: string): boolean {\n  return /^[a-z][a-z0-9_]{2,40}$/.test(name);\n}\n\n// ─── Service ────────────────────────────────────────────────────────────\n\nexport class UserToolService {\n  private tools = new Map<string, UserTool>();\n  private stateDir: string;\n\n  constructor(opts?: { stateDir?: string }) {\n    this.stateDir = opts?.stateDir ?? join(\n      process.env.HOME ?? '', '.openclawnch', 'user-tools'\n    );\n    this.loadState();\n  }\n\n  /** Create a new user-defined tool. */\n  create(params: {\n    name: string;\n    label: string;\n    description: string;\n    createdBy: string;\n    params: UserToolParam[];\n    definition: UserToolDefinition;\n    isWrite?: boolean;\n    maxBudgetUsd?: number;\n    tags?: string[];\n  }): UserTool {\n    // Validate name\n    if (!isValidToolName(params.name)) {\n      throw new UserToolError(\n        `Invalid tool name \"${params.name}\". Must be 3-40 chars, lowercase alphanumeric + underscores, starting with a letter.`\n      );\n    }\n    if (isNameReserved(params.name)) {\n      throw new UserToolError(\n        `Tool name \"${params.name}\" conflicts with a built-in tool or reserved prefix.`\n      );\n    }\n    if (this.getByName(params.name)) {\n      throw new UserToolError(`A user tool named \"${params.name}\" already exists.`);\n    }\n\n    // Validate definition\n    this.validateDefinition(params.definition);\n\n    const id = `ut_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;\n    const now = Date.now();\n\n    const tool: UserTool = {\n      id,\n      name: params.name,\n      label: params.label,\n      description: params.description,\n      createdBy: params.createdBy,\n      params: params.params,\n      definition: params.definition,\n      isWrite: params.isWrite ?? false,\n      enabled: true,\n      usageCount: 0,\n      maxBudgetUsd: params.maxBudgetUsd ?? 1,\n      tags: params.tags ?? [],\n      createdAt: now,\n      updatedAt: now,\n    };\n\n    this.tools.set(id, tool);\n    this.saveState();\n    return tool;\n  }\n\n  /** Update an existing user tool. */\n  update(id: string, updates: Partial<Pick<UserTool, 'label' | 'description' | 'params' | 'definition' | 'isWrite' | 'enabled' | 'maxBudgetUsd' | 'tags'>>): UserTool | null {\n    const tool = this.tools.get(id);\n    if (!tool) return null;\n\n    if (updates.definition) this.validateDefinition(updates.definition);\n\n    Object.assign(tool, updates, { updatedAt: Date.now() });\n    this.saveState();\n    return tool;\n  }\n\n  /** Delete a user tool. */\n  delete(id: string): boolean {\n    const existed = this.tools.delete(id);\n    if (existed) this.saveState();\n    return existed;\n  }\n\n  /** Get a user tool by ID. */\n  get(id: string): UserTool | null {\n    return this.tools.get(id) ?? null;\n  }\n\n  /** Get a user tool by name. */\n  getByName(name: string): UserTool | null {\n    for (const t of this.tools.values()) {\n      if (t.name === name) return t;\n    }\n    return null;\n  }\n\n  /** List all user tools, optionally filtered. */\n  list(opts?: { createdBy?: string; enabled?: boolean; type?: UserToolType }): UserTool[] {\n    let all = Array.from(this.tools.values());\n    if (opts?.createdBy) all = all.filter(t => t.createdBy === opts.createdBy);\n    if (opts?.enabled !== undefined) all = all.filter(t => t.enabled === opts.enabled);\n    if (opts?.type) all = all.filter(t => t.definition.type === opts.type);\n    return all.sort((a, b) => b.updatedAt - a.updatedAt);\n  }\n\n  /** Get all enabled user tools (for runtime registration). */\n  getEnabledTools(): UserTool[] {\n    return Array.from(this.tools.values()).filter(t => t.enabled);\n  }\n\n  /** Increment usage count after a tool execution. */\n  recordUsage(id: string): void {\n    const tool = this.tools.get(id);\n    if (tool) {\n      tool.usageCount += 1;\n      tool.updatedAt = Date.now();\n      this.saveState();\n    }\n  }\n\n  /** Check if a name is available for a new tool. */\n  isNameAvailable(name: string): { available: boolean; reason?: string } {\n    if (!isValidToolName(name)) {\n      return { available: false, reason: 'Invalid format. Must be 3-40 chars, lowercase alphanumeric + underscores.' };\n    }\n    if (isNameReserved(name)) {\n      return { available: false, reason: 'Conflicts with a built-in tool or reserved prefix.' };\n    }\n    if (this.getByName(name)) {\n      return { available: false, reason: 'A user tool with this name already exists.' };\n    }\n    return { available: true };\n  }\n\n  /** Clear all state (for testing). */\n  clear(): void {\n    this.tools.clear();\n  }\n\n  // ── Validation ──────────────────────────────────────────────────────\n\n  private validateDefinition(def: UserToolDefinition): void {\n    switch (def.type) {\n      case 'api_connector':\n        if (!def.baseUrl) throw new UserToolError('API connector requires a baseUrl.');\n        if (!def.path) throw new UserToolError('API connector requires a path.');\n        if (!def.method) throw new UserToolError('API connector requires a method.');\n        try { new URL(def.baseUrl); } catch {\n          throw new UserToolError(`Invalid baseUrl: \"${def.baseUrl}\".`);\n        }\n        break;\n\n      case 'composed':\n        if (!def.steps || def.steps.length === 0) {\n          throw new UserToolError('Composed tool requires at least one step.');\n        }\n        if (def.steps.length > 10) {\n          throw new UserToolError('Composed tool cannot have more than 10 steps.');\n        }\n        for (const step of def.steps) {\n          if (!step.tool) throw new UserToolError(`Step \"${step.label}\" requires a tool name.`);\n        }\n        break;\n\n      case 'custom':\n        if (!def.behavior || def.behavior.length < 10) {\n          throw new UserToolError('Custom tool requires a behavior description (at least 10 chars).');\n        }\n        if (!def.allowedTools || def.allowedTools.length === 0) {\n          throw new UserToolError('Custom tool requires at least one allowed tool.');\n        }\n        if ((def.maxCalls ?? 5) > 20) {\n          throw new UserToolError('Custom tool maxCalls cannot exceed 20.');\n        }\n        break;\n\n      default:\n        throw new UserToolError(`Unknown tool type: ${(def as any).type}`);\n    }\n  }\n\n  // ── Persistence ─────────────────────────────────────────────────────\n\n  private loadState(): void {\n    try {\n      const filePath = join(this.stateDir, 'tools.json');\n      if (existsSync(filePath)) {\n        const data = JSON.parse(readFileSync(filePath, 'utf8'));\n        for (const t of data) {\n          this.tools.set(t.id, t);\n        }\n      }\n    } catch { /* fresh start */ }\n  }\n\n  private saveState(): void {\n    try {\n      if (!existsSync(this.stateDir)) mkdirSync(this.stateDir, { recursive: true });\n      const filePath = join(this.stateDir, 'tools.json');\n      writeFileSync(filePath, JSON.stringify(Array.from(this.tools.values()), null, 2), 'utf8');\n    } catch { /* best effort */ }\n  }\n}\n\n// ─── Error Class ────────────────────────────────────────────────────────\n\nexport class UserToolError extends Error {\n  constructor(message: string) {\n    super(message);\n    this.name = 'UserToolError';\n  }\n}\n\n// ─── Singleton ──────────────────────────────────────────────────────────\n\nlet instance: UserToolService | null = null;\n\nexport function getUserToolService(opts?: { stateDir?: string }): UserToolService {\n  if (!instance) {\n    instance = new UserToolService(opts);\n  }\n  return instance;\n}\n\nexport function resetUserToolService(): void {\n  instance?.clear();\n  instance = null;\n}\n"],"mappings":"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"}