feat(tools): google slides tool, terminal console virtualization, tool fixes (#2209)

* feat: google slides tool

* fix oauth for slides, add remaining endpoints, update docs

* optimize json dump viewer using react window

* change slides to use google drive credentials

* fix some tools

* ack PR comments

---------

Co-authored-by: waleed <walif6@gmail.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
This commit is contained in:
Emir Karabeg
2025-12-06 12:16:21 -08:00
committed by GitHub
parent a50edf8131
commit 0b28128f25
55 changed files with 3396 additions and 201 deletions

View File

@@ -1084,6 +1084,27 @@ export function GoogleDocsIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function GoogleSlidesIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 48 48'
width='96px'
height='96px'
>
<path
fill='#FFC107'
d='M37,45H11c-1.657,0-3-1.343-3-3V6c0-1.657,1.343-3,3-3h19l10,10v29C40,43.657,38.657,45,37,45z'
/>
<path fill='#FFECB3' d='M40 13L30 13 30 3z' />
<path fill='#FFA000' d='M30 13L40 23 40 13z' />
<path fill='#FFF8E1' d='M14 21H34V35H14z' />
<path fill='#FFA000' d='M16 23H32V26H16zM16 28H28V30H16z' />
</svg>
)
}
export function GoogleCalendarIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg

View File

