/ tools / RemoteTriggerTool / RemoteTriggerTool.ts
RemoteTriggerTool.ts
  1  import axios from 'axios'
  2  import { z } from 'zod/v4'
  3  import { getOauthConfig } from '../../constants/oauth.js'
  4  import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
  5  import { getOrganizationUUID } from '../../services/oauth/client.js'
  6  import { isPolicyAllowed } from '../../services/policyLimits/index.js'
  7  import type { ToolUseContext } from '../../Tool.js'
  8  import { buildTool, type ToolDef } from '../../Tool.js'
  9  import {
 10    checkAndRefreshOAuthTokenIfNeeded,
 11    getClaudeAIOAuthTokens,
 12  } from '../../utils/auth.js'
 13  import { lazySchema } from '../../utils/lazySchema.js'
 14  import { jsonStringify } from '../../utils/slowOperations.js'
 15  import { DESCRIPTION, PROMPT, REMOTE_TRIGGER_TOOL_NAME } from './prompt.js'
 16  import { renderToolResultMessage, renderToolUseMessage } from './UI.js'
 17  
 18  const inputSchema = lazySchema(() =>
 19    z.strictObject({
 20      action: z.enum(['list', 'get', 'create', 'update', 'run']),
 21      trigger_id: z
 22        .string()
 23        .regex(/^[\w-]+$/)
 24        .optional()
 25        .describe('Required for get, update, and run'),
 26      body: z
 27        .record(z.string(), z.unknown())
 28        .optional()
 29        .describe('JSON body for create and update'),
 30    }),
 31  )
 32  type InputSchema = ReturnType<typeof inputSchema>
 33  export type Input = z.infer<InputSchema>
 34  
 35  const outputSchema = lazySchema(() =>
 36    z.object({
 37      status: z.number(),
 38      json: z.string(),
 39    }),
 40  )
 41  type OutputSchema = ReturnType<typeof outputSchema>
 42  export type Output = z.infer<OutputSchema>
 43  
 44  const TRIGGERS_BETA = 'ccr-triggers-2026-01-30'
 45  
 46  export const RemoteTriggerTool = buildTool({
 47    name: REMOTE_TRIGGER_TOOL_NAME,
 48    searchHint: 'manage scheduled remote agent triggers',
 49    maxResultSizeChars: 100_000,
 50    shouldDefer: true,
 51    get inputSchema(): InputSchema {
 52      return inputSchema()
 53    },
 54    get outputSchema(): OutputSchema {
 55      return outputSchema()
 56    },
 57    isEnabled() {
 58      return (
 59        getFeatureValue_CACHED_MAY_BE_STALE('tengu_surreal_dali', false) &&
 60        isPolicyAllowed('allow_remote_sessions')
 61      )
 62    },
 63    isConcurrencySafe() {
 64      return true
 65    },
 66    isReadOnly(input: Input) {
 67      return input.action === 'list' || input.action === 'get'
 68    },
 69    toAutoClassifierInput(input: Input) {
 70      return `RemoteTrigger ${input.action}${input.trigger_id ? ` ${input.trigger_id}` : ''}`
 71    },
 72    async description() {
 73      return DESCRIPTION
 74    },
 75    async prompt() {
 76      return PROMPT
 77    },
 78    async call(input: Input, context: ToolUseContext) {
 79      await checkAndRefreshOAuthTokenIfNeeded()
 80      const accessToken = getClaudeAIOAuthTokens()?.accessToken
 81      if (!accessToken) {
 82        throw new Error(
 83          'Not authenticated with a claude.ai account. Run /login and try again.',
 84        )
 85      }
 86      const orgUUID = await getOrganizationUUID()
 87      if (!orgUUID) {
 88        throw new Error('Unable to resolve organization UUID.')
 89      }
 90  
 91      const base = `${getOauthConfig().BASE_API_URL}/v1/code/triggers`
 92      const headers = {
 93        Authorization: `Bearer ${accessToken}`,
 94        'Content-Type': 'application/json',
 95        'anthropic-version': '2023-06-01',
 96        'anthropic-beta': TRIGGERS_BETA,
 97        'x-organization-uuid': orgUUID,
 98      }
 99  
100      const { action, trigger_id, body } = input
101      let method: 'GET' | 'POST'
102      let url: string
103      let data: unknown
104      switch (action) {
105        case 'list':
106          method = 'GET'
107          url = base
108          break
109        case 'get':
110          if (!trigger_id) throw new Error('get requires trigger_id')
111          method = 'GET'
112          url = `${base}/${trigger_id}`
113          break
114        case 'create':
115          if (!body) throw new Error('create requires body')
116          method = 'POST'
117          url = base
118          data = body
119          break
120        case 'update':
121          if (!trigger_id) throw new Error('update requires trigger_id')
122          if (!body) throw new Error('update requires body')
123          method = 'POST'
124          url = `${base}/${trigger_id}`
125          data = body
126          break
127        case 'run':
128          if (!trigger_id) throw new Error('run requires trigger_id')
129          method = 'POST'
130          url = `${base}/${trigger_id}/run`
131          data = {}
132          break
133      }
134  
135      const res = await axios.request({
136        method,
137        url,
138        headers,
139        data,
140        timeout: 20_000,
141        signal: context.abortController.signal,
142        validateStatus: () => true,
143      })
144  
145      return {
146        data: {
147          status: res.status,
148          json: jsonStringify(res.data),
149        },
150      }
151    },
152    mapToolResultToToolResultBlockParam(output, toolUseID) {
153      return {
154        tool_use_id: toolUseID,
155        type: 'tool_result',
156        content: `HTTP ${output.status}\n${output.json}`,
157      }
158    },
159    renderToolUseMessage,
160    renderToolResultMessage,
161  } satisfies ToolDef<InputSchema, Output>)