@@ -34,6 +34,7 @@ import {
GoogleFormsIcon,
GoogleIcon,
GoogleSheetsIcon,
GoogleSlidesIcon,
GoogleVaultIcon,
GrafanaIcon,
HubspotIcon,
@@ -192,6 +193,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
hubspot: HubspotIcon,
grafana: GrafanaIcon,
google_vault: GoogleVaultIcon,
google_slides: GoogleSlidesIcon,
google_sheets: GoogleSheetsIcon,
google_forms: GoogleFormsIcon,
google_drive: GoogleDriveIcon,

View File

@@ -149,6 +149,7 @@ Get the top pages of a target domain sorted by organic traffic. Returns page URL
| `date` | string | No | Date for historical data in YYYY-MM-DD format \(defaults to today\) |
| `limit` | number | No | Maximum number of results to return \(default: 100\) |
| `offset` | number | No | Number of results to skip for pagination |
| `select` | string | No | Comma-separated list of fields to return \(e.g., url,traffic,keywords,top_keyword,value\). Default: url,traffic,keywords,top_keyword,value |
| `apiKey` | string | Yes | Ahrefs API Key |
#### Output

View File

@@ -0,0 +1,185 @@
---
title: Google Slides
description: Read, write, and create presentations
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="google_slides"
color="#E0E0E0"
/>
{/* MANUAL-CONTENT-START:intro */}
[Google Slides](https://slides.google.com) is a dynamic cloud-based presentation application that allows users to create, edit, collaborate on, and present slideshows in real-time. As part of Google's productivity suite, Google Slides offers a flexible platform for designing engaging presentations, collaborating with others, and sharing content seamlessly through the cloud.
Learn how to integrate the Google Slides tools in Sim to effortlessly manage presentations as part of your automated workflows. With Sim, you can read, write, create, and update Google Slides presentations directly through your agents and automated processes, making it easy to deliver up-to-date information, generate custom reports, or produce branded decks programmatically.
With Google Slides, you can:
- **Create and edit presentations**: Design visually appealing slides with themes, layouts, and multimedia content
- **Collaborate in real-time**: Work simultaneously with teammates, comment, assign tasks, and receive live feedback on presentations
- **Present anywhere**: Display presentations online or offline, share links, or publish to the web
- **Add images and rich content**: Insert images, graphics, charts, and videos to make your presentations engaging
- **Integrate with other services**: Connect seamlessly with Google Drive, Docs, Sheets, and other third-party tools
- **Access from any device**: Use Google Slides on desktops, laptops, tablets, and mobile devices for maximum flexibility
In Sim, the Google Slides integration enables your agents to interact directly with presentation files programmatically. Automate tasks like reading slide content, inserting new slides or images, replacing text throughout a deck, generating new presentations, and retrieving slide thumbnails. This empowers you to scale content creation, keep presentations up-to-date, and embed them into automated document workflows. By connecting Sim with Google Slides, you facilitate AI-driven presentation management—making it easy to generate, update, or extract information from presentations without manual effort.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Integrate Google Slides into the workflow. Can read, write, create presentations, replace text, add slides, add images, and get thumbnails.
## Tools
### `google_slides_read`
Read content from a Google Slides presentation
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `presentationId` | string | Yes | The ID of the presentation to read |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `slides` | json | Array of slides with their content |
| `metadata` | json | Presentation metadata including ID, title, and URL |
### `google_slides_write`
Write or update content in a Google Slides presentation
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `presentationId` | string | Yes | The ID of the presentation to write to |
| `content` | string | Yes | The content to write to the slide |
| `slideIndex` | number | No | The index of the slide to write to \(defaults to first slide\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `updatedContent` | boolean | Indicates if presentation content was updated successfully |
| `metadata` | json | Updated presentation metadata including ID, title, and URL |
### `google_slides_create`
Create a new Google Slides presentation
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `title` | string | Yes | The title of the presentation to create |
| `content` | string | No | The content to add to the first slide |
| `folderSelector` | string | No | Select the folder to create the presentation in |
| `folderId` | string | No | The ID of the folder to create the presentation in \(internal use\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `metadata` | json | Created presentation metadata including ID, title, and URL |
### `google_slides_replace_all_text`
Find and replace all occurrences of text throughout a Google Slides presentation
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `presentationId` | string | Yes | The ID of the presentation |
| `findText` | string | Yes | The text to find \(e.g., \{\{placeholder\}\}\) |
| `replaceText` | string | Yes | The text to replace with |
| `matchCase` | boolean | No | Whether the search should be case-sensitive \(default: true\) |
| `pageObjectIds` | string | No | Comma-separated list of slide object IDs to limit replacements to specific slides \(leave empty for all slides\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `occurrencesChanged` | number | Number of text occurrences that were replaced |
| `metadata` | json | Operation metadata including presentation ID and URL |
### `google_slides_add_slide`
Add a new slide to a Google Slides presentation with a specified layout
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `presentationId` | string | Yes | The ID of the presentation |
| `layout` | string | No | The predefined layout for the slide \(BLANK, TITLE, TITLE_AND_BODY, TITLE_ONLY, SECTION_HEADER, etc.\). Defaults to BLANK. |
| `insertionIndex` | number | No | The optional zero-based index indicating where to insert the slide. If not specified, the slide is added at the end. |
| `placeholderIdMappings` | string | No | JSON array of placeholder mappings to assign custom object IDs to placeholders. Format: \[\{"layoutPlaceholder":\{"type":"TITLE"\},"objectId":"custom_title_id"\}\] |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `slideId` | string | The object ID of the newly created slide |
| `metadata` | json | Operation metadata including presentation ID, layout, and URL |
### `google_slides_add_image`
Insert an image into a specific slide in a Google Slides presentation
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `presentationId` | string | Yes | The ID of the presentation |
| `pageObjectId` | string | Yes | The object ID of the slide/page to add the image to |
| `imageUrl` | string | Yes | The publicly accessible URL of the image \(must be PNG, JPEG, or GIF, max 50MB\) |
| `width` | number | No | Width of the image in points \(default: 300\) |
| `height` | number | No | Height of the image in points \(default: 200\) |
| `positionX` | number | No | X position from the left edge in points \(default: 100\) |
| `positionY` | number | No | Y position from the top edge in points \(default: 100\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `imageId` | string | The object ID of the newly created image |
| `metadata` | json | Operation metadata including presentation ID and image URL |
### `google_slides_get_thumbnail`
Generate a thumbnail image of a specific slide in a Google Slides presentation
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `presentationId` | string | Yes | The ID of the presentation |
| `pageObjectId` | string | Yes | The object ID of the slide/page to get a thumbnail for |
| `thumbnailSize` | string | No | The size of the thumbnail: SMALL \(200px\), MEDIUM \(800px\), or LARGE \(1600px\). Defaults to MEDIUM. |
| `mimeType` | string | No | The MIME type of the thumbnail image: PNG or GIF. Defaults to PNG. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `contentUrl` | string | URL to the thumbnail image \(valid for 30 minutes\) |
| `width` | number | Width of the thumbnail in pixels |
| `height` | number | Height of the thumbnail in pixels |
| `metadata` | json | Operation metadata including presentation ID and page object ID |
## Notes
- Category: `tools`
- Type: `google_slides`

View File

@@ -364,6 +364,8 @@ Update an existing schedule in incident.io
| `id` | string | Yes | The ID of the schedule to update |
| `name` | string | No | New name for the schedule |
| `timezone` | string | No | New timezone for the schedule \(e.g., America/New_York\) |
| `config` | string | No | Schedule configuration as JSON string with rotations. Example: \{"rotations": \[\{"name": "Primary", "users": \[\{"id": "user_id"\}\], "handover_start_at": "2024-01-01T09:00:00Z", "handovers": \[\{"interval": 1, "interval_type": "weekly"\}\]\}\]\} |
| `Example` | string | No | No description |
#### Output

View File

@@ -170,7 +170,7 @@ Create or update a company in Intercom
| `plan` | string | No | The company plan name |
| `size` | number | No | The number of employees in the company |
| `industry` | string | No | The industry the company operates in |
| `monthly_spend` | number | No | How much revenue the company generates for your business |
| `monthly_spend` | number | No | How much revenue the company generates for your business. Note: This field truncates floats to whole integers \(e.g., 155.98 becomes 155\) |
| `custom_attributes` | string | No | Custom attributes as JSON object |
#### Output
@@ -199,7 +199,7 @@ Retrieve a single company by ID from Intercom
### `intercom_list_companies`
List all companies from Intercom with pagination support
List all companies from Intercom with pagination support. Note: This endpoint has a limit of 10,000 companies that can be returned using pagination. For datasets larger than 10,000 companies, use the Scroll API instead.
#### Input
@@ -262,7 +262,7 @@ Reply to a conversation as an admin in Intercom
| `conversationId` | string | Yes | Conversation ID to reply to |
| `message_type` | string | Yes | Message type: "comment" or "note" |
| `body` | string | Yes | The text body of the reply |
| `admin_id` | string | Yes | The ID of the admin authoring the reply |
| `admin_id` | string | No | The ID of the admin authoring the reply. If not provided, a default admin \(Operator/Fin\) will be used. |
| `attachment_urls` | string | No | Comma-separated list of image URLs \(max 10\) |
#### Output

View File

@@ -1,6 +1,6 @@
---
title: Kalshi
description: Access prediction markets data from Kalshi
description: Access prediction markets and trade on Kalshi
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
@@ -28,7 +28,7 @@ By using these unified tools and endpoints, you can seamlessly incorporate Kalsh
## Usage Instructions
Integrate Kalshi prediction markets into the workflow. Can get markets, market, events, event, balance, positions, orders, orderbook, trades, candlesticks, fills, series, and exchange status.
Integrate Kalshi prediction markets into the workflow. Can get markets, market, events, event, balance, positions, orders, orderbook, trades, candlesticks, fills, series, exchange status, and place/cancel/amend trades.
@@ -175,16 +175,34 @@ Retrieve your orders from Kalshi with optional filtering
| `success` | boolean | Operation success status |
| `output` | object | Orders data and metadata |
### `kalshi_get_order`
Retrieve details of a specific order by ID from Kalshi
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `keyId` | string | Yes | Your Kalshi API Key ID |
| `privateKey` | string | Yes | Your RSA Private Key \(PEM format\) |
| `orderId` | string | Yes | The order ID to retrieve |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Order data |
### `kalshi_get_orderbook`
Retrieve the orderbook (bids and asks) for a specific market
Retrieve the orderbook (yes and no bids) for a specific market
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `ticker` | string | Yes | Market ticker \(e.g., KXBTC-24DEC31\) |
| `depth` | number | No | Number of price levels to return per side |
#### Output
@@ -195,15 +213,12 @@ Retrieve the orderbook (bids and asks) for a specific market
### `kalshi_get_trades`
Retrieve recent trades across all markets or for a specific market
Retrieve recent trades across all markets
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `ticker` | string | No | Filter by market ticker |
| `minTs` | number | No | Minimum timestamp \(Unix milliseconds\) |
| `maxTs` | number | No | Maximum timestamp \(Unix milliseconds\) |
| `limit` | string | No | Number of results \(1-1000, default: 100\) |
| `cursor` | string | No | Pagination cursor for next page |
@@ -224,9 +239,9 @@ Retrieve OHLC candlestick data for a specific market
| --------- | ---- | -------- | ----------- |
| `seriesTicker` | string | Yes | Series ticker |
| `ticker` | string | Yes | Market ticker \(e.g., KXBTC-24DEC31\) |
| `startTs` | number | No | Start timestamp \(Unix milliseconds\) |
| `endTs` | number | No | End timestamp \(Unix milliseconds\) |
| `periodInterval` | number | No | Period interval: 1 \(1min\), 60 \(1hour\), or 1440 \(1day\) |
| `startTs` | number | Yes | Start timestamp \(Unix seconds\) |
| `endTs` | number | Yes | End timestamp \(Unix seconds\) |
| `periodInterval` | number | Yes | Period interval: 1 \(1min\), 60 \(1hour\), or 1440 \(1day\) |
#### Output
@@ -292,6 +307,89 @@ Retrieve the current status of the Kalshi exchange (trading and exchange activit
| `success` | boolean | Operation success status |
| `output` | object | Exchange status data and metadata |
### `kalshi_create_order`
Create a new order on a Kalshi prediction market
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `keyId` | string | Yes | Your Kalshi API Key ID |
| `privateKey` | string | Yes | Your RSA Private Key \(PEM format\) |
| `ticker` | string | Yes | Market ticker \(e.g., KXBTC-24DEC31\) |
| `side` | string | Yes | Side of the order: 'yes' or 'no' |
| `action` | string | Yes | Action type: 'buy' or 'sell' |
| `count` | string | Yes | Number of contracts \(minimum 1\) |
| `type` | string | No | Order type: 'limit' or 'market' \(default: limit\) |
| `yesPrice` | string | No | Yes price in cents \(1-99\) |
| `noPrice` | string | No | No price in cents \(1-99\) |
| `yesPriceDollars` | string | No | Yes price in dollars \(e.g., "0.56"\) |
| `noPriceDollars` | string | No | No price in dollars \(e.g., "0.56"\) |
| `clientOrderId` | string | No | Custom order identifier |
| `expirationTs` | string | No | Unix timestamp for order expiration |
| `timeInForce` | string | No | Time in force: 'fill_or_kill', 'good_till_canceled', 'immediate_or_cancel' |
| `buyMaxCost` | string | No | Maximum cost in cents \(auto-enables fill_or_kill\) |
| `postOnly` | string | No | Set to 'true' for maker-only orders |
| `reduceOnly` | string | No | Set to 'true' for position reduction only |
| `selfTradePreventionType` | string | No | Self-trade prevention: 'taker_at_cross' or 'maker' |
| `orderGroupId` | string | No | Associated order group ID |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Created order data |
### `kalshi_cancel_order`
Cancel an existing order on Kalshi
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `keyId` | string | Yes | Your Kalshi API Key ID |
| `privateKey` | string | Yes | Your RSA Private Key \(PEM format\) |
| `orderId` | string | Yes | The order ID to cancel |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Canceled order data |
### `kalshi_amend_order`
Modify the price or quantity of an existing order on Kalshi
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `keyId` | string | Yes | Your Kalshi API Key ID |
| `privateKey` | string | Yes | Your RSA Private Key \(PEM format\) |
| `orderId` | string | Yes | The order ID to amend |
| `ticker` | string | Yes | Market ticker |
| `side` | string | Yes | Side of the order: 'yes' or 'no' |
| `action` | string | Yes | Action type: 'buy' or 'sell' |
| `clientOrderId` | string | Yes | The original client-specified order ID |
| `updatedClientOrderId` | string | Yes | The new client-specified order ID after amendment |
| `count` | string | No | Updated quantity for the order |
| `yesPrice` | string | No | Updated yes price in cents \(1-99\) |
| `noPrice` | string | No | Updated no price in cents \(1-99\) |
| `yesPriceDollars` | string | No | Updated yes price in dollars \(e.g., "0.56"\) |
| `noPriceDollars` | string | No | Updated no price in dollars \(e.g., "0.56"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Amended order data |
## Notes

View File

@@ -29,6 +29,7 @@
"google_forms",
"google_search",
"google_sheets",
"google_slides",
"google_vault",
"grafana",
"hubspot",

View File

@@ -44,7 +44,7 @@ Retrieve a list of prediction markets from Polymarket with optional filtering
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `closed` | string | No | Filter by closed status \(true/false\). Use false for active markets only. |
| `order` | string | No | Sort field \(e.g., id, volume, liquidity\) |
| `order` | string | No | Sort field \(e.g., volumeNum, liquidityNum, startDate, endDate, createdAt\) |
| `ascending` | string | No | Sort direction \(true for ascending, false for descending\) |
| `tagId` | string | No | Filter by tag ID |
| `limit` | string | No | Number of results per page \(recommended: 25-50\) |
@@ -84,7 +84,7 @@ Retrieve a list of events from Polymarket with optional filtering
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `closed` | string | No | Filter by closed status \(true/false\). Use false for active events only. |
| `order` | string | No | Sort field \(e.g., id, volume\) |
| `order` | string | No | Sort field \(e.g., volume, liquidity, startDate, endDate, createdAt\) |
| `ascending` | string | No | Sort direction \(true for ascending, false for descending\) |
| `tagId` | string | No | Filter by tag ID |
| `limit` | string | No | Number of results per page \(recommended: 25-50\) |

View File

@@ -1,40 +1,21 @@
import { useCallback, useEffect, useState } from 'react'
import { useTerminalStore } from '@/stores/terminal'
/**
* Constants for output panel sizing
* Must match MIN_OUTPUT_PANEL_WIDTH_PX and BLOCK_COLUMN_WIDTH_PX in terminal.tsx
*/
const MIN_WIDTH = 440
const BLOCK_COLUMN_WIDTH = 240
/**
* Custom hook to handle output panel horizontal resize functionality.
* Manages mouse events for resizing and enforces min/max width constraints.
*
* @returns Resize state and handlers
*/
export function useOutputPanelResize() {
const { setOutputPanelWidth } = useTerminalStore()
const setOutputPanelWidth = useTerminalStore((state) => state.setOutputPanelWidth)
const [isResizing, setIsResizing] = useState(false)
/**
* Handles mouse down on resize handle
*/
const handleMouseDown = useCallback(() => {
setIsResizing(true)
}, [])
/**
* Setup resize event listeners and body styles when resizing.
* Cleanup is handled automatically by the effect's return function.
*/
useEffect(() => {
if (!isResizing) return
const handleMouseMove = (e: MouseEvent) => {
// Calculate width from the right edge of the viewport
// Account for panel width on the right side
const panelWidth = Number.parseInt(
getComputedStyle(document.documentElement).getPropertyValue('--panel-width') || '0'
)
@@ -43,13 +24,10 @@ export function useOutputPanelResize() {
)
const newWidth = window.innerWidth - e.clientX - panelWidth
// Calculate max width: total terminal width minus block column width
const terminalWidth = window.innerWidth - sidebarWidth - panelWidth
const maxWidth = terminalWidth - BLOCK_COLUMN_WIDTH
// Clamp between min and max width
const clampedWidth = Math.max(MIN_WIDTH, Math.min(newWidth, maxWidth))
setOutputPanelWidth(clampedWidth)
}

View File

@@ -1,38 +1,22 @@
import { useCallback, useEffect } from 'react'
import { useTerminalStore } from '@/stores/terminal'
/**
* Constants for terminal sizing
*/
const MIN_HEIGHT = 30
const MAX_HEIGHT_PERCENTAGE = 0.7 // 70% of viewport height
const MAX_HEIGHT_PERCENTAGE = 0.7
/**
* Custom hook to handle terminal resize functionality.
* Manages mouse events for resizing and enforces min/max height constraints.
* Maximum height is capped at 70% of the viewport height for optimal layout.
*
* @returns Resize state and handlers
*/
export function useTerminalResize() {
const { setTerminalHeight, isResizing, setIsResizing } = useTerminalStore()
const setTerminalHeight = useTerminalStore((state) => state.setTerminalHeight)
const isResizing = useTerminalStore((state) => state.isResizing)
const setIsResizing = useTerminalStore((state) => state.setIsResizing)
/**
* Handles mouse down on resize handle
*/
const handleMouseDown = useCallback(() => {
setIsResizing(true)
}, [setIsResizing])
/**
* Setup resize event listeners and body styles when resizing
* Cleanup is handled automatically by the effect's return function
*/
useEffect(() => {
if (!isResizing) return
const handleMouseMove = (e: MouseEvent) => {
// Calculate height from the bottom edge of the viewport
const newHeight = window.innerHeight - e.clientY
const maxHeight = window.innerHeight * MAX_HEIGHT_PERCENTAGE

View File

@@ -1,6 +1,6 @@
'use client'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import clsx from 'clsx'
import {
ArrowDown,
@@ -18,9 +18,9 @@ import {
Trash2,
X,
} from 'lucide-react'
import { useShallow } from 'zustand/react/shallow'
import {
Button,
Code,
Input,
Popover,
PopoverContent,
@@ -28,6 +28,7 @@ import {
PopoverScrollArea,
PopoverTrigger,
Tooltip,
VirtualizedCodeViewer,
} from '@/components/emcn'
import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
import { createCommands } from '@/app/workspace/[workspaceId]/utils/commands-utils'
@@ -237,6 +238,42 @@ const isEventFromEditableElement = (e: KeyboardEvent): boolean => {
return false
}
interface OutputCodeContentProps {
code: string
language: 'javascript' | 'json'
wrapText: boolean
searchQuery: string | undefined
currentMatchIndex: number
onMatchCountChange: (count: number) => void
contentRef: React.RefObject<HTMLDivElement | null>
}
const OutputCodeContent = React.memo(function OutputCodeContent({
code,
language,
wrapText,
searchQuery,
currentMatchIndex,
onMatchCountChange,
contentRef,
}: OutputCodeContentProps) {
return (
<VirtualizedCodeViewer
code={code}
showGutter
language={language}
className='m-0 min-h-full rounded-none border-0 bg-[var(--surface-1)]'
paddingLeft={8}
gutterStyle={{ backgroundColor: 'transparent' }}
wrapText={wrapText}
searchQuery={searchQuery}
currentMatchIndex={currentMatchIndex}
onMatchCountChange={onMatchCountChange}
contentRef={contentRef}
/>
)
})
/**
* Terminal component with resizable height that persists across page refreshes.
*
@@ -254,7 +291,6 @@ export function Terminal() {
const prevEntriesLengthRef = useRef(0)
const prevWorkflowEntriesLengthRef = useRef(0)
const {
terminalHeight,
setTerminalHeight,
lastExpandedHeight,
outputPanelWidth,
@@ -263,10 +299,16 @@ export function Terminal() {
setOpenOnRun,
setHasHydrated,
} = useTerminalStore()
const entries = useTerminalConsoleStore((state) => state.entries)
const isExpanded = useTerminalStore((state) => state.terminalHeight > NEAR_MIN_THRESHOLD)
const { activeWorkflowId } = useWorkflowRegistry()
const workflowEntriesSelector = useCallback(
(state: { entries: ConsoleEntry[] }) =>
state.entries.filter((entry) => entry.workflowId === activeWorkflowId),
[activeWorkflowId]
)
const entries = useTerminalConsoleStore(useShallow(workflowEntriesSelector))
const clearWorkflowConsole = useTerminalConsoleStore((state) => state.clearWorkflowConsole)
const exportConsoleCSV = useTerminalConsoleStore((state) => state.exportConsoleCSV)
const { activeWorkflowId } = useWorkflowRegistry()
const [selectedEntry, setSelectedEntry] = useState<ConsoleEntry | null>(null)
const [isToggling, setIsToggling] = useState(false)
const [wrapText, setWrapText] = useState(true)
@@ -304,8 +346,6 @@ export function Terminal() {
hasActiveFilters,
} = useTerminalFilters()
const isExpanded = terminalHeight > NEAR_MIN_THRESHOLD
/**
* Expands the terminal to its last meaningful height, with safeguards:
* - Never expands below {@link DEFAULT_EXPANDED_HEIGHT}.
@@ -322,13 +362,7 @@ export function Terminal() {
setTerminalHeight(targetHeight)
}, [lastExpandedHeight, setTerminalHeight])
/**
* Get all entries for current workflow (before filtering) for filter options
*/
const allWorkflowEntries = useMemo(() => {
if (!activeWorkflowId) return []
return entries.filter((entry) => entry.workflowId === activeWorkflowId)
}, [entries, activeWorkflowId])
const allWorkflowEntries = entries
/**
* Filter entries for current workflow and apply filters
@@ -425,6 +459,11 @@ export function Terminal() {
return selectedEntry.output
}, [selectedEntry, showInput])
const outputDataStringified = useMemo(() => {
if (outputData === null || outputData === undefined) return ''
return JSON.stringify(outputData, null, 2)
}, [outputData])
/**
* Auto-open the terminal on new entries when "Open on run" is enabled.
* This mirrors the header toggle behavior by using expandToLastHeight,
@@ -439,13 +478,12 @@ export function Terminal() {
const previousLength = prevWorkflowEntriesLengthRef.current
const currentLength = allWorkflowEntries.length
// Only react when new entries are added for the active workflow
if (currentLength > previousLength && terminalHeight <= MIN_HEIGHT) {
if (currentLength > previousLength && !isExpanded) {
expandToLastHeight()
}
prevWorkflowEntriesLengthRef.current = currentLength
}, [allWorkflowEntries.length, expandToLastHeight, openOnRun, terminalHeight])
}, [allWorkflowEntries.length, expandToLastHeight, openOnRun, isExpanded])
/**
* Handle row click - toggle if clicking same entry
@@ -485,13 +523,11 @@ export function Terminal() {
const handleCopy = useCallback(() => {
if (!selectedEntry) return
const textToCopy = shouldShowCodeDisplay
? selectedEntry.input.code
: JSON.stringify(outputData, null, 2)
const textToCopy = shouldShowCodeDisplay ? selectedEntry.input.code : outputDataStringified
navigator.clipboard.writeText(textToCopy)
setShowCopySuccess(true)
}, [selectedEntry, outputData, shouldShowCodeDisplay])
}, [selectedEntry, outputDataStringified, shouldShowCodeDisplay])
/**
* Clears the console for the active workflow.
@@ -542,7 +578,7 @@ export function Terminal() {
}, [matchCount])
/**
* Handles match count change from Code.Viewer.
* Handles match count change from VirtualizedCodeViewer.
*/
const handleMatchCountChange = useCallback((count: number) => {
setMatchCount(count)
@@ -1576,15 +1612,11 @@ export function Terminal() {
{/* Content */}
<div className='flex-1 overflow-x-auto overflow-y-auto'>
{shouldShowCodeDisplay ? (
<Code.Viewer
<OutputCodeContent
code={selectedEntry.input.code}
showGutter
language={
(selectedEntry.input.language as 'javascript' | 'json') || 'javascript'
}
className='m-0 min-h-full rounded-none border-0 bg-[var(--surface-1)]'
paddingLeft={8}
gutterStyle={{ backgroundColor: 'transparent' }}
wrapText={wrapText}
searchQuery={isOutputSearchActive ? outputSearchQuery : undefined}
currentMatchIndex={currentMatchIndex}
@@ -1592,13 +1624,9 @@ export function Terminal() {
contentRef={outputContentRef}
/>
) : (
<Code.Viewer
code={JSON.stringify(outputData, null, 2)}
showGutter
<OutputCodeContent
code={outputDataStringified}
language='json'
className='m-0 min-h-full rounded-none border-0 bg-[var(--surface-1)]'
paddingLeft={8}
gutterStyle={{ backgroundColor: 'transparent' }}
wrapText={wrapText}
searchQuery={isOutputSearchActive ? outputSearchQuery : undefined}
currentMatchIndex={currentMatchIndex}

View File

@@ -0,0 +1,433 @@
import { GoogleSlidesIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { GoogleSlidesResponse } from '@/tools/google_slides/types'
export const GoogleSlidesBlock: BlockConfig<GoogleSlidesResponse> = {
type: 'google_slides',
name: 'Google Slides',
description: 'Read, write, and create presentations',
authMode: AuthMode.OAuth,
longDescription:
'Integrate Google Slides into the workflow. Can read, write, create presentations, replace text, add slides, add images, and get thumbnails.',
docsLink: 'https://docs.sim.ai/tools/google_slides',
category: 'tools',
bgColor: '#E0E0E0',
icon: GoogleSlidesIcon,
subBlocks: [
// Operation selector
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'Read Presentation', id: 'read' },
{ label: 'Write to Presentation', id: 'write' },
{ label: 'Create Presentation', id: 'create' },
{ label: 'Replace All Text', id: 'replace_all_text' },
{ label: 'Add Slide', id: 'add_slide' },
{ label: 'Add Image', id: 'add_image' },
{ label: 'Get Thumbnail', id: 'get_thumbnail' },
],
value: () => 'read',
},
// Google Slides Credentials
{
id: 'credential',
title: 'Google Account',
type: 'oauth-input',
required: true,
serviceId: 'google-drive',
requiredScopes: [
'https://www.googleapis.com/auth/drive.file',
'https://www.googleapis.com/auth/drive',
],
placeholder: 'Select Google account',
},
// Presentation selector (basic mode) - for operations that need an existing presentation
{
id: 'presentationId',
title: 'Select Presentation',
type: 'file-selector',
canonicalParamId: 'presentationId',
serviceId: 'google-drive',
requiredScopes: [],
mimeType: 'application/vnd.google-apps.presentation',
placeholder: 'Select a presentation',
dependsOn: ['credential'],
mode: 'basic',
condition: {
field: 'operation',
value: ['read', 'write', 'replace_all_text', 'add_slide', 'add_image', 'get_thumbnail'],
},
},
// Manual presentation ID input (advanced mode)
{
id: 'manualPresentationId',
title: 'Presentation ID',
type: 'short-input',
canonicalParamId: 'presentationId',
placeholder: 'Enter presentation ID',
dependsOn: ['credential'],
mode: 'advanced',
condition: {
field: 'operation',
value: ['read', 'write', 'replace_all_text', 'add_slide', 'add_image', 'get_thumbnail'],
},
},
// ========== Write Operation Fields ==========
{
id: 'slideIndex',
title: 'Slide Index',
type: 'short-input',
placeholder: 'Enter slide index (0 for first slide)',
condition: { field: 'operation', value: 'write' },
},
{
id: 'content',
title: 'Content',
type: 'long-input',
placeholder: 'Enter slide content',
condition: { field: 'operation', value: 'write' },
required: true,
},
// ========== Create Operation Fields ==========
{
id: 'title',
title: 'Presentation Title',
type: 'short-input',
placeholder: 'Enter title for the new presentation',
condition: { field: 'operation', value: 'create' },
required: true,
},
// Folder selector (basic mode)
{
id: 'folderSelector',
title: 'Select Parent Folder',
type: 'file-selector',
canonicalParamId: 'folderId',
serviceId: 'google-drive',
requiredScopes: [],
mimeType: 'application/vnd.google-apps.folder',
placeholder: 'Select a parent folder',
dependsOn: ['credential'],
mode: 'basic',
condition: { field: 'operation', value: 'create' },
},
// Manual folder ID input (advanced mode)
{
id: 'folderId',
title: 'Parent Folder ID',
type: 'short-input',
canonicalParamId: 'folderId',
placeholder: 'Enter parent folder ID (leave empty for root folder)',
dependsOn: ['credential'],
mode: 'advanced',
condition: { field: 'operation', value: 'create' },
},
// Content Field for create operation
{
id: 'createContent',
title: 'Initial Content',
type: 'long-input',
placeholder: 'Enter initial slide content (optional)',
condition: { field: 'operation', value: 'create' },
},
// ========== Replace All Text Operation Fields ==========
{
id: 'findText',
title: 'Find Text',
type: 'short-input',
placeholder: 'Text to find (e.g., {{placeholder}})',
condition: { field: 'operation', value: 'replace_all_text' },
required: true,
},
{
id: 'replaceText',
title: 'Replace With',
type: 'short-input',
placeholder: 'Text to replace with',
condition: { field: 'operation', value: 'replace_all_text' },
required: true,
},
{
id: 'matchCase',
title: 'Match Case',
type: 'switch',
condition: { field: 'operation', value: 'replace_all_text' },
},
{
id: 'pageObjectIds',
title: 'Limit to Slides (IDs)',
type: 'short-input',
placeholder: 'Comma-separated slide IDs (leave empty for all)',
condition: { field: 'operation', value: 'replace_all_text' },
mode: 'advanced',
},
// ========== Add Slide Operation Fields ==========
{
id: 'layout',
title: 'Slide Layout',
type: 'dropdown',
options: [
{ label: 'Blank', id: 'BLANK' },
{ label: 'Title', id: 'TITLE' },
{ label: 'Title and Body', id: 'TITLE_AND_BODY' },
{ label: 'Title Only', id: 'TITLE_ONLY' },
{ label: 'Title and Two Columns', id: 'TITLE_AND_TWO_COLUMNS' },
{ label: 'Section Header', id: 'SECTION_HEADER' },
{ label: 'Caption Only', id: 'CAPTION_ONLY' },
{ label: 'Main Point', id: 'MAIN_POINT' },
{ label: 'Big Number', id: 'BIG_NUMBER' },
],
condition: { field: 'operation', value: 'add_slide' },
value: () => 'BLANK',
},
{
id: 'insertionIndex',
title: 'Insertion Position',
type: 'short-input',
placeholder: 'Position to insert slide (leave empty for end)',
condition: { field: 'operation', value: 'add_slide' },
},
{
id: 'placeholderIdMappings',
title: 'Placeholder ID Mappings',
type: 'long-input',
placeholder: 'JSON array: [{"layoutPlaceholder":{"type":"TITLE"},"objectId":"my_title"}]',
condition: { field: 'operation', value: 'add_slide' },
mode: 'advanced',
},
// ========== Add Image Operation Fields ==========
{
id: 'pageObjectId',
title: 'Slide ID',
type: 'short-input',
placeholder: 'Object ID of the slide to add image to',
condition: { field: 'operation', value: 'add_image' },
required: true,
},
{
id: 'imageUrl',
title: 'Image URL',
type: 'short-input',
placeholder: 'Public URL of the image (PNG, JPEG, or GIF)',
condition: { field: 'operation', value: 'add_image' },
required: true,
},
{
id: 'imageWidth',
title: 'Width (points)',
type: 'short-input',
placeholder: 'Image width in points (default: 300)',
condition: { field: 'operation', value: 'add_image' },
},
{
id: 'imageHeight',
title: 'Height (points)',
type: 'short-input',
placeholder: 'Image height in points (default: 200)',
condition: { field: 'operation', value: 'add_image' },
},
{
id: 'positionX',
title: 'X Position (points)',
type: 'short-input',
placeholder: 'X position from left (default: 100)',
condition: { field: 'operation', value: 'add_image' },
},
{
id: 'positionY',
title: 'Y Position (points)',
type: 'short-input',
placeholder: 'Y position from top (default: 100)',
condition: { field: 'operation', value: 'add_image' },
},
// ========== Get Thumbnail Operation Fields ==========
{
id: 'thumbnailPageId',
title: 'Slide ID',
type: 'short-input',
placeholder: 'Object ID of the slide to get thumbnail for',
condition: { field: 'operation', value: 'get_thumbnail' },
required: true,
},
{
id: 'thumbnailSize',
title: 'Thumbnail Size',
type: 'dropdown',
options: [
{ label: 'Small (200px)', id: 'SMALL' },
{ label: 'Medium (800px)', id: 'MEDIUM' },
{ label: 'Large (1600px)', id: 'LARGE' },
],
condition: { field: 'operation', value: 'get_thumbnail' },
value: () => 'MEDIUM',
},
{
id: 'mimeType',
title: 'Image Format',
type: 'dropdown',
options: [
{ label: 'PNG', id: 'PNG' },
{ label: 'GIF', id: 'GIF' },
],
condition: { field: 'operation', value: 'get_thumbnail' },
value: () => 'PNG',
},
],
tools: {
access: [
'google_slides_read',
'google_slides_write',
'google_slides_create',
'google_slides_replace_all_text',
'google_slides_add_slide',
'google_slides_add_image',
'google_slides_get_thumbnail',
],
config: {
tool: (params) => {
switch (params.operation) {
case 'read':
return 'google_slides_read'
case 'write':
return 'google_slides_write'
case 'create':
return 'google_slides_create'
case 'replace_all_text':
return 'google_slides_replace_all_text'
case 'add_slide':
return 'google_slides_add_slide'
case 'add_image':
return 'google_slides_add_image'
case 'get_thumbnail':
return 'google_slides_get_thumbnail'
default:
throw new Error(`Invalid Google Slides operation: ${params.operation}`)
}
},
params: (params) => {
const {
credential,
presentationId,
manualPresentationId,
folderSelector,
folderId,
slideIndex,
createContent,
thumbnailPageId,
imageWidth,
imageHeight,
...rest
} = params
const effectivePresentationId = (presentationId || manualPresentationId || '').trim()
const effectiveFolderId = (folderSelector || folderId || '').trim()
const result: Record<string, any> = {
...rest,
presentationId: effectivePresentationId || undefined,
credential,
}
// Handle operation-specific params
if (params.operation === 'write' && slideIndex) {
result.slideIndex = Number.parseInt(slideIndex as string, 10)
}
if (params.operation === 'create') {
result.folderId = effectiveFolderId || undefined
if (createContent) {
result.content = createContent
}
}
if (params.operation === 'add_slide' && params.insertionIndex) {
result.insertionIndex = Number.parseInt(params.insertionIndex as string, 10)
}
if (params.operation === 'add_image') {
if (imageWidth) {
result.width = Number.parseInt(imageWidth as string, 10)
}
if (imageHeight) {
result.height = Number.parseInt(imageHeight as string, 10)
}
if (params.positionX) {
result.positionX = Number.parseInt(params.positionX as string, 10)
}
if (params.positionY) {
result.positionY = Number.parseInt(params.positionY as string, 10)
}
}
if (params.operation === 'get_thumbnail') {
result.pageObjectId = thumbnailPageId
}
return result
},
},
},
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
credential: { type: 'string', description: 'Google Slides access token' },
presentationId: { type: 'string', description: 'Presentation identifier' },
manualPresentationId: { type: 'string', description: 'Manual presentation identifier' },
// Write operation
slideIndex: { type: 'number', description: 'Slide index to write to' },
content: { type: 'string', description: 'Slide content' },
// Create operation
title: { type: 'string', description: 'Presentation title' },
folderSelector: { type: 'string', description: 'Selected folder' },
folderId: { type: 'string', description: 'Folder identifier' },
createContent: { type: 'string', description: 'Initial slide content' },
// Replace all text operation
findText: { type: 'string', description: 'Text to find' },
replaceText: { type: 'string', description: 'Text to replace with' },
matchCase: { type: 'boolean', description: 'Whether to match case' },
pageObjectIds: {
type: 'string',
description: 'Comma-separated slide IDs to limit replacements',
},
// Add slide operation
layout: { type: 'string', description: 'Slide layout' },
insertionIndex: { type: 'number', description: 'Position to insert slide' },
placeholderIdMappings: { type: 'string', description: 'JSON array of placeholder ID mappings' },
// Add image operation
pageObjectId: { type: 'string', description: 'Slide object ID for image' },
imageUrl: { type: 'string', description: 'Image URL' },
imageWidth: { type: 'number', description: 'Image width in points' },
imageHeight: { type: 'number', description: 'Image height in points' },
positionX: { type: 'number', description: 'X position in points' },
positionY: { type: 'number', description: 'Y position in points' },
// Get thumbnail operation
thumbnailPageId: { type: 'string', description: 'Slide object ID for thumbnail' },
thumbnailSize: { type: 'string', description: 'Thumbnail size' },
mimeType: { type: 'string', description: 'Image format (PNG or GIF)' },
},
outputs: {
// Read operation
slides: { type: 'json', description: 'Presentation slides' },
metadata: { type: 'json', description: 'Presentation metadata' },
// Write operation
updatedContent: { type: 'boolean', description: 'Content update status' },
// Replace all text operation
occurrencesChanged: { type: 'number', description: 'Number of text occurrences replaced' },
// Add slide operation
slideId: { type: 'string', description: 'Object ID of newly created slide' },
// Add image operation
imageId: { type: 'string', description: 'Object ID of newly created image' },
// Get thumbnail operation
contentUrl: { type: 'string', description: 'URL to the thumbnail image' },
width: { type: 'number', description: 'Thumbnail width in pixels' },
height: { type: 'number', description: 'Thumbnail height in pixels' },
},
}

View File

@@ -5,9 +5,9 @@ import { AuthMode } from '@/blocks/types'
export const KalshiBlock: BlockConfig = {
type: 'kalshi',
name: 'Kalshi',
description: 'Access prediction markets data from Kalshi',
description: 'Access prediction markets and trade on Kalshi',
longDescription:
'Integrate Kalshi prediction markets into the workflow. Can get markets, market, events, event, balance, positions, orders, orderbook, trades, candlesticks, fills, series, and exchange status.',
'Integrate Kalshi prediction markets into the workflow. Can get markets, market, events, event, balance, positions, orders, orderbook, trades, candlesticks, fills, series, exchange status, and place/cancel/amend trades.',
docsLink: 'https://docs.sim.ai/tools/kalshi',
authMode: AuthMode.ApiKey,
category: 'tools',
@@ -26,12 +26,16 @@ export const KalshiBlock: BlockConfig = {
{ label: 'Get Balance', id: 'get_balance' },
{ label: 'Get Positions', id: 'get_positions' },
{ label: 'Get Orders', id: 'get_orders' },
{ label: 'Get Order', id: 'get_order' },
{ label: 'Get Orderbook', id: 'get_orderbook' },
{ label: 'Get Trades', id: 'get_trades' },
{ label: 'Get Candlesticks', id: 'get_candlesticks' },
{ label: 'Get Fills', id: 'get_fills' },
{ label: 'Get Series by Ticker', id: 'get_series_by_ticker' },
{ label: 'Get Exchange Status', id: 'get_exchange_status' },
{ label: 'Create Order', id: 'create_order' },
{ label: 'Cancel Order', id: 'cancel_order' },
{ label: 'Amend Order', id: 'amend_order' },
],
value: () => 'get_markets',
},
@@ -43,7 +47,16 @@ export const KalshiBlock: BlockConfig = {
placeholder: 'Your Kalshi API Key ID',
condition: {
field: 'operation',
value: ['get_balance', 'get_positions', 'get_orders', 'get_fills'],
value: [
'get_balance',
'get_positions',
'get_orders',
'get_order',
'get_fills',
'create_order',
'cancel_order',
'amend_order',
],
},
required: true,
},
@@ -55,7 +68,16 @@ export const KalshiBlock: BlockConfig = {
placeholder: 'Your RSA Private Key (PEM format)',
condition: {
field: 'operation',
value: ['get_balance', 'get_positions', 'get_orders', 'get_fills'],
value: [
'get_balance',
'get_positions',
'get_orders',
'get_order',
'get_fills',
'create_order',
'cancel_order',
'amend_order',
],
},
required: true,
},
@@ -147,35 +169,20 @@ export const KalshiBlock: BlockConfig = {
],
condition: { field: 'operation', value: ['get_orders'] },
},
// Get Orderbook fields
{
id: 'depth',
title: 'Depth',
type: 'short-input',
placeholder: 'Number of price levels per side',
condition: { field: 'operation', value: ['get_orderbook'] },
},
// Get Trades fields
{
id: 'tickerTrades',
title: 'Market Ticker',
type: 'short-input',
placeholder: 'Filter by market ticker (optional)',
condition: { field: 'operation', value: ['get_trades'] },
},
// Get Fills timestamp filters
{
id: 'minTs',
title: 'Min Timestamp',
type: 'short-input',
placeholder: 'Minimum timestamp (Unix milliseconds)',
condition: { field: 'operation', value: ['get_trades', 'get_fills'] },
condition: { field: 'operation', value: ['get_fills'] },
},
{
id: 'maxTs',
title: 'Max Timestamp',
type: 'short-input',
placeholder: 'Maximum timestamp (Unix milliseconds)',
condition: { field: 'operation', value: ['get_trades', 'get_fills'] },
condition: { field: 'operation', value: ['get_fills'] },
},
// Get Candlesticks fields
{
@@ -198,14 +205,16 @@ export const KalshiBlock: BlockConfig = {
id: 'startTs',
title: 'Start Timestamp',
type: 'short-input',
placeholder: 'Start timestamp (Unix milliseconds)',
placeholder: 'Start timestamp (Unix seconds)',
required: true,
condition: { field: 'operation', value: ['get_candlesticks'] },
},
{
id: 'endTs',
title: 'End Timestamp',
type: 'short-input',
placeholder: 'End timestamp (Unix milliseconds)',
placeholder: 'End timestamp (Unix seconds)',
required: true,
condition: { field: 'operation', value: ['get_candlesticks'] },
},
{
@@ -213,11 +222,11 @@ export const KalshiBlock: BlockConfig = {
title: 'Period Interval',
type: 'dropdown',
options: [
{ label: 'All', id: '' },
{ label: '1 minute', id: '1' },
{ label: '1 hour', id: '60' },
{ label: '1 day', id: '1440' },
],
required: true,
condition: { field: 'operation', value: ['get_candlesticks'] },
},
// Get Fills fields
@@ -244,6 +253,146 @@ export const KalshiBlock: BlockConfig = {
required: true,
condition: { field: 'operation', value: ['get_series_by_ticker'] },
},
// Order ID for get_order, cancel_order, amend_order
{
id: 'orderIdParam',
title: 'Order ID',
type: 'short-input',
placeholder: 'Order ID',
required: true,
condition: { field: 'operation', value: ['get_order', 'cancel_order', 'amend_order'] },
},
// Create Order fields
{
id: 'tickerOrder',
title: 'Market Ticker',
type: 'short-input',
placeholder: 'Market ticker (e.g., KXBTC-24DEC31)',
required: true,
condition: { field: 'operation', value: ['create_order', 'amend_order'] },
},
{
id: 'side',
title: 'Side',
type: 'dropdown',
options: [
{ label: 'Yes', id: 'yes' },
{ label: 'No', id: 'no' },
],
required: true,
condition: { field: 'operation', value: ['create_order', 'amend_order'] },
},
{
id: 'action',
title: 'Action',
type: 'dropdown',
options: [
{ label: 'Buy', id: 'buy' },
{ label: 'Sell', id: 'sell' },
],
required: true,
condition: { field: 'operation', value: ['create_order', 'amend_order'] },
},
{
id: 'count',
title: 'Contracts',
type: 'short-input',
placeholder: 'Number of contracts',
required: true,
condition: { field: 'operation', value: ['create_order'] },
},
{
id: 'countAmend',
title: 'Contracts',
type: 'short-input',
placeholder: 'Updated number of contracts (optional)',
condition: { field: 'operation', value: ['amend_order'] },
},
{
id: 'orderType',
title: 'Order Type',
type: 'dropdown',
options: [
{ label: 'Limit', id: 'limit' },
{ label: 'Market', id: 'market' },
],
condition: { field: 'operation', value: ['create_order'] },
},
{
id: 'yesPrice',
title: 'Yes Price (cents)',
type: 'short-input',
placeholder: 'Yes price in cents (1-99)',
condition: { field: 'operation', value: ['create_order', 'amend_order'] },
},
{
id: 'noPrice',
title: 'No Price (cents)',
type: 'short-input',
placeholder: 'No price in cents (1-99)',
condition: { field: 'operation', value: ['create_order', 'amend_order'] },
},
{
id: 'clientOrderId',
title: 'Client Order ID',
type: 'short-input',
placeholder: 'Custom order identifier (optional)',
condition: { field: 'operation', value: ['create_order'] },
},
{
id: 'clientOrderIdAmend',
title: 'Client Order ID',
type: 'short-input',
placeholder: 'Original client order ID',
required: true,
condition: { field: 'operation', value: ['amend_order'] },
},
{
id: 'updatedClientOrderId',
title: 'New Client Order ID',
type: 'short-input',
placeholder: 'New client order ID after amendment',
required: true,
condition: { field: 'operation', value: ['amend_order'] },
},
{
id: 'timeInForce',
title: 'Time in Force',
type: 'dropdown',
options: [
{ label: 'Good Till Canceled', id: 'good_till_canceled' },
{ label: 'Fill or Kill', id: 'fill_or_kill' },
{ label: 'Immediate or Cancel', id: 'immediate_or_cancel' },
],
condition: { field: 'operation', value: ['create_order'] },
},
{
id: 'expirationTs',
title: 'Expiration',
type: 'short-input',
placeholder: 'Unix timestamp for order expiration',
condition: { field: 'operation', value: ['create_order'] },
},
{
id: 'postOnly',
title: 'Post Only',
type: 'dropdown',
options: [
{ label: 'No', id: '' },
{ label: 'Yes', id: 'true' },
],
condition: { field: 'operation', value: ['create_order'] },
},
{
id: 'reduceOnly',
title: 'Reduce Only',
type: 'dropdown',
options: [
{ label: 'No', id: '' },
{ label: 'Yes', id: 'true' },
],
condition: { field: 'operation', value: ['create_order'] },
},
// Pagination fields
{
id: 'limit',
@@ -289,12 +438,16 @@ export const KalshiBlock: BlockConfig = {
'kalshi_get_balance',
'kalshi_get_positions',
'kalshi_get_orders',
'kalshi_get_order',
'kalshi_get_orderbook',
'kalshi_get_trades',
'kalshi_get_candlesticks',
'kalshi_get_fills',
'kalshi_get_series_by_ticker',
'kalshi_get_exchange_status',
'kalshi_create_order',
'kalshi_cancel_order',
'kalshi_amend_order',
],
config: {
tool: (params) => {
@@ -313,6 +466,8 @@ export const KalshiBlock: BlockConfig = {
return 'kalshi_get_positions'
case 'get_orders':
return 'kalshi_get_orders'
case 'get_order':
return 'kalshi_get_order'
case 'get_orderbook':
return 'kalshi_get_orderbook'
case 'get_trades':
@@ -325,6 +480,12 @@ export const KalshiBlock: BlockConfig = {
return 'kalshi_get_series_by_ticker'
case 'get_exchange_status':
return 'kalshi_get_exchange_status'
case 'create_order':
return 'kalshi_create_order'
case 'cancel_order':
return 'kalshi_cancel_order'
case 'amend_order':
return 'kalshi_amend_order'
default:
return 'kalshi_get_markets'
}
@@ -334,11 +495,15 @@ export const KalshiBlock: BlockConfig = {
operation,
orderStatus,
tickerFilter,
tickerTrades,
tickerFills,
tickerCandlesticks,
seriesTickerCandlesticks,
seriesTickerGet,
orderIdParam,
tickerOrder,
orderType,
countAmend,
clientOrderIdAmend,
...rest
} = params
const cleanParams: Record<string, any> = {}
@@ -353,11 +518,6 @@ export const KalshiBlock: BlockConfig = {
cleanParams.ticker = tickerFilter
}
// Map tickerTrades to ticker for get_trades
if (operation === 'get_trades' && tickerTrades) {
cleanParams.ticker = tickerTrades
}
// Map tickerFills to ticker for get_fills
if (operation === 'get_fills' && tickerFills) {
cleanParams.ticker = tickerFills
@@ -374,6 +534,36 @@ export const KalshiBlock: BlockConfig = {
cleanParams.seriesTicker = seriesTickerGet
}
// Map orderIdParam to orderId for get_order, cancel_order, amend_order
if (
(operation === 'get_order' ||
operation === 'cancel_order' ||
operation === 'amend_order') &&
orderIdParam
) {
cleanParams.orderId = orderIdParam
}
// Map tickerOrder to ticker for create_order, amend_order
if ((operation === 'create_order' || operation === 'amend_order') && tickerOrder) {
cleanParams.ticker = tickerOrder
}
// Map orderType to type for create_order
if (operation === 'create_order' && orderType) {
cleanParams.type = orderType
}
// Map countAmend to count for amend_order
if (operation === 'amend_order' && countAmend) {
cleanParams.count = countAmend
}
// Map clientOrderIdAmend to clientOrderId for amend_order
if (operation === 'amend_order' && clientOrderIdAmend) {
cleanParams.clientOrderId = clientOrderIdAmend
}
Object.entries(rest).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
cleanParams[key] = value

View File

@@ -193,17 +193,40 @@ export const PolymarketBlock: BlockConfig = {
{
id: 'order',
title: 'Sort By',
type: 'short-input',
placeholder: 'Sort field (e.g., id, volume, liquidity)',
condition: { field: 'operation', value: ['get_markets', 'get_events'] },
type: 'dropdown',
options: [
{ label: 'Default', id: '' },
{ label: 'Volume', id: 'volumeNum' },
{ label: 'Liquidity', id: 'liquidityNum' },
{ label: 'Start Date', id: 'startDate' },
{ label: 'End Date', id: 'endDate' },
{ label: 'Created At', id: 'createdAt' },
{ label: 'Updated At', id: 'updatedAt' },
],
condition: { field: 'operation', value: ['get_markets'] },
},
{
id: 'orderEvents',
title: 'Sort By',
type: 'dropdown',
options: [
{ label: 'Default', id: '' },
{ label: 'Volume', id: 'volume' },
{ label: 'Liquidity', id: 'liquidity' },
{ label: 'Start Date', id: 'startDate' },
{ label: 'End Date', id: 'endDate' },
{ label: 'Created At', id: 'createdAt' },
{ label: 'Updated At', id: 'updatedAt' },
],
condition: { field: 'operation', value: ['get_events'] },
},
{
id: 'ascending',
title: 'Sort Order',
type: 'dropdown',
options: [
{ label: 'Descending (newest first)', id: 'false' },
{ label: 'Ascending (oldest first)', id: 'true' },
{ label: 'Descending', id: 'false' },
{ label: 'Ascending', id: 'true' },
],
condition: { field: 'operation', value: ['get_markets', 'get_events'] },
},
@@ -298,7 +321,7 @@ export const PolymarketBlock: BlockConfig = {
}
},
params: (params) => {
const { operation, marketSlug, eventSlug, ...rest } = params
const { operation, marketSlug, eventSlug, orderEvents, order, ...rest } = params
const cleanParams: Record<string, any> = {}
// Map marketSlug to slug for get_market
@@ -311,6 +334,13 @@ export const PolymarketBlock: BlockConfig = {
cleanParams.slug = eventSlug
}
// Map order field based on operation (markets use volumeNum/liquidityNum, events use volume/liquidity)
if (operation === 'get_markets' && order) {
cleanParams.order = order
} else if (operation === 'get_events' && orderEvents) {
cleanParams.order = orderEvents
}
// Convert numeric fields from string to number for get_price_history
if (operation === 'get_price_history') {
if (rest.fidelity) cleanParams.fidelity = Number(rest.fidelity)

View File

@@ -35,6 +35,7 @@ import { GoogleDocsBlock } from '@/blocks/blocks/google_docs'
import { GoogleDriveBlock } from '@/blocks/blocks/google_drive'
import { GoogleFormsBlock } from '@/blocks/blocks/google_form'
import { GoogleSheetsBlock } from '@/blocks/blocks/google_sheets'
import { GoogleSlidesBlock } from '@/blocks/blocks/google_slides'
import { GoogleVaultBlock } from '@/blocks/blocks/google_vault'
import { GrafanaBlock } from '@/blocks/blocks/grafana'
import { GuardrailsBlock } from '@/blocks/blocks/guardrails'
@@ -172,6 +173,7 @@ export const registry: Record<string, BlockConfig> = {
google_forms: GoogleFormsBlock,
google_search: GoogleSearchBlock,
google_sheets: GoogleSheetsBlock,
google_slides: GoogleSlidesBlock,
google_vault: GoogleVaultBlock,
hubspot: HubSpotBlock,
huggingface: HuggingFaceBlock,

View File

@@ -0,0 +1,303 @@
'use client'
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { highlight, languages } from 'prismjs'
import { List, type RowComponentProps, useDynamicRowHeight, useListRef } from 'react-window'
import 'prismjs/components/prism-javascript'
import 'prismjs/components/prism-python'
import 'prismjs/components/prism-json'
import { cn } from '@/lib/core/utils/cn'
import { CODE_LINE_HEIGHT_PX, calculateGutterWidth } from './code'
/**
* Virtualized code viewer for large outputs.
* Uses react-window to render only visible lines, keeping DOM minimal.
* Supports Prism syntax highlighting, line numbers, text wrapping, and search.
*
* @example
* ```tsx
* <VirtualizedCodeViewer
* code={JSON.stringify(data, null, 2)}
* showGutter
* language="json"
* wrapText
* searchQuery="error"
* currentMatchIndex={0}
* />
* ```
*/
/**
* Props for the VirtualizedCodeViewer component.
*/
interface VirtualizedCodeViewerProps {
/** Code content to display */
code: string
/** Whether to show line numbers gutter */
showGutter?: boolean
/** Language for syntax highlighting */
language?: 'javascript' | 'json' | 'python'
/** Additional CSS classes for the container */
className?: string
/** Left padding offset */
paddingLeft?: number
/** Inline styles for the gutter */
gutterStyle?: React.CSSProperties
/** Whether to wrap text */
wrapText?: boolean
/** Search query to highlight in the code */
searchQuery?: string
/** Index of the currently active match */
currentMatchIndex?: number
/** Callback when match count changes */
onMatchCountChange?: (count: number) => void
/** Ref for the content container */
contentRef?: React.RefObject<HTMLDivElement | null>
}
interface HighlightedLine {
lineNumber: number
html: string
}
interface CodeRowProps {
lines: HighlightedLine[]
gutterWidth: number
showGutter: boolean
gutterStyle?: React.CSSProperties
leftOffset: number
wrapText: boolean
}
function escapeRegex(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
function countSearchMatches(code: string, searchQuery: string): number {
if (!searchQuery.trim()) return 0
const escaped = escapeRegex(searchQuery)
const regex = new RegExp(escaped, 'gi')
const matches = code.match(regex)
return matches?.length ?? 0
}
function applySearchHighlightingToLine(
html: string,
searchQuery: string,
currentMatchIndex: number,
globalMatchOffset: number
): { html: string; matchesInLine: number } {
if (!searchQuery.trim()) return { html, matchesInLine: 0 }
const escaped = escapeRegex(searchQuery)
const regex = new RegExp(`(${escaped})`, 'gi')
const parts = html.split(/(<[^>]+>)/g)
let matchesInLine = 0
const result = parts
.map((part) => {
if (part.startsWith('<') && part.endsWith('>')) {
return part
}
return part.replace(regex, (match) => {
const globalIndex = globalMatchOffset + matchesInLine
const isCurrentMatch = globalIndex === currentMatchIndex
matchesInLine++
const bgClass = isCurrentMatch
? 'bg-[#F6AD55] text-[#1a1a1a] dark:bg-[#F6AD55] dark:text-[#1a1a1a]'
: 'bg-[#FCD34D]/40 dark:bg-[#FCD34D]/30'
return `<mark class="${bgClass} rounded-[2px]" data-search-match>${match}</mark>`
})
})
.join('')
return { html: result, matchesInLine }
}
function CodeRow({ index, style, ...props }: RowComponentProps<CodeRowProps>) {
const { lines, gutterWidth, showGutter, gutterStyle, leftOffset, wrapText } = props
const line = lines[index]
return (
<div style={style} className='flex' data-row-index={index}>
{showGutter && (
<div
className='flex-shrink-0 select-none pr-0.5 text-right text-[var(--text-muted)] text-xs tabular-nums leading-[21px] dark:text-[#a8a8a8]'
style={{ width: gutterWidth, marginLeft: leftOffset, ...gutterStyle }}
>
{line.lineNumber}
</div>
)}
<pre
className={cn(
'm-0 flex-1 pr-2 pl-2 font-mono text-[13px] text-[var(--text-primary)] leading-[21px] dark:text-[#eeeeee]',
wrapText ? 'whitespace-pre-wrap break-words' : 'whitespace-pre'
)}
dangerouslySetInnerHTML={{ __html: line.html || '&nbsp;' }}
/>
</div>
)
}
export const VirtualizedCodeViewer = memo(function VirtualizedCodeViewer({
code,
showGutter = true,
language = 'json',
className,
paddingLeft = 0,
gutterStyle,
wrapText = false,
searchQuery,
currentMatchIndex = 0,
onMatchCountChange,
contentRef,
}: VirtualizedCodeViewerProps) {
const containerRef = useRef<HTMLDivElement>(null)
const listRef = useListRef(null)
const [containerHeight, setContainerHeight] = useState(400)
const dynamicRowHeight = useDynamicRowHeight({
defaultRowHeight: CODE_LINE_HEIGHT_PX,
key: wrapText ? 'wrap' : 'nowrap',
})
const matchCount = useMemo(() => countSearchMatches(code, searchQuery || ''), [code, searchQuery])
useEffect(() => {
onMatchCountChange?.(matchCount)
}, [matchCount, onMatchCountChange])
const lines = useMemo(() => code.split('\n'), [code])
const lineCount = lines.length
const gutterWidth = useMemo(() => calculateGutterWidth(lineCount), [lineCount])
const highlightedLines = useMemo(() => {
const lang = languages[language] || languages.javascript
return lines.map((line, idx) => ({
lineNumber: idx + 1,
html: highlight(line, lang, language),
}))
}, [lines, language])
const matchOffsets = useMemo(() => {
if (!searchQuery?.trim()) return []
const offsets: number[] = []
let cumulative = 0
const escaped = escapeRegex(searchQuery)
const regex = new RegExp(escaped, 'gi')
for (const line of lines) {
offsets.push(cumulative)
const matches = line.match(regex)
cumulative += matches?.length ?? 0
}
return offsets
}, [lines, searchQuery])
const linesWithSearch = useMemo(() => {
if (!searchQuery?.trim()) return highlightedLines
return highlightedLines.map((line, idx) => {
const { html } = applySearchHighlightingToLine(
line.html,
searchQuery,
currentMatchIndex,
matchOffsets[idx]
)
return { ...line, html }
})
}, [highlightedLines, searchQuery, currentMatchIndex, matchOffsets])
useEffect(() => {
if (!searchQuery?.trim() || matchCount === 0 || !listRef.current) return
let accumulated = 0
for (let i = 0; i < matchOffsets.length; i++) {
const matchesInThisLine = (matchOffsets[i + 1] ?? matchCount) - matchOffsets[i]
if (currentMatchIndex >= accumulated && currentMatchIndex < accumulated + matchesInThisLine) {
listRef.current.scrollToRow({ index: i, align: 'center' })
break
}
accumulated += matchesInThisLine
}
}, [currentMatchIndex, searchQuery, matchCount, matchOffsets, listRef])
useEffect(() => {
const container = containerRef.current
if (!container) return
const parent = container.parentElement
if (!parent) return
const updateHeight = () => {
setContainerHeight(parent.clientHeight)
}
updateHeight()
const resizeObserver = new ResizeObserver(updateHeight)
resizeObserver.observe(parent)
return () => resizeObserver.disconnect()
}, [])
useEffect(() => {
if (!wrapText) return
const container = containerRef.current
if (!container) return
const rows = container.querySelectorAll('[data-row-index]')
if (rows.length === 0) return
return dynamicRowHeight.observeRowElements(rows)
}, [wrapText, dynamicRowHeight, linesWithSearch])
const setRefs = useCallback(
(el: HTMLDivElement | null) => {
containerRef.current = el
if (contentRef && 'current' in contentRef) {
contentRef.current = el
}
},
[contentRef]
)
const rowProps = useMemo(
() => ({
lines: linesWithSearch,
gutterWidth,
showGutter,
gutterStyle,
leftOffset: paddingLeft,
wrapText,
}),
[linesWithSearch, gutterWidth, showGutter, gutterStyle, paddingLeft, wrapText]
)
return (
<div
ref={setRefs}
className={cn(
'code-editor-theme relative rounded-[4px] border border-[var(--border-strong)]',
'bg-[var(--surface-1)] font-medium font-mono text-sm',
'dark:bg-[#1F1F1F]',
className
)}
style={{ height: containerHeight }}
>
<List
listRef={listRef}
defaultHeight={containerHeight}
rowCount={lineCount}
rowHeight={wrapText ? dynamicRowHeight : CODE_LINE_HEIGHT_PX}
rowComponent={CodeRow}
rowProps={rowProps}
overscanCount={5}
className='overflow-x-auto'
/>
</div>
)
})

View File

@@ -8,6 +8,7 @@ export {
highlight,
languages,
} from './code/code'
export { VirtualizedCodeViewer } from './code/code-optimized'
export { Combobox, type ComboboxOption } from './combobox/combobox'
export { Input } from './input/input'
export { Label } from './label/label'

View File

@@ -1084,6 +1084,27 @@ export function GoogleDocsIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function GoogleSlidesIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 48 48'
width='96px'
height='96px'
>
<path
fill='#FFC107'
d='M37,45H11c-1.657,0-3-1.343-3-3V6c0-1.657,1.343-3,3-3h19l10,10v29C40,43.657,38.657,45,37,45z'
/>
<path fill='#FFECB3' d='M40 13L30 13 30 3z' />
<path fill='#FFA000' d='M30 13L40 23 40 13z' />
<path fill='#FFF8E1' d='M14 21H34V35H14z' />
<path fill='#FFA000' d='M16 23H32V26H16zM16 28H28V30H16z' />
</svg>
)
}
export function GoogleCalendarIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg

View File

@@ -98,6 +98,8 @@ function resolveFileSelector(
return { key: 'google.drive', context, allowSearch: true }
case 'google-docs':
return { key: 'google.drive', context, allowSearch: true }
case 'google-slides':
return { key: 'google.drive', context, allowSearch: true }
case 'onedrive': {
const key: SelectorKey = subBlock.mimeType === 'file' ? 'onedrive.files' : 'onedrive.folders'
return { key, context, allowSearch: true }

View File

@@ -482,7 +482,6 @@ export const auth = betterAuth({
prompt: 'consent',
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-forms`,
},
{
providerId: 'google-vault',
clientId: env.GOOGLE_CLIENT_ID as string,

View File

@@ -68,6 +68,7 @@
"@react-email/components": "^0.0.34",
"@react-email/render": "2.0.0",
"@trigger.dev/sdk": "4.1.2",
"@types/react-window": "2.0.0",
"@types/three": "0.177.0",
"better-auth": "1.3.12",
"browser-image-compression": "^2.0.2",
@@ -115,6 +116,7 @@
"react-hook-form": "^7.54.2",
"react-markdown": "^10.1.0",
"react-simple-code-editor": "^0.14.1",
"react-window": "2.2.3",
"reactflow": "^11.11.4",
"rehype-autolink-headings": "^7.1.0",
"rehype-slug": "^6.0.0",

View File

@@ -46,6 +46,13 @@ export const topPagesTool: ToolConfig<AhrefsTopPagesParams, AhrefsTopPagesRespon
visibility: 'user-only',
description: 'Number of results to skip for pagination',
},
select: {
type: 'string',
required: false,
visibility: 'user-only',
description:
'Comma-separated list of fields to return (e.g., url,traffic,keywords,top_keyword,value). Default: url,traffic,keywords,top_keyword,value',
},
apiKey: {
type: 'string',
required: true,
@@ -62,6 +69,9 @@ export const topPagesTool: ToolConfig<AhrefsTopPagesParams, AhrefsTopPagesRespon
// Date is required - default to today if not provided
const date = params.date || new Date().toISOString().split('T')[0]
url.searchParams.set('date', date)
// Select is required by API v3 - default to common fields if not provided
const select = params.select || 'url,traffic,keywords,top_keyword,value'
url.searchParams.set('select', select)
if (params.mode) url.searchParams.set('mode', params.mode)
if (params.limit) url.searchParams.set('limit', String(params.limit))
if (params.offset) url.searchParams.set('offset', String(params.offset))

View File

@@ -126,6 +126,7 @@ export interface AhrefsTopPagesParams extends AhrefsBaseParams {
mode?: AhrefsTargetMode
limit?: number
offset?: number
select?: string // Comma-separated list of fields to return (e.g., "url,traffic,keywords,top_keyword,value")
}
export interface AhrefsTopPage {

View File

@@ -0,0 +1,198 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesAddImageTool')
interface AddImageParams {
accessToken: string
presentationId: string
pageObjectId: string
imageUrl: string
width?: number
height?: number
positionX?: number
positionY?: number
}
interface AddImageResponse {
success: boolean
output: {
imageId: string
metadata: {
presentationId: string
pageObjectId: string
imageUrl: string
url: string
}
}
}
// EMU (English Metric Units) conversion: 1 inch = 914400 EMU, 1 pt = 12700 EMU
const PT_TO_EMU = 12700
export const addImageTool: ToolConfig<AddImageParams, AddImageResponse> = {
id: 'google_slides_add_image',
name: 'Add Image to Google Slides',
description: 'Insert an image into a specific slide in a Google Slides presentation',
version: '1.0',
oauth: {
required: true,
provider: 'google-drive',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
description: 'The ID of the presentation',
},
pageObjectId: {
type: 'string',
required: true,
description: 'The object ID of the slide/page to add the image to',
},
imageUrl: {
type: 'string',
required: true,
description: 'The publicly accessible URL of the image (must be PNG, JPEG, or GIF, max 50MB)',
},
width: {
type: 'number',
required: false,
description: 'Width of the image in points (default: 300)',
},
height: {
type: 'number',
required: false,
description: 'Height of the image in points (default: 200)',
},
positionX: {
type: 'number',
required: false,
description: 'X position from the left edge in points (default: 100)',
},
positionY: {
type: 'number',
required: false,
description: 'Y position from the top edge in points (default: 100)',
},
},
request: {
url: (params) => {
const presentationId = params.presentationId?.trim()
if (!presentationId) {
throw new Error('Presentation ID is required')
}
return `https://slides.googleapis.com/v1/presentations/${presentationId}:batchUpdate`
},
method: 'POST',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
const pageObjectId = params.pageObjectId?.trim()
const imageUrl = params.imageUrl?.trim()
if (!pageObjectId) {
throw new Error('Page Object ID is required')
}
if (!imageUrl) {
throw new Error('Image URL is required')
}
// Generate a unique object ID for the new image
const imageObjectId = `image_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`
// Convert points to EMU (default sizes if not specified)
const widthEmu = (params.width || 300) * PT_TO_EMU
const heightEmu = (params.height || 200) * PT_TO_EMU
const translateX = (params.positionX || 100) * PT_TO_EMU
const translateY = (params.positionY || 100) * PT_TO_EMU
return {
requests: [
{
createImage: {
objectId: imageObjectId,
url: imageUrl,
elementProperties: {
pageObjectId: pageObjectId,
size: {
width: {
magnitude: widthEmu,
unit: 'EMU',
},
height: {
magnitude: heightEmu,
unit: 'EMU',
},
},
transform: {
scaleX: 1,
scaleY: 1,
translateX: translateX,
translateY: translateY,
unit: 'EMU',
},
},
},
},
],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to add image')
}
// The response contains the created image's object ID
const createImageReply = data.replies?.[0]?.createImage
const imageId = createImageReply?.objectId || ''
const presentationId = params?.presentationId?.trim() || ''
const pageObjectId = params?.pageObjectId?.trim() || ''
return {
success: true,
output: {
imageId,
metadata: {
presentationId,
pageObjectId,
imageUrl: params?.imageUrl?.trim() || '',
url: `https://docs.google.com/presentation/d/${presentationId}/edit`,
},
},
}
},
outputs: {
imageId: {
type: 'string',
description: 'The object ID of the newly created image',
},
metadata: {
type: 'json',
description: 'Operation metadata including presentation ID and image URL',
},
},
}

View File

@@ -0,0 +1,187 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesAddSlideTool')
interface AddSlideParams {
accessToken: string
presentationId: string
layout?: string
insertionIndex?: number
placeholderIdMappings?: string
}
interface AddSlideResponse {
success: boolean
output: {
slideId: string
metadata: {
presentationId: string
layout: string
insertionIndex?: number
url: string
}
}
}
// Predefined layouts available in Google Slides API
const PREDEFINED_LAYOUTS = [
'BLANK',
'CAPTION_ONLY',
'TITLE',
'TITLE_AND_BODY',
'TITLE_AND_TWO_COLUMNS',
'TITLE_ONLY',
'SECTION_HEADER',
'SECTION_TITLE_AND_DESCRIPTION',
'ONE_COLUMN_TEXT',
'MAIN_POINT',
'BIG_NUMBER',
] as const
export const addSlideTool: ToolConfig<AddSlideParams, AddSlideResponse> = {
id: 'google_slides_add_slide',
name: 'Add Slide to Google Slides',
description: 'Add a new slide to a Google Slides presentation with a specified layout',
version: '1.0',
oauth: {
required: true,
provider: 'google-drive',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
description: 'The ID of the presentation',
},
layout: {
type: 'string',
required: false,
description:
'The predefined layout for the slide (BLANK, TITLE, TITLE_AND_BODY, TITLE_ONLY, SECTION_HEADER, etc.). Defaults to BLANK.',
},
insertionIndex: {
type: 'number',
required: false,
description:
'The optional zero-based index indicating where to insert the slide. If not specified, the slide is added at the end.',
},
placeholderIdMappings: {
type: 'string',
required: false,
description:
'JSON array of placeholder mappings to assign custom object IDs to placeholders. Format: [{"layoutPlaceholder":{"type":"TITLE"},"objectId":"custom_title_id"}]',
},
},
request: {
url: (params) => {
const presentationId = params.presentationId?.trim()
if (!presentationId) {
throw new Error('Presentation ID is required')
}
return `https://slides.googleapis.com/v1/presentations/${presentationId}:batchUpdate`
},
method: 'POST',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
// Generate a unique object ID for the new slide
const slideObjectId = `slide_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`
// Validate and normalize the layout
let layout = (params.layout || 'BLANK').toUpperCase()
if (!PREDEFINED_LAYOUTS.includes(layout as (typeof PREDEFINED_LAYOUTS)[number])) {
logger.warn(`Invalid layout "${params.layout}", defaulting to BLANK`)
layout = 'BLANK'
}
const createSlideRequest: Record<string, any> = {
objectId: slideObjectId,
slideLayoutReference: {
predefinedLayout: layout,
},
}
// Add insertion index if specified
if (params.insertionIndex !== undefined && params.insertionIndex >= 0) {
createSlideRequest.insertionIndex = params.insertionIndex
}
// Add placeholder ID mappings if specified (for advanced use cases)
if (params.placeholderIdMappings?.trim()) {
try {
const mappings = JSON.parse(params.placeholderIdMappings)
if (Array.isArray(mappings) && mappings.length > 0) {
createSlideRequest.placeholderIdMappings = mappings
}
} catch (e) {
logger.warn('Invalid placeholderIdMappings JSON, ignoring:', e)
}
}
return {
requests: [
{
createSlide: createSlideRequest,
},
],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to add slide')
}
// The response contains the created slide's object ID
const createSlideReply = data.replies?.[0]?.createSlide
const slideId = createSlideReply?.objectId || ''
const presentationId = params?.presentationId?.trim() || ''
const layout = (params?.layout || 'BLANK').toUpperCase()
return {
success: true,
output: {
slideId,
metadata: {
presentationId,
layout,
insertionIndex: params?.insertionIndex,
url: `https://docs.google.com/presentation/d/${presentationId}/edit`,
},
},
}
},
outputs: {
slideId: {
type: 'string',
description: 'The object ID of the newly created slide',
},
metadata: {
type: 'json',
description: 'Operation metadata including presentation ID, layout, and URL',
},
},
}

View File

@@ -0,0 +1,158 @@
import { createLogger } from '@/lib/logs/console/logger'
import type {
GoogleSlidesCreateResponse,
GoogleSlidesToolParams,
} from '@/tools/google_slides/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesCreateTool')
export const createTool: ToolConfig<GoogleSlidesToolParams, GoogleSlidesCreateResponse> = {
id: 'google_slides_create',
name: 'Create Google Slides Presentation',
description: 'Create a new Google Slides presentation',
version: '1.0',
oauth: {
required: true,
provider: 'google-drive',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
title: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The title of the presentation to create',
},
content: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'The content to add to the first slide',
},
folderSelector: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Select the folder to create the presentation in',
},
folderId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'The ID of the folder to create the presentation in (internal use)',
},
},
request: {
url: () => {
return 'https://www.googleapis.com/drive/v3/files'
},
method: 'POST',
headers: (params) => {
// Validate access token
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
if (!params.title) {
throw new Error('Title is required')
}
const requestBody: any = {
name: params.title,
mimeType: 'application/vnd.google-apps.presentation',
}
// Add parent folder if specified (prefer folderSelector over folderId)
const folderId = params.folderSelector || params.folderId
if (folderId) {
requestBody.parents = [folderId]
}
return requestBody
},
},
postProcess: async (result, params, executeTool) => {
if (!result.success) {
return result
}
const presentationId = result.output.metadata.presentationId
if (params.content && presentationId) {
try {
const writeParams = {
accessToken: params.accessToken,
presentationId: presentationId,
content: params.content,
}
const writeResult = await executeTool('google_slides_write', writeParams)
if (!writeResult.success) {
logger.warn(
'Failed to add content to presentation, but presentation was created:',
writeResult.error
)
}
} catch (error) {
logger.warn('Error adding content to presentation:', { error })
// Don't fail the overall operation if adding content fails
}
}
return result
},
transformResponse: async (response: Response) => {
try {
// Get the response data
const responseText = await response.text()
const data = JSON.parse(responseText)
const presentationId = data.id
const title = data.name
const metadata = {
presentationId,
title: title || 'Untitled Presentation',
mimeType: 'application/vnd.google-apps.presentation',
url: `https://docs.google.com/presentation/d/${presentationId}/edit`,
}
return {
success: true,
output: {
metadata,
},
}
} catch (error) {
logger.error('Google Slides create - Error processing response:', {
error,
})
throw error
}
},
outputs: {
metadata: {
type: 'json',
description: 'Created presentation metadata including ID, title, and URL',
},
},
}

View File

@@ -0,0 +1,168 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesGetThumbnailTool')
interface GetThumbnailParams {
accessToken: string
presentationId: string
pageObjectId: string
thumbnailSize?: string
mimeType?: string
}
interface GetThumbnailResponse {
success: boolean
output: {
contentUrl: string
width: number
height: number
metadata: {
presentationId: string
pageObjectId: string
thumbnailSize: string
mimeType: string
}
}
}
// Available thumbnail sizes
const THUMBNAIL_SIZES = ['SMALL', 'MEDIUM', 'LARGE'] as const
// Available MIME types for thumbnails
const MIME_TYPES = ['PNG', 'GIF'] as const
export const getThumbnailTool: ToolConfig<GetThumbnailParams, GetThumbnailResponse> = {
id: 'google_slides_get_thumbnail',
name: 'Get Slide Thumbnail',
description: 'Generate a thumbnail image of a specific slide in a Google Slides presentation',
version: '1.0',
oauth: {
required: true,
provider: 'google-drive',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
description: 'The ID of the presentation',
},
pageObjectId: {
type: 'string',
required: true,
description: 'The object ID of the slide/page to get a thumbnail for',
},
thumbnailSize: {
type: 'string',
required: false,
description:
'The size of the thumbnail: SMALL (200px), MEDIUM (800px), or LARGE (1600px). Defaults to MEDIUM.',
},
mimeType: {
type: 'string',
required: false,
description: 'The MIME type of the thumbnail image: PNG or GIF. Defaults to PNG.',
},
},
request: {
url: (params) => {
const presentationId = params.presentationId?.trim()
const pageObjectId = params.pageObjectId?.trim()
if (!presentationId) {
throw new Error('Presentation ID is required')
}
if (!pageObjectId) {
throw new Error('Page Object ID is required')
}
// Build the URL with query parameters for thumbnail properties
let size = (params.thumbnailSize || 'MEDIUM').toUpperCase()
if (!THUMBNAIL_SIZES.includes(size as (typeof THUMBNAIL_SIZES)[number])) {
size = 'MEDIUM'
}
// Validate and normalize mimeType
let mimeType = (params.mimeType || 'PNG').toUpperCase()
if (!MIME_TYPES.includes(mimeType as (typeof MIME_TYPES)[number])) {
mimeType = 'PNG'
}
// The API uses thumbnailProperties as query parameters
let url = `https://slides.googleapis.com/v1/presentations/${presentationId}/pages/${pageObjectId}/thumbnail?thumbnailProperties.thumbnailSize=${size}`
// Add mimeType if not the default (PNG)
if (mimeType !== 'PNG') {
url += `&thumbnailProperties.mimeType=${mimeType}`
}
return url
},
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to get thumbnail')
}
const presentationId = params?.presentationId?.trim() || ''
const pageObjectId = params?.pageObjectId?.trim() || ''
const thumbnailSize = (params?.thumbnailSize || 'MEDIUM').toUpperCase()
const mimeType = (params?.mimeType || 'PNG').toUpperCase()
return {
success: true,
output: {
contentUrl: data.contentUrl,
width: data.width,
height: data.height,
metadata: {
presentationId,
pageObjectId,
thumbnailSize,
mimeType,
},
},
}
},
outputs: {
contentUrl: {
type: 'string',
description: 'URL to the thumbnail image (valid for 30 minutes)',
},
width: {
type: 'number',
description: 'Width of the thumbnail in pixels',
},
height: {
type: 'number',
description: 'Height of the thumbnail in pixels',
},
metadata: {
type: 'json',
description: 'Operation metadata including presentation ID and page object ID',
},
},
}

View File

@@ -0,0 +1,15 @@
import { addImageTool } from '@/tools/google_slides/add_image'
import { addSlideTool } from '@/tools/google_slides/add_slide'
import { createTool } from '@/tools/google_slides/create'
import { getThumbnailTool } from '@/tools/google_slides/get_thumbnail'
import { readTool } from '@/tools/google_slides/read'
import { replaceAllTextTool } from '@/tools/google_slides/replace_all_text'
import { writeTool } from '@/tools/google_slides/write'
export const googleSlidesReadTool = readTool
export const googleSlidesWriteTool = writeTool
export const googleSlidesCreateTool = createTool
export const googleSlidesReplaceAllTextTool = replaceAllTextTool
export const googleSlidesAddSlideTool = addSlideTool
export const googleSlidesGetThumbnailTool = getThumbnailTool
export const googleSlidesAddImageTool = addImageTool

View File

@@ -0,0 +1,84 @@
import type { GoogleSlidesReadResponse, GoogleSlidesToolParams } from '@/tools/google_slides/types'
import type { ToolConfig } from '@/tools/types'
export const readTool: ToolConfig<GoogleSlidesToolParams, GoogleSlidesReadResponse> = {
id: 'google_slides_read',
name: 'Read Google Slides Presentation',
description: 'Read content from a Google Slides presentation',
version: '1.0',
oauth: {
required: true,
provider: 'google-drive',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The ID of the presentation to read',
},
},
request: {
url: (params) => {
// Ensure presentationId is valid
const presentationId = params.presentationId?.trim() || params.manualPresentationId?.trim()
if (!presentationId) {
throw new Error('Presentation ID is required')
}
return `https://slides.googleapis.com/v1/presentations/${presentationId}`
},
method: 'GET',
headers: (params) => {
// Validate access token
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
}
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
// Extract slides from the response
const slides = data.slides || []
// Create presentation metadata
const metadata = {
presentationId: data.presentationId,
title: data.title || 'Untitled Presentation',
pageSize: data.pageSize,
mimeType: 'application/vnd.google-apps.presentation',
url: `https://docs.google.com/presentation/d/${data.presentationId}/edit`,
}
return {
success: true,
output: {
slides,
metadata,
},
}
},
outputs: {
slides: { type: 'json', description: 'Array of slides with their content' },
metadata: {
type: 'json',
description: 'Presentation metadata including ID, title, and URL',
},
},
}

View File

@@ -0,0 +1,164 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesReplaceAllTextTool')
interface ReplaceAllTextParams {
accessToken: string
presentationId: string
findText: string
replaceText: string
matchCase?: boolean
pageObjectIds?: string
}
interface ReplaceAllTextResponse {
success: boolean
output: {
occurrencesChanged: number
metadata: {
presentationId: string
findText: string
replaceText: string
url: string
}
}
}
export const replaceAllTextTool: ToolConfig<ReplaceAllTextParams, ReplaceAllTextResponse> = {
id: 'google_slides_replace_all_text',
name: 'Replace All Text in Google Slides',
description: 'Find and replace all occurrences of text throughout a Google Slides presentation',
version: '1.0',
oauth: {
required: true,
provider: 'google-drive',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
description: 'The ID of the presentation',
},
findText: {
type: 'string',
required: true,
description: 'The text to find (e.g., {{placeholder}})',
},
replaceText: {
type: 'string',
required: true,
description: 'The text to replace with',
},
matchCase: {
type: 'boolean',
required: false,
description: 'Whether the search should be case-sensitive (default: true)',
},
pageObjectIds: {
type: 'string',
required: false,
description:
'Comma-separated list of slide object IDs to limit replacements to specific slides (leave empty for all slides)',
},
},
request: {
url: (params) => {
const presentationId = params.presentationId?.trim()
if (!presentationId) {
throw new Error('Presentation ID is required')
}
return `https://slides.googleapis.com/v1/presentations/${presentationId}:batchUpdate`
},
method: 'POST',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
if (!params.findText) {
throw new Error('Find text is required')
}
if (params.replaceText === undefined || params.replaceText === null) {
throw new Error('Replace text is required')
}
const replaceAllTextRequest: Record<string, any> = {
containsText: {
text: params.findText,
matchCase: params.matchCase !== false, // Default to true
},
replaceText: params.replaceText,
}
// Add pageObjectIds if specified to limit replacements to specific slides
if (params.pageObjectIds?.trim()) {
replaceAllTextRequest.pageObjectIds = params.pageObjectIds
.split(',')
.map((id) => id.trim())
.filter((id) => id.length > 0)
}
return {
requests: [
{
replaceAllText: replaceAllTextRequest,
},
],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to replace text')
}
// The response contains replies array with replaceAllText results
const replaceResult = data.replies?.[0]?.replaceAllText
const occurrencesChanged = replaceResult?.occurrencesChanged || 0
const presentationId = params?.presentationId?.trim() || ''
return {
success: true,
output: {
occurrencesChanged,
metadata: {
presentationId,
findText: params?.findText || '',
replaceText: params?.replaceText || '',
url: `https://docs.google.com/presentation/d/${presentationId}/edit`,
},
},
}
},
outputs: {
occurrencesChanged: {
type: 'number',
description: 'Number of text occurrences that were replaced',
},
metadata: {
type: 'json',
description: 'Operation metadata including presentation ID and URL',
},
},
}

View File

@@ -0,0 +1,50 @@
import type { ToolResponse } from '@/tools/types'
export interface GoogleSlidesMetadata {
presentationId: string
title: string
pageSize?: {
width: number
height: number
}
mimeType?: string
createdTime?: string
modifiedTime?: string
url?: string
}
export interface GoogleSlidesReadResponse extends ToolResponse {
output: {
slides: any[]
metadata: GoogleSlidesMetadata
}
}
export interface GoogleSlidesWriteResponse extends ToolResponse {
output: {
updatedContent: boolean
metadata: GoogleSlidesMetadata
}
}
export interface GoogleSlidesCreateResponse extends ToolResponse {
output: {
metadata: GoogleSlidesMetadata
}
}
export interface GoogleSlidesToolParams {
accessToken: string
presentationId?: string
manualPresentationId?: string
title?: string
content?: string
slideIndex?: number
folderId?: string
folderSelector?: string
}
export type GoogleSlidesResponse =
| GoogleSlidesReadResponse
| GoogleSlidesWriteResponse
| GoogleSlidesCreateResponse

View File

@@ -0,0 +1,207 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { GoogleSlidesToolParams, GoogleSlidesWriteResponse } from '@/tools/google_slides/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesWriteTool')
export const writeTool: ToolConfig<GoogleSlidesToolParams, GoogleSlidesWriteResponse> = {
id: 'google_slides_write',
name: 'Write to Google Slides Presentation',
description: 'Write or update content in a Google Slides presentation',
version: '1.0',
oauth: {
required: true,
provider: 'google-drive',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
description: 'The ID of the presentation to write to',
},
content: {
type: 'string',
required: true,
description: 'The content to write to the slide',
},
slideIndex: {
type: 'number',
required: false,
description: 'The index of the slide to write to (defaults to first slide)',
},
},
request: {
url: (params) => {
// Ensure presentationId is valid
const presentationId = params.presentationId?.trim() || params.manualPresentationId?.trim()
if (!presentationId) {
throw new Error('Presentation ID is required')
}
// First, we'll read the presentation to get slide information
return `https://slides.googleapis.com/v1/presentations/${presentationId}`
},
method: 'GET',
headers: (params) => {
// Validate access token
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
}
},
},
postProcess: async (result, params, _executeTool) => {
if (!result.success) {
return result
}
// Validate content
if (!params.content) {
throw new Error('Content is required')
}
const presentationId = params.presentationId?.trim() || params.manualPresentationId?.trim()
if (!presentationId) {
throw new Error('Presentation ID is required')
}
try {
// Get the presentation data from the initial read
const presentationData = await fetch(
`https://slides.googleapis.com/v1/presentations/${presentationId}`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${params.accessToken}`,
},
}
).then((res) => res.json())
const slideIndex = params.slideIndex || 0
const slide = presentationData.slides?.[slideIndex]
if (!slide) {
throw new Error(`Slide at index ${slideIndex} not found`)
}
// Create requests to add content to the slide
const textBoxId = `textbox_${Date.now()}`
const requests = [
{
createShape: {
objectId: textBoxId,
shapeType: 'TEXT_BOX',
elementProperties: {
pageObjectId: slide.objectId,
size: {
width: {
magnitude: 400,
unit: 'PT',
},
height: {
magnitude: 100,
unit: 'PT',
},
},
transform: {
scaleX: 1,
scaleY: 1,
translateX: 50,
translateY: 100,
unit: 'PT',
},
},
},
},
{
insertText: {
objectId: textBoxId,
text: params.content,
insertionIndex: 0,
},
},
]
// Make the batchUpdate request
const updateResponse = await fetch(
`https://slides.googleapis.com/v1/presentations/${presentationId}:batchUpdate`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ requests }),
}
)
if (!updateResponse.ok) {
const errorText = await updateResponse.text()
logger.error('Failed to update presentation:', { errorText })
throw new Error('Failed to update presentation')
}
// Create presentation metadata
const metadata = {
presentationId,
title: presentationData.title || 'Updated Presentation',
mimeType: 'application/vnd.google-apps.presentation',
url: `https://docs.google.com/presentation/d/${presentationId}/edit`,
}
return {
success: true,
output: {
updatedContent: true,
metadata,
},
}
} catch (error) {
logger.error('Error in postProcess:', { error })
throw error
}
},
outputs: {
updatedContent: {
type: 'boolean',
description: 'Indicates if presentation content was updated successfully',
},
metadata: {
type: 'json',
description: 'Updated presentation metadata including ID, title, and URL',
},
},
transformResponse: async (response: Response) => {
// This is just for the initial read, the actual response comes from postProcess
const data = await response.json()
const metadata = {
presentationId: data.presentationId,
title: data.title || 'Presentation',
mimeType: 'application/vnd.google-apps.presentation',
url: `https://docs.google.com/presentation/d/${data.presentationId}/edit`,
}
return {
success: true,
output: {
updatedContent: false,
metadata,
},
}
},
}

View File

@@ -38,6 +38,13 @@ export const schedulesUpdateTool: ToolConfig<
visibility: 'user-or-llm',
description: 'New timezone for the schedule (e.g., America/New_York)',
},
config: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Schedule configuration as JSON string with rotations. Example: {"rotations": [{"name": "Primary", "users": [{"id": "user_id"}], "handover_start_at": "2024-01-01T09:00:00Z", "handovers": [{"interval": 1, "interval_type": "weekly"}]}]}',
},
},
request: {
@@ -51,6 +58,10 @@ export const schedulesUpdateTool: ToolConfig<
const schedule: Record<string, any> = {}
if (params.name) schedule.name = params.name
if (params.timezone) schedule.timezone = params.timezone
if (params.config) {
schedule.config =
typeof params.config === 'string' ? JSON.parse(params.config) : params.config
}
return { schedule }
},
},

View File

@@ -262,6 +262,16 @@ export interface WorkflowsCreateParams extends IncidentioBaseParams {
name: string
folder?: string
state?: 'active' | 'draft' | 'disabled'
trigger?: string
steps?: string
condition_groups?: string
runs_on_incidents?: 'newly_created' | 'newly_created_and_active' | 'active' | 'all'
runs_on_incident_modes?: string
include_private_incidents?: boolean
continue_on_step_error?: boolean
once_for?: string
expressions?: string
delay?: string
}
export interface WorkflowsCreateResponse extends ToolResponse {
@@ -597,6 +607,7 @@ export interface IncidentioSchedulesUpdateParams extends IncidentioBaseParams {
id: string
name?: string
timezone?: string
config?: string
}
export interface IncidentioSchedulesUpdateResponse extends ToolResponse {

View File

@@ -31,7 +31,83 @@ export const workflowsCreateTool: ToolConfig<WorkflowsCreateParams, WorkflowsCre
required: false,
visibility: 'user-or-llm',
description: 'State of the workflow (active, draft, or disabled)',
default: 'active',
default: 'draft',
},
trigger: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Trigger type for the workflow (e.g., "incident.updated", "incident.created")',
default: 'incident.updated',
},
steps: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Array of workflow steps as JSON string. Example: [{"label": "Notify team", "name": "slack.post_message"}]',
default: '[]',
},
condition_groups: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Array of condition groups as JSON string to control when the workflow runs. Example: [{"conditions": [{"operation": "one_of", "param_bindings": [], "subject": "incident.severity"}]}]',
default: '[]',
},
runs_on_incidents: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'When to run the workflow: "newly_created" (only new incidents), "newly_created_and_active" (new and active incidents), "active" (only active incidents), or "all" (all incidents)',
default: 'newly_created',
},
runs_on_incident_modes: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Array of incident modes to run on as JSON string. Example: ["standard", "retrospective"]',
default: '["standard"]',
},
include_private_incidents: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether to include private incidents',
default: true,
},
continue_on_step_error: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether to continue executing subsequent steps if a step fails',
default: false,
},
once_for: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Array of fields to ensure the workflow runs only once per unique combination of these fields, as JSON string. Example: ["incident.id"]',
default: '[]',
},
expressions: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Array of workflow expressions as JSON string for advanced workflow logic. Example: [{"label": "My expression", "operations": []}]',
default: '[]',
},
delay: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Delay configuration as JSON string. Example: {"for_seconds": 60, "conditions_apply_over_delay": false}',
},
},
@@ -43,18 +119,29 @@ export const workflowsCreateTool: ToolConfig<WorkflowsCreateParams, WorkflowsCre
Authorization: `Bearer ${params.apiKey}`,
}),
body: (params) => {
// Helper function to safely parse JSON strings
const parseJsonParam = (jsonString: string | undefined, defaultValue: any) => {
if (!jsonString) return defaultValue
try {
return JSON.parse(jsonString)
} catch (error) {
console.warn(`Failed to parse JSON parameter: ${jsonString}`, error)
return defaultValue
}
}
// incident.io requires all these fields to create a workflow
const body: Record<string, any> = {
name: params.name,
trigger: 'incident.updated',
once_for: [],
condition_groups: [],
steps: [],
expressions: [],
include_private_incidents: true,
runs_on_incident_modes: ['standard'],
continue_on_step_error: false,
runs_on_incidents: 'newly_created',
trigger: params.trigger || 'incident.updated',
once_for: parseJsonParam(params.once_for, []),
condition_groups: parseJsonParam(params.condition_groups, []),
steps: parseJsonParam(params.steps, []),
expressions: parseJsonParam(params.expressions, []),
include_private_incidents: params.include_private_incidents ?? true,
runs_on_incident_modes: parseJsonParam(params.runs_on_incident_modes, ['standard']),
continue_on_step_error: params.continue_on_step_error ?? false,
runs_on_incidents: params.runs_on_incidents || 'newly_created',
state: params.state || 'draft',
}
@@ -62,6 +149,10 @@ export const workflowsCreateTool: ToolConfig<WorkflowsCreateParams, WorkflowsCre
body.folder = params.folder
}
if (params.delay) {
body.delay = parseJsonParam(params.delay, undefined)
}
return body
},
},

View File

@@ -84,7 +84,8 @@ export const intercomCreateCompanyTool: ToolConfig<
type: 'number',
required: false,
visibility: 'user-only',
description: 'How much revenue the company generates for your business',
description:
'How much revenue the company generates for your business. Note: This field truncates floats to whole integers (e.g., 155.98 becomes 155)',
},
custom_attributes: {
type: 'string',

View File

@@ -29,7 +29,8 @@ export const intercomListCompaniesTool: ToolConfig<
> = {
id: 'intercom_list_companies',
name: 'List Companies from Intercom',
description: 'List all companies from Intercom with pagination support',
description:
'List all companies from Intercom with pagination support. Note: This endpoint has a limit of 10,000 companies that can be returned using pagination. For datasets larger than 10,000 companies, use the Scroll API instead.',
version: '1.0.0',
params: {

View File

@@ -61,9 +61,10 @@ export const intercomReplyConversationTool: ToolConfig<
},
admin_id: {
type: 'string',
required: true,
required: false,
visibility: 'user-only',
description: 'The ID of the admin authoring the reply',
description:
'The ID of the admin authoring the reply. If not provided, a default admin (Operator/Fin) will be used.',
},
attachment_urls: {
type: 'string',

View File

@@ -0,0 +1,163 @@
import type { ToolConfig } from '@/tools/types'
import type { KalshiAuthParams, KalshiOrder } from './types'
import { buildKalshiAuthHeaders, buildKalshiUrl, handleKalshiError } from './types'
export interface KalshiAmendOrderParams extends KalshiAuthParams {
orderId: string // Order ID to amend (required)
ticker: string // Market ticker (required)
side: string // 'yes' or 'no' (required)
action: string // 'buy' or 'sell' (required)
clientOrderId: string // Original client order ID (required)
updatedClientOrderId: string // New client order ID (required)
count?: string // Updated quantity
yesPrice?: string // Updated yes price in cents (1-99)
noPrice?: string // Updated no price in cents (1-99)
yesPriceDollars?: string // Updated yes price in dollars
noPriceDollars?: string // Updated no price in dollars
}
export interface KalshiAmendOrderResponse {
success: boolean
output: {
order: KalshiOrder
metadata: {
operation: 'amend_order'
}
success: boolean
}
}
export const kalshiAmendOrderTool: ToolConfig<KalshiAmendOrderParams, KalshiAmendOrderResponse> = {
id: 'kalshi_amend_order',
name: 'Amend Order on Kalshi',
description: 'Modify the price or quantity of an existing order on Kalshi',
version: '1.0.0',
params: {
keyId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Kalshi API Key ID',
},
privateKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your RSA Private Key (PEM format)',
},
orderId: {
type: 'string',
required: true,
description: 'The order ID to amend',
},
ticker: {
type: 'string',
required: true,
description: 'Market ticker',
},
side: {
type: 'string',
required: true,
description: "Side of the order: 'yes' or 'no'",
},
action: {
type: 'string',
required: true,
description: "Action type: 'buy' or 'sell'",
},
clientOrderId: {
type: 'string',
required: true,
description: 'The original client-specified order ID',
},
updatedClientOrderId: {
type: 'string',
required: true,
description: 'The new client-specified order ID after amendment',
},
count: {
type: 'string',
required: false,
description: 'Updated quantity for the order',
},
yesPrice: {
type: 'string',
required: false,
description: 'Updated yes price in cents (1-99)',
},
noPrice: {
type: 'string',
required: false,
description: 'Updated no price in cents (1-99)',
},
yesPriceDollars: {
type: 'string',
required: false,
description: 'Updated yes price in dollars (e.g., "0.56")',
},
noPriceDollars: {
type: 'string',
required: false,
description: 'Updated no price in dollars (e.g., "0.56")',
},
},
request: {
url: (params) => buildKalshiUrl(`/portfolio/orders/${params.orderId}/amend`),
method: 'POST',
headers: (params) => {
const path = `/trade-api/v2/portfolio/orders/${params.orderId}/amend`
return buildKalshiAuthHeaders(params.keyId, params.privateKey, 'POST', path)
},
body: (params) => {
const body: Record<string, any> = {
ticker: params.ticker,
side: params.side.toLowerCase(),
action: params.action.toLowerCase(),
client_order_id: params.clientOrderId,
updated_client_order_id: params.updatedClientOrderId,
}
if (params.count) body.count = Number.parseInt(params.count, 10)
if (params.yesPrice) body.yes_price = Number.parseInt(params.yesPrice, 10)
if (params.noPrice) body.no_price = Number.parseInt(params.noPrice, 10)
if (params.yesPriceDollars) body.yes_price_dollars = params.yesPriceDollars
if (params.noPriceDollars) body.no_price_dollars = params.noPriceDollars
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
handleKalshiError(data, response.status, 'amend_order')
}
return {
success: true,
output: {
order: data.order,
metadata: {
operation: 'amend_order' as const,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Amended order data',
properties: {
order: { type: 'object', description: 'The amended order object' },
metadata: { type: 'object', description: 'Operation metadata' },
success: { type: 'boolean', description: 'Operation success' },
},
},
},
}

View File

@@ -0,0 +1,90 @@
import type { ToolConfig } from '@/tools/types'
import type { KalshiAuthParams, KalshiOrder } from './types'
import { buildKalshiAuthHeaders, buildKalshiUrl, handleKalshiError } from './types'
export interface KalshiCancelOrderParams extends KalshiAuthParams {
orderId: string // Order ID to cancel (required)
}
export interface KalshiCancelOrderResponse {
success: boolean
output: {
order: KalshiOrder
reducedBy: number
metadata: {
operation: 'cancel_order'
}
success: boolean
}
}
export const kalshiCancelOrderTool: ToolConfig<KalshiCancelOrderParams, KalshiCancelOrderResponse> =
{
id: 'kalshi_cancel_order',
name: 'Cancel Order on Kalshi',
description: 'Cancel an existing order on Kalshi',
version: '1.0.0',
params: {
keyId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Kalshi API Key ID',
},
privateKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your RSA Private Key (PEM format)',
},
orderId: {
type: 'string',
required: true,
description: 'The order ID to cancel',
},
},
request: {
url: (params) => buildKalshiUrl(`/portfolio/orders/${params.orderId}`),
method: 'DELETE',
headers: (params) => {
const path = `/trade-api/v2/portfolio/orders/${params.orderId}`
return buildKalshiAuthHeaders(params.keyId, params.privateKey, 'DELETE', path)
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
handleKalshiError(data, response.status, 'cancel_order')
}
return {
success: true,
output: {
order: data.order,
reducedBy: data.reduced_by || 0,
metadata: {
operation: 'cancel_order' as const,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Canceled order data',
properties: {
order: { type: 'object', description: 'The canceled order object' },
reducedBy: { type: 'number', description: 'Number of contracts canceled' },
metadata: { type: 'object', description: 'Operation metadata' },
success: { type: 'boolean', description: 'Operation success' },
},
},
},
}

View File

@@ -0,0 +1,208 @@
import type { ToolConfig } from '@/tools/types'
import type { KalshiAuthParams, KalshiOrder } from './types'
import { buildKalshiAuthHeaders, buildKalshiUrl, handleKalshiError } from './types'
export interface KalshiCreateOrderParams extends KalshiAuthParams {
ticker: string // Market ticker (required)
side: string // 'yes' or 'no' (required)
action: string // 'buy' or 'sell' (required)
count: string // Number of contracts (required)
type?: string // 'limit' or 'market' (default: limit)
yesPrice?: string // Yes price in cents (1-99)
noPrice?: string // No price in cents (1-99)
yesPriceDollars?: string // Yes price in dollars (e.g., "0.56")
noPriceDollars?: string // No price in dollars (e.g., "0.56")
clientOrderId?: string // Custom order identifier
expirationTs?: string // Unix timestamp expiration
timeInForce?: string // 'fill_or_kill', 'good_till_canceled', 'immediate_or_cancel'
buyMaxCost?: string // Maximum cost in cents
postOnly?: string // 'true' or 'false' - maker-only orders
reduceOnly?: string // 'true' or 'false' - position reduction only
selfTradePreventionType?: string // 'taker_at_cross' or 'maker'
orderGroupId?: string // Associated order group
}
export interface KalshiCreateOrderResponse {
success: boolean
output: {
order: KalshiOrder
metadata: {
operation: 'create_order'
}
success: boolean
}
}
export const kalshiCreateOrderTool: ToolConfig<KalshiCreateOrderParams, KalshiCreateOrderResponse> =
{
id: 'kalshi_create_order',
name: 'Create Order on Kalshi',
description: 'Create a new order on a Kalshi prediction market',
version: '1.0.0',
params: {
keyId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Kalshi API Key ID',
},
privateKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your RSA Private Key (PEM format)',
},
ticker: {
type: 'string',
required: true,
description: 'Market ticker (e.g., KXBTC-24DEC31)',
},
side: {
type: 'string',
required: true,
description: "Side of the order: 'yes' or 'no'",
},
action: {
type: 'string',
required: true,
description: "Action type: 'buy' or 'sell'",
},
count: {
type: 'string',
required: true,
description: 'Number of contracts (minimum 1)',
},
type: {
type: 'string',
required: false,
description: "Order type: 'limit' or 'market' (default: limit)",
},
yesPrice: {
type: 'string',
required: false,
description: 'Yes price in cents (1-99)',
},
noPrice: {
type: 'string',
required: false,
description: 'No price in cents (1-99)',
},
yesPriceDollars: {
type: 'string',
required: false,
description: 'Yes price in dollars (e.g., "0.56")',
},
noPriceDollars: {
type: 'string',
required: false,
description: 'No price in dollars (e.g., "0.56")',
},
clientOrderId: {
type: 'string',
required: false,
description: 'Custom order identifier',
},
expirationTs: {
type: 'string',
required: false,
description: 'Unix timestamp for order expiration',
},
timeInForce: {
type: 'string',
required: false,
description: "Time in force: 'fill_or_kill', 'good_till_canceled', 'immediate_or_cancel'",
},
buyMaxCost: {
type: 'string',
required: false,
description: 'Maximum cost in cents (auto-enables fill_or_kill)',
},
postOnly: {
type: 'string',
required: false,
description: "Set to 'true' for maker-only orders",
},
reduceOnly: {
type: 'string',
required: false,
description: "Set to 'true' for position reduction only",
},
selfTradePreventionType: {
type: 'string',
required: false,
description: "Self-trade prevention: 'taker_at_cross' or 'maker'",
},
orderGroupId: {
type: 'string',
required: false,
description: 'Associated order group ID',
},
},
request: {
url: () => buildKalshiUrl('/portfolio/orders'),
method: 'POST',
headers: (params) => {
const path = '/trade-api/v2/portfolio/orders'
return buildKalshiAuthHeaders(params.keyId, params.privateKey, 'POST', path)
},
body: (params) => {
const body: Record<string, any> = {
ticker: params.ticker,
side: params.side.toLowerCase(),
action: params.action.toLowerCase(),
count: Number.parseInt(params.count, 10),
}
if (params.type) body.type = params.type.toLowerCase()
if (params.yesPrice) body.yes_price = Number.parseInt(params.yesPrice, 10)
if (params.noPrice) body.no_price = Number.parseInt(params.noPrice, 10)
if (params.yesPriceDollars) body.yes_price_dollars = params.yesPriceDollars
if (params.noPriceDollars) body.no_price_dollars = params.noPriceDollars
if (params.clientOrderId) body.client_order_id = params.clientOrderId
if (params.expirationTs) body.expiration_ts = Number.parseInt(params.expirationTs, 10)
if (params.timeInForce) body.time_in_force = params.timeInForce
if (params.buyMaxCost) body.buy_max_cost = Number.parseInt(params.buyMaxCost, 10)
if (params.postOnly) body.post_only = params.postOnly === 'true'
if (params.reduceOnly) body.reduce_only = params.reduceOnly === 'true'
if (params.selfTradePreventionType)
body.self_trade_prevention_type = params.selfTradePreventionType
if (params.orderGroupId) body.order_group_id = params.orderGroupId
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
handleKalshiError(data, response.status, 'create_order')
}
return {
success: true,
output: {
order: data.order,
metadata: {
operation: 'create_order' as const,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Created order data',
properties: {
order: { type: 'object', description: 'The created order object' },
metadata: { type: 'object', description: 'Operation metadata' },
success: { type: 'boolean', description: 'Operation success' },
},
},
},
}

View File

@@ -28,11 +28,13 @@ export const kalshiGetBalanceTool: ToolConfig<KalshiGetBalanceParams, KalshiGetB
keyId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Kalshi API Key ID',
},
privateKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your RSA Private Key (PEM format)',
},
},

View File

@@ -5,9 +5,9 @@ import { buildKalshiUrl, handleKalshiError } from './types'
export interface KalshiGetCandlesticksParams {
seriesTicker: string
ticker: string
startTs?: number
endTs?: number
periodInterval?: number // 1, 60, or 1440 (1min, 1hour, 1day)
startTs: number
endTs: number
periodInterval: number // 1, 60, or 1440 (1min, 1hour, 1day)
}
export interface KalshiGetCandlesticksResponse {
@@ -46,17 +46,17 @@ export const kalshiGetCandlesticksTool: ToolConfig<
},
startTs: {
type: 'number',
required: false,
description: 'Start timestamp (Unix milliseconds)',
required: true,
description: 'Start timestamp (Unix seconds)',
},
endTs: {
type: 'number',
required: false,
description: 'End timestamp (Unix milliseconds)',
required: true,
description: 'End timestamp (Unix seconds)',
},
periodInterval: {
type: 'number',
required: false,
required: true,
description: 'Period interval: 1 (1min), 60 (1hour), or 1440 (1day)',
},
},
@@ -64,16 +64,15 @@ export const kalshiGetCandlesticksTool: ToolConfig<
request: {
url: (params) => {
const queryParams = new URLSearchParams()
if (params.startTs !== undefined) queryParams.append('start_ts', params.startTs.toString())
if (params.endTs !== undefined) queryParams.append('end_ts', params.endTs.toString())
if (params.periodInterval !== undefined)
queryParams.append('period_interval', params.periodInterval.toString())
queryParams.append('start_ts', params.startTs.toString())
queryParams.append('end_ts', params.endTs.toString())
queryParams.append('period_interval', params.periodInterval.toString())
const query = queryParams.toString()
const url = buildKalshiUrl(
`/series/${params.seriesTicker}/markets/${params.ticker}/candlesticks`
)
return query ? `${url}?${query}` : url
return `${url}?${query}`
},
method: 'GET',
headers: () => ({

View File

@@ -37,11 +37,13 @@ export const kalshiGetFillsTool: ToolConfig<KalshiGetFillsParams, KalshiGetFills
keyId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Kalshi API Key ID',
},
privateKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your RSA Private Key (PEM format)',
},
ticker: {

View File

@@ -0,0 +1,86 @@
import type { ToolConfig } from '@/tools/types'
import type { KalshiAuthParams, KalshiOrder } from './types'
import { buildKalshiAuthHeaders, buildKalshiUrl, handleKalshiError } from './types'
export interface KalshiGetOrderParams extends KalshiAuthParams {
orderId: string // Order ID to retrieve (required)
}
export interface KalshiGetOrderResponse {
success: boolean
output: {
order: KalshiOrder
metadata: {
operation: 'get_order'
}
success: boolean
}
}
export const kalshiGetOrderTool: ToolConfig<KalshiGetOrderParams, KalshiGetOrderResponse> = {
id: 'kalshi_get_order',
name: 'Get Order from Kalshi',
description: 'Retrieve details of a specific order by ID from Kalshi',
version: '1.0.0',
params: {
keyId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Kalshi API Key ID',
},
privateKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your RSA Private Key (PEM format)',
},
orderId: {
type: 'string',
required: true,
description: 'The order ID to retrieve',
},
},
request: {
url: (params) => buildKalshiUrl(`/portfolio/orders/${params.orderId}`),
method: 'GET',
headers: (params) => {
const path = `/trade-api/v2/portfolio/orders/${params.orderId}`
return buildKalshiAuthHeaders(params.keyId, params.privateKey, 'GET', path)
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
handleKalshiError(data, response.status, 'get_order')
}
return {
success: true,
output: {
order: data.order,
metadata: {
operation: 'get_order' as const,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Order data',
properties: {
order: { type: 'object', description: 'The order object' },
metadata: { type: 'object', description: 'Operation metadata' },
success: { type: 'boolean', description: 'Operation success' },
},
},
},
}

View File

@@ -4,7 +4,6 @@ import { buildKalshiUrl, handleKalshiError } from './types'
export interface KalshiGetOrderbookParams {
ticker: string
depth?: number
}
export interface KalshiGetOrderbookResponse {
@@ -25,7 +24,7 @@ export const kalshiGetOrderbookTool: ToolConfig<
> = {
id: 'kalshi_get_orderbook',
name: 'Get Market Orderbook from Kalshi',
description: 'Retrieve the orderbook (bids and asks) for a specific market',
description: 'Retrieve the orderbook (yes and no bids) for a specific market',
version: '1.0.0',
params: {
@@ -34,22 +33,10 @@ export const kalshiGetOrderbookTool: ToolConfig<
required: true,
description: 'Market ticker (e.g., KXBTC-24DEC31)',
},
depth: {
type: 'number',
required: false,
description: 'Number of price levels to return per side',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams()
if (params.depth !== undefined) queryParams.append('depth', params.depth.toString())
const query = queryParams.toString()
const url = buildKalshiUrl(`/markets/${params.ticker}/orderbook`)
return query ? `${url}?${query}` : url
},
url: (params) => buildKalshiUrl(`/markets/${params.ticker}/orderbook`),
method: 'GET',
headers: () => ({
'Content-Type': 'application/json',

View File

@@ -36,11 +36,13 @@ export const kalshiGetOrdersTool: ToolConfig<KalshiGetOrdersParams, KalshiGetOrd
keyId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Kalshi API Key ID',
},
privateKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your RSA Private Key (PEM format)',
},
ticker: {

View File

@@ -39,11 +39,13 @@ export const kalshiGetPositionsTool: ToolConfig<
keyId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Kalshi API Key ID',
},
privateKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your RSA Private Key (PEM format)',
},
ticker: {

View File

@@ -2,11 +2,7 @@ import type { ToolConfig } from '@/tools/types'
import type { KalshiPaginationParams, KalshiPagingInfo, KalshiTrade } from './types'
import { buildKalshiUrl, handleKalshiError } from './types'
export interface KalshiGetTradesParams extends KalshiPaginationParams {
ticker?: string
minTs?: number
maxTs?: number
}
export interface KalshiGetTradesParams extends KalshiPaginationParams {}
export interface KalshiGetTradesResponse {
success: boolean
@@ -24,25 +20,10 @@ export interface KalshiGetTradesResponse {
export const kalshiGetTradesTool: ToolConfig<KalshiGetTradesParams, KalshiGetTradesResponse> = {
id: 'kalshi_get_trades',
name: 'Get Trades from Kalshi',
description: 'Retrieve recent trades across all markets or for a specific market',
description: 'Retrieve recent trades across all markets',
version: '1.0.0',
params: {
ticker: {
type: 'string',
required: false,
description: 'Filter by market ticker',
},
minTs: {
type: 'number',
required: false,
description: 'Minimum timestamp (Unix milliseconds)',
},
maxTs: {
type: 'number',
required: false,
description: 'Maximum timestamp (Unix milliseconds)',
},
limit: {
type: 'string',
required: false,
@@ -58,9 +39,6 @@ export const kalshiGetTradesTool: ToolConfig<KalshiGetTradesParams, KalshiGetTra
request: {
url: (params) => {
const queryParams = new URLSearchParams()
if (params.ticker) queryParams.append('ticker', params.ticker)
if (params.minTs !== undefined) queryParams.append('min_ts', params.minTs.toString())
if (params.maxTs !== undefined) queryParams.append('max_ts', params.maxTs.toString())
if (params.limit) queryParams.append('limit', params.limit)
if (params.cursor) queryParams.append('cursor', params.cursor)

View File

@@ -1,3 +1,6 @@
export { kalshiAmendOrderTool } from './amend_order'
export { kalshiCancelOrderTool } from './cancel_order'
export { kalshiCreateOrderTool } from './create_order'
export { kalshiGetBalanceTool } from './get_balance'
export { kalshiGetCandlesticksTool } from './get_candlesticks'
export { kalshiGetEventTool } from './get_event'
@@ -6,6 +9,7 @@ export { kalshiGetExchangeStatusTool } from './get_exchange_status'
export { kalshiGetFillsTool } from './get_fills'
export { kalshiGetMarketTool } from './get_market'
export { kalshiGetMarketsTool } from './get_markets'
export { kalshiGetOrderTool } from './get_order'
export { kalshiGetOrderbookTool } from './get_orderbook'
export { kalshiGetOrdersTool } from './get_orders'
export { kalshiGetPositionsTool } from './get_positions'

View File

@@ -4,7 +4,7 @@ import { buildGammaUrl, handlePolymarketError } from './types'
export interface PolymarketGetEventsParams extends PolymarketPaginationParams {
closed?: string // 'true' or 'false' - filter for closed/active events
order?: string // sort field
order?: string // sort field (e.g., 'volume', 'liquidity', 'startDate', 'endDate')
ascending?: string // 'true' or 'false' - sort direction
tagId?: string // filter by tag ID
}
@@ -39,7 +39,7 @@ export const polymarketGetEventsTool: ToolConfig<
order: {
type: 'string',
required: false,
description: 'Sort field (e.g., id, volume)',
description: 'Sort field (e.g., volume, liquidity, startDate, endDate, createdAt)',
},
ascending: {
type: 'string',

View File

@@ -4,7 +4,7 @@ import { buildGammaUrl, handlePolymarketError } from './types'
export interface PolymarketGetMarketsParams extends PolymarketPaginationParams {
closed?: string // 'true' or 'false' - filter for closed/active markets
order?: string // sort field (e.g., 'id', 'volume', 'liquidity')
order?: string // sort field - use camelCase (e.g., 'volumeNum', 'liquidityNum', 'startDate', 'endDate')
ascending?: string // 'true' or 'false' - sort direction
tagId?: string // filter by tag ID
}
@@ -39,7 +39,7 @@ export const polymarketGetMarketsTool: ToolConfig<
order: {
type: 'string',
required: false,
description: 'Sort field (e.g., id, volume, liquidity)',
description: 'Sort field (e.g., volumeNum, liquidityNum, startDate, endDate, createdAt)',
},
ascending: {
type: 'string',

View File

@@ -295,6 +295,15 @@ import {
googleSheetsUpdateTool,
googleSheetsWriteTool,
} from '@/tools/google_sheets'
import {
googleSlidesAddImageTool,
googleSlidesAddSlideTool,
googleSlidesCreateTool,
googleSlidesGetThumbnailTool,
googleSlidesReadTool,
googleSlidesReplaceAllTextTool,
googleSlidesWriteTool,
} from '@/tools/google_slides'
import {
createMattersExportTool,
createMattersHoldsTool,
@@ -441,6 +450,9 @@ import {
jiraWriteTool,
} from '@/tools/jira'
import {
kalshiAmendOrderTool,
kalshiCancelOrderTool,
kalshiCreateOrderTool,
kalshiGetBalanceTool,
kalshiGetCandlesticksTool,
kalshiGetEventsTool,
@@ -451,6 +463,7 @@ import {
kalshiGetMarketTool,
kalshiGetOrderbookTool,
kalshiGetOrdersTool,
kalshiGetOrderTool,
kalshiGetPositionsTool,
kalshiGetSeriesByTickerTool,
kalshiGetTradesTool,
@@ -1376,12 +1389,16 @@ export const tools: Record<string, ToolConfig> = {
kalshi_get_balance: kalshiGetBalanceTool,
kalshi_get_positions: kalshiGetPositionsTool,
kalshi_get_orders: kalshiGetOrdersTool,
kalshi_get_order: kalshiGetOrderTool,
kalshi_get_orderbook: kalshiGetOrderbookTool,
kalshi_get_trades: kalshiGetTradesTool,
kalshi_get_candlesticks: kalshiGetCandlesticksTool,
kalshi_get_fills: kalshiGetFillsTool,
kalshi_get_series_by_ticker: kalshiGetSeriesByTickerTool,
kalshi_get_exchange_status: kalshiGetExchangeStatusTool,
kalshi_create_order: kalshiCreateOrderTool,
kalshi_cancel_order: kalshiCancelOrderTool,
kalshi_amend_order: kalshiAmendOrderTool,
polymarket_get_markets: polymarketGetMarketsTool,
polymarket_get_market: polymarketGetMarketTool,
polymarket_get_events: polymarketGetEventsTool,
@@ -1679,6 +1696,13 @@ export const tools: Record<string, ToolConfig> = {
google_sheets_write: googleSheetsWriteTool,
google_sheets_update: googleSheetsUpdateTool,
google_sheets_append: googleSheetsAppendTool,
google_slides_read: googleSlidesReadTool,
google_slides_write: googleSlidesWriteTool,
google_slides_create: googleSlidesCreateTool,
google_slides_replace_all_text: googleSlidesReplaceAllTextTool,
google_slides_add_slide: googleSlidesAddSlideTool,
google_slides_get_thumbnail: googleSlidesGetThumbnailTool,
google_slides_add_image: googleSlidesAddImageTool,
perplexity_chat: perplexityChatTool,
perplexity_search: perplexitySearchTool,
posthog_capture_event: posthogCaptureEventTool,

View File

@@ -1,5 +1,6 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "simstudio",
@@ -113,6 +114,7 @@
"@react-email/components": "^0.0.34",
"@react-email/render": "2.0.0",
"@trigger.dev/sdk": "4.1.2",
"@types/react-window": "2.0.0",
"@types/three": "0.177.0",
"better-auth": "1.3.12",
"browser-image-compression": "^2.0.2",
@@ -160,6 +162,7 @@
"react-hook-form": "^7.54.2",
"react-markdown": "^10.1.0",
"react-simple-code-editor": "^0.14.1",
"react-window": "2.2.3",
"reactflow": "^11.11.4",
"rehype-autolink-headings": "^7.1.0",
"rehype-slug": "^6.0.0",
@@ -1423,6 +1426,8 @@
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@types/react-window": ["@types/react-window@2.0.0", "", { "dependencies": { "react-window": "*" } }, "sha512-E8hMDtImEpMk1SjswSvqoSmYvk7GEtyVaTa/GJV++FdDNuMVVEzpAClyJ0nqeKYBrMkGiyH6M1+rPLM0Nu1exQ=="],
"@types/shimmer": ["@types/shimmer@1.2.0", "", {}, "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg=="],
"@types/ssh2": ["@types/ssh2@1.15.5", "", { "dependencies": { "@types/node": "^18.11.18" } }, "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ=="],
@@ -2809,6 +2814,8 @@
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
"react-window": ["react-window@2.2.3", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-gTRqQYC8ojbiXyd9duYFiSn2TJw0ROXCgYjenOvNKITWzK0m0eCvkUsEUM08xvydkMh7ncp+LE0uS3DeNGZxnQ=="],
"reactflow": ["reactflow@11.11.4", "", { "dependencies": { "@reactflow/background": "11.3.14", "@reactflow/controls": "11.2.14", "@reactflow/core": "11.11.4", "@reactflow/minimap": "11.7.14", "@reactflow/node-resizer": "2.2.14", "@reactflow/node-toolbar": "1.3.14" }, "peerDependencies": { "react": ">=17", "react-dom": ">=17" } }, "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og=="],
"read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="],