chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
+188
View File
@@ -0,0 +1,188 @@
import type {
GoogleForm,
GoogleFormsBatchUpdateParams,
GoogleFormsBatchUpdateResponse,
} from '@/tools/google_forms/types'
import { buildBatchUpdateUrl, getGoogleFormsErrorMessage } from '@/tools/google_forms/utils'
import type { ToolConfig } from '@/tools/types'
interface BatchUpdateApiResponse {
replies?: Record<string, unknown>[]
writeControl?: {
requiredRevisionId?: string
targetRevisionId?: string
}
form?: GoogleForm
}
export const batchUpdateTool: ToolConfig<
GoogleFormsBatchUpdateParams,
GoogleFormsBatchUpdateResponse
> = {
id: 'google_forms_batch_update',
name: 'Google Forms: Batch Update',
description: 'Apply multiple updates to a form (add items, update info, change settings, etc.)',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-forms',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token',
},
formId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Forms form ID',
},
requests: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'Array of update requests (updateFormInfo, updateSettings, createItem, updateItem, moveItem, deleteItem)',
},
includeFormInResponse: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether to return the updated form in the response',
},
},
request: {
url: (params: GoogleFormsBatchUpdateParams) => buildBatchUpdateUrl(params.formId),
method: 'POST',
headers: (params: GoogleFormsBatchUpdateParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params: GoogleFormsBatchUpdateParams) => ({
requests: params.requests,
includeFormInResponse: params.includeFormInResponse ?? false,
}),
},
transformResponse: async (response: Response) => {
const data = (await response.json()) as BatchUpdateApiResponse
if (!response.ok) {
return {
success: false,
output: {
replies: [],
writeControl: null,
form: null,
},
error: getGoogleFormsErrorMessage(data, 'Failed to batch update form'),
}
}
return {
success: true,
output: {
replies: data.replies ?? [],
writeControl: data.writeControl ?? null,
form: data.form ?? null,
},
}
},
outputs: {
replies: {
type: 'array',
description: 'The replies from each update request',
items: {
type: 'json',
},
},
writeControl: {
type: 'object',
description: 'Write control information with revision IDs',
optional: true,
properties: {
requiredRevisionId: {
type: 'string',
description: 'Required revision ID for conflict detection',
},
targetRevisionId: { type: 'string', description: 'Target revision ID' },
},
},
form: {
type: 'object',
description: 'The updated form (if includeFormInResponse was true)',
optional: true,
properties: {
formId: { type: 'string', description: 'The form ID' },
info: {
type: 'object',
description: 'Form info containing title and description',
properties: {
title: { type: 'string', description: 'The form title visible to responders' },
description: { type: 'string', description: 'The form description' },
documentTitle: { type: 'string', description: 'The document title visible in Drive' },
},
},
settings: {
type: 'object',
description: 'Form settings',
properties: {
quizSettings: {
type: 'object',
description: 'Quiz settings',
properties: {
isQuiz: { type: 'boolean', description: 'Whether the form is a quiz' },
},
},
emailCollectionType: { type: 'string', description: 'Email collection type' },
},
},
items: {
type: 'array',
description: 'The form items (questions, sections, etc.)',
items: {
type: 'object',
properties: {
itemId: { type: 'string', description: 'Item ID' },
title: { type: 'string', description: 'Item title' },
description: { type: 'string', description: 'Item description' },
questionItem: { type: 'json', description: 'Question item configuration' },
questionGroupItem: { type: 'json', description: 'Question group configuration' },
pageBreakItem: { type: 'json', description: 'Page break configuration' },
textItem: { type: 'json', description: 'Text item configuration' },
imageItem: { type: 'json', description: 'Image item configuration' },
videoItem: { type: 'json', description: 'Video item configuration' },
},
},
},
revisionId: { type: 'string', description: 'The revision ID of the form' },
responderUri: { type: 'string', description: 'The URI to share with responders' },
linkedSheetId: { type: 'string', description: 'The ID of the linked Google Sheet' },
publishSettings: {
type: 'object',
description: 'Form publish settings',
properties: {
publishState: {
type: 'object',
description: 'Current publish state',
properties: {
isPublished: { type: 'boolean', description: 'Whether the form is published' },
isAcceptingResponses: {
type: 'boolean',
description: 'Whether the form is accepting responses',
},
},
},
},
},
},
},
},
}
+105
View File
@@ -0,0 +1,105 @@
import type {
GoogleForm,
GoogleFormsCreateFormParams,
GoogleFormsCreateFormResponse,
} from '@/tools/google_forms/types'
import { buildCreateFormUrl, getGoogleFormsErrorMessage } from '@/tools/google_forms/utils'
import type { ToolConfig } from '@/tools/types'
export const createFormTool: ToolConfig<
GoogleFormsCreateFormParams,
GoogleFormsCreateFormResponse
> = {
id: 'google_forms_create_form',
name: 'Google Forms: Create Form',
description: 'Create a new Google Form with a title',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-forms',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token',
},
title: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The title of the form visible to responders',
},
documentTitle: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'The document title visible in Drive (defaults to form title)',
},
unpublished: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'If true, create an unpublished form that does not accept responses',
},
},
request: {
url: (params: GoogleFormsCreateFormParams) => buildCreateFormUrl(params.unpublished),
method: 'POST',
headers: (params: GoogleFormsCreateFormParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params: GoogleFormsCreateFormParams) => ({
info: {
title: params.title,
...(params.documentTitle ? { documentTitle: params.documentTitle } : {}),
},
}),
},
transformResponse: async (response: Response) => {
const data = (await response.json()) as GoogleForm
if (!response.ok) {
return {
success: false,
output: {
formId: '',
title: null,
documentTitle: null,
responderUri: null,
revisionId: null,
},
error: getGoogleFormsErrorMessage(data, 'Failed to create form'),
}
}
return {
success: true,
output: {
formId: data.formId ?? '',
title: data.info?.title ?? null,
documentTitle: data.info?.documentTitle ?? null,
responderUri: data.responderUri ?? null,
revisionId: data.revisionId ?? null,
},
}
},
outputs: {
formId: { type: 'string', description: 'The ID of the created form' },
title: { type: 'string', description: 'The form title', optional: true },
documentTitle: { type: 'string', description: 'The document title in Drive', optional: true },
responderUri: {
type: 'string',
description: 'The URI to share with responders',
optional: true,
},
revisionId: { type: 'string', description: 'The revision ID of the form', optional: true },
},
}
+119
View File
@@ -0,0 +1,119 @@
import type {
GoogleFormsCreateWatchParams,
GoogleFormsCreateWatchResponse,
GoogleFormsWatch,
} from '@/tools/google_forms/types'
import { buildCreateWatchUrl, getGoogleFormsErrorMessage } from '@/tools/google_forms/utils'
import type { ToolConfig } from '@/tools/types'
export const createWatchTool: ToolConfig<
GoogleFormsCreateWatchParams,
GoogleFormsCreateWatchResponse
> = {
id: 'google_forms_create_watch',
name: 'Google Forms: Create Watch',
description: 'Create a notification watch for form changes (schema changes or new responses)',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-forms',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token',
},
formId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Forms form ID to watch',
},
eventType: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Event type to watch: SCHEMA (form changes) or RESPONSES (new submissions)',
},
topicName: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The Cloud Pub/Sub topic name (format: projects/{project}/topics/{topic})',
},
watchId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Custom watch ID (4-63 chars, lowercase letters, numbers, hyphens)',
},
},
request: {
url: (params: GoogleFormsCreateWatchParams) => buildCreateWatchUrl(params.formId),
method: 'POST',
headers: (params: GoogleFormsCreateWatchParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params: GoogleFormsCreateWatchParams) => ({
watch: {
target: {
topic: {
topicName: params.topicName,
},
},
eventType: params.eventType,
},
...(params.watchId ? { watchId: params.watchId } : {}),
}),
},
transformResponse: async (response: Response) => {
const data = (await response.json()) as GoogleFormsWatch
if (!response.ok) {
return {
success: false,
output: {
id: '',
eventType: '',
topicName: null,
createTime: null,
expireTime: null,
state: null,
},
error: getGoogleFormsErrorMessage(data, 'Failed to create watch'),
}
}
return {
success: true,
output: {
id: data.id ?? '',
eventType: data.eventType ?? '',
topicName: data.target?.topic?.topicName ?? null,
createTime: data.createTime ?? null,
expireTime: data.expireTime ?? null,
state: data.state ?? null,
},
}
},
outputs: {
id: { type: 'string', description: 'The watch ID' },
eventType: { type: 'string', description: 'The event type being watched' },
topicName: { type: 'string', description: 'The Cloud Pub/Sub topic', optional: true },
createTime: { type: 'string', description: 'When the watch was created', optional: true },
expireTime: {
type: 'string',
description: 'When the watch expires (7 days after creation)',
optional: true,
},
state: { type: 'string', description: 'The watch state (ACTIVE, SUSPENDED)', optional: true },
},
}
@@ -0,0 +1,76 @@
import type {
GoogleFormsDeleteWatchParams,
GoogleFormsDeleteWatchResponse,
} from '@/tools/google_forms/types'
import { buildDeleteWatchUrl } from '@/tools/google_forms/utils'
import type { ToolConfig } from '@/tools/types'
export const deleteWatchTool: ToolConfig<
GoogleFormsDeleteWatchParams,
GoogleFormsDeleteWatchResponse
> = {
id: 'google_forms_delete_watch',
name: 'Google Forms: Delete Watch',
description: 'Delete a notification watch from a form',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-forms',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token',
},
formId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Forms form ID',
},
watchId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Watch ID to delete',
},
},
request: {
url: (params: GoogleFormsDeleteWatchParams) =>
buildDeleteWatchUrl(params.formId, params.watchId),
method: 'DELETE',
headers: (params: GoogleFormsDeleteWatchParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const data = (await response.json()) as { error?: { message?: string } }
return {
success: false,
output: {
deleted: false,
},
error: data.error?.message ?? 'Failed to delete watch',
}
}
return {
success: true,
output: {
deleted: true,
},
}
},
outputs: {
deleted: { type: 'boolean', description: 'Whether the watch was successfully deleted' },
},
}
+118
View File
@@ -0,0 +1,118 @@
import type {
GoogleForm,
GoogleFormsGetFormParams,
GoogleFormsGetFormResponse,
} from '@/tools/google_forms/types'
import { buildGetFormUrl, getGoogleFormsErrorMessage } from '@/tools/google_forms/utils'
import type { ToolConfig } from '@/tools/types'
export const getFormTool: ToolConfig<GoogleFormsGetFormParams, GoogleFormsGetFormResponse> = {
id: 'google_forms_get_form',
name: 'Google Forms: Get Form',
description: 'Retrieve a form structure including its items, settings, and metadata',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-forms',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token',
},
formId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Forms form ID to retrieve',
},
},
request: {
url: (params: GoogleFormsGetFormParams) => buildGetFormUrl(params.formId),
method: 'GET',
headers: (params: GoogleFormsGetFormParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = (await response.json()) as GoogleForm
if (!response.ok) {
return {
success: false,
output: {
formId: '',
title: null,
description: null,
documentTitle: null,
responderUri: null,
linkedSheetId: null,
revisionId: null,
items: [],
settings: null,
publishSettings: null,
},
error: getGoogleFormsErrorMessage(data, 'Failed to get form'),
}
}
return {
success: true,
output: {
formId: data.formId ?? '',
title: data.info?.title ?? null,
description: data.info?.description ?? null,
documentTitle: data.info?.documentTitle ?? null,
responderUri: data.responderUri ?? null,
linkedSheetId: data.linkedSheetId ?? null,
revisionId: data.revisionId ?? null,
items: data.items ?? [],
settings: data.settings ?? null,
publishSettings: data.publishSettings ?? null,
},
}
},
outputs: {
formId: { type: 'string', description: 'The form ID' },
title: { type: 'string', description: 'The form title visible to responders', optional: true },
description: { type: 'string', description: 'The form description', optional: true },
documentTitle: {
type: 'string',
description: 'The document title visible in Drive',
optional: true,
},
responderUri: {
type: 'string',
description: 'The URI to share with responders',
optional: true,
},
linkedSheetId: {
type: 'string',
description: 'The ID of the linked Google Sheet',
optional: true,
},
revisionId: { type: 'string', description: 'The revision ID of the form', optional: true },
items: {
type: 'array',
description: 'The form items (questions, sections, etc.)',
items: {
type: 'object',
properties: {
itemId: { type: 'string', description: 'Item ID' },
title: { type: 'string', description: 'Item title' },
description: { type: 'string', description: 'Item description' },
},
},
},
settings: { type: 'json', description: 'Form settings', optional: true },
publishSettings: { type: 'json', description: 'Form publish settings', optional: true },
},
}
@@ -0,0 +1,230 @@
import type {
GoogleFormsGetResponsesParams,
GoogleFormsResponse,
GoogleFormsResponseList,
} from '@/tools/google_forms/types'
import { buildGetResponseUrl, buildListResponsesUrl } from '@/tools/google_forms/utils'
import type { ToolConfig } from '@/tools/types'
export const getResponsesTool: ToolConfig<GoogleFormsGetResponsesParams> = {
id: 'google_forms_get_responses',
name: 'Google Forms: Get Responses',
description: 'Retrieve a single response or list responses from a Google Form',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-forms',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth2 access token',
},
formId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Forms form ID',
},
responseId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Response ID - if provided, returns this specific response',
},
pageSize: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description:
'Maximum number of responses to return (service may return fewer). Defaults to 5000.',
},
pageToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Page token from a previous list response to fetch the next page of responses',
},
filter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Filter responses, e.g. "timestamp > 2024-01-01T00:00:00Z" (RFC3339 UTC). Only timestamp filters are supported.',
},
},
request: {
url: (params: GoogleFormsGetResponsesParams) =>
params.responseId
? buildGetResponseUrl({ formId: params.formId, responseId: params.responseId })
: buildListResponsesUrl({
formId: params.formId,
pageSize: params.pageSize ? Number(params.pageSize) : undefined,
pageToken: params.pageToken,
filter: params.filter,
}),
method: 'GET',
headers: (params: GoogleFormsGetResponsesParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response, params?: GoogleFormsGetResponsesParams) => {
const data = (await response.json()) as unknown
if (!response.ok) {
let errorMessage = response.statusText || 'Failed to fetch responses'
if (data && typeof data === 'object') {
const record = data as Record<string, unknown>
const error = record.error as { message?: string } | undefined
if (error?.message) {
errorMessage = error.message
}
}
return {
success: false,
output: (data as Record<string, unknown>) || {},
error: errorMessage,
}
}
// Normalize answers into a flat key/value map per response
const normalizeAnswerContainer = (container: unknown): unknown => {
if (!container || typeof container !== 'object') return container
const record = container as Record<string, unknown>
const answers = record.answers as unknown[] | undefined
if (Array.isArray(answers)) {
const values = answers.map((entry) => {
if (entry && typeof entry === 'object') {
const er = entry as Record<string, unknown>
if (typeof er.value !== 'undefined') return er.value
}
return entry
})
return values.length === 1 ? values[0] : values
}
return container
}
const normalizeAnswers = (answers: unknown): Record<string, unknown> => {
if (!answers || typeof answers !== 'object') return {}
const src = answers as Record<string, unknown>
const out: Record<string, unknown> = {}
for (const [questionId, answerObj] of Object.entries(src)) {
if (answerObj && typeof answerObj === 'object') {
const aRec = answerObj as Record<string, unknown>
// Find first *Answers property that contains an answers array
const key = Object.keys(aRec).find(
(k) => k.toLowerCase().endsWith('answers') && Array.isArray((aRec[k] as any)?.answers)
)
if (key) {
out[questionId] = normalizeAnswerContainer(aRec[key])
continue
}
}
out[questionId] = answerObj as unknown
}
return out
}
const normalizeResponse = (r: GoogleFormsResponse): Record<string, unknown> => ({
responseId: r.responseId,
createTime: r.createTime,
lastSubmittedTime: r.lastSubmittedTime,
answers: normalizeAnswers(r.answers as unknown),
})
// Distinguish single vs list response shapes
const isList = (obj: unknown): obj is GoogleFormsResponseList =>
!!obj && typeof obj === 'object' && Array.isArray((obj as GoogleFormsResponseList).responses)
if (isList(data)) {
const listData = data as GoogleFormsResponseList
const toTimestamp = (s?: string): number => {
if (!s) return 0
const t = Date.parse(s)
return Number.isNaN(t) ? 0 : t
}
const sorted = (listData.responses || [])
.slice()
.sort(
(a, b) =>
toTimestamp(b.lastSubmittedTime || b.createTime) -
toTimestamp(a.lastSubmittedTime || a.createTime)
)
const normalized = sorted.map((r) => normalizeResponse(r))
const output: Record<string, unknown> = {
responses: normalized,
nextPageToken: listData.nextPageToken ?? null,
raw: listData,
}
return {
success: true,
output,
}
}
const single = data as GoogleFormsResponse
const normalizedSingle = normalizeResponse(single)
const output: Record<string, unknown> = {
response: normalizedSingle,
raw: single,
}
return {
success: true,
output,
}
},
outputs: {
responses: {
type: 'array',
description: 'Array of form responses (when no responseId provided)',
items: {
type: 'object',
properties: {
responseId: { type: 'string', description: 'Unique response ID' },
createTime: { type: 'string', description: 'When the response was created' },
lastSubmittedTime: {
type: 'string',
description: 'When the response was last submitted',
},
answers: {
type: 'json',
description: 'Map of question IDs to answer values',
},
},
},
},
nextPageToken: {
type: 'string',
description: 'Token to fetch the next page of responses (null when no more pages)',
optional: true,
},
response: {
type: 'object',
description: 'Single form response (when responseId is provided)',
properties: {
responseId: { type: 'string', description: 'Unique response ID' },
createTime: { type: 'string', description: 'When the response was created' },
lastSubmittedTime: { type: 'string', description: 'When the response was last submitted' },
answers: {
type: 'json',
description: 'Map of question IDs to answer values',
},
},
},
raw: {
type: 'json',
description: 'Raw API response data',
},
},
}
+21
View File
@@ -0,0 +1,21 @@
import { batchUpdateTool } from '@/tools/google_forms/batch_update'
import { createFormTool } from '@/tools/google_forms/create_form'
import { createWatchTool } from '@/tools/google_forms/create_watch'
import { deleteWatchTool } from '@/tools/google_forms/delete_watch'
import { getFormTool } from '@/tools/google_forms/get_form'
import { getResponsesTool } from '@/tools/google_forms/get_responses'
import { listWatchesTool } from '@/tools/google_forms/list_watches'
import { renewWatchTool } from '@/tools/google_forms/renew_watch'
import { setPublishSettingsTool } from '@/tools/google_forms/set_publish_settings'
export const googleFormsGetResponsesTool = getResponsesTool
export const googleFormsGetFormTool = getFormTool
export const googleFormsCreateFormTool = createFormTool
export const googleFormsBatchUpdateTool = batchUpdateTool
export const googleFormsSetPublishSettingsTool = setPublishSettingsTool
export const googleFormsCreateWatchTool = createWatchTool
export const googleFormsListWatchesTool = listWatchesTool
export const googleFormsDeleteWatchTool = deleteWatchTool
export const googleFormsRenewWatchTool = renewWatchTool
export * from './types'
@@ -0,0 +1,98 @@
import type {
GoogleFormsListWatchesParams,
GoogleFormsListWatchesResponse,
GoogleFormsWatch,
} from '@/tools/google_forms/types'
import { buildListWatchesUrl, getGoogleFormsErrorMessage } from '@/tools/google_forms/utils'
import type { ToolConfig } from '@/tools/types'
interface ListWatchesApiResponse {
watches?: GoogleFormsWatch[]
}
export const listWatchesTool: ToolConfig<
GoogleFormsListWatchesParams,
GoogleFormsListWatchesResponse
> = {
id: 'google_forms_list_watches',
name: 'Google Forms: List Watches',
description: 'List all notification watches for a form',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-forms',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token',
},
formId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Forms form ID',
},
},
request: {
url: (params: GoogleFormsListWatchesParams) => buildListWatchesUrl(params.formId),
method: 'GET',
headers: (params: GoogleFormsListWatchesParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = (await response.json()) as ListWatchesApiResponse
if (!response.ok) {
return {
success: false,
output: {
watches: [],
},
error: getGoogleFormsErrorMessage(data, 'Failed to list watches'),
}
}
const watches = (data.watches ?? []).map((watch) => ({
id: watch.id,
target: watch.target,
eventType: watch.eventType,
createTime: watch.createTime,
expireTime: watch.expireTime,
state: watch.state,
errorType: watch.errorType,
}))
return {
success: true,
output: {
watches,
},
}
},
outputs: {
watches: {
type: 'array',
description: 'List of watches for the form',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Watch ID' },
eventType: { type: 'string', description: 'Event type (SCHEMA or RESPONSES)' },
createTime: { type: 'string', description: 'When the watch was created' },
expireTime: { type: 'string', description: 'When the watch expires' },
state: { type: 'string', description: 'Watch state' },
},
},
},
},
}
@@ -0,0 +1,86 @@
import type {
GoogleFormsRenewWatchParams,
GoogleFormsRenewWatchResponse,
GoogleFormsWatch,
} from '@/tools/google_forms/types'
import { buildRenewWatchUrl, getGoogleFormsErrorMessage } from '@/tools/google_forms/utils'
import type { ToolConfig } from '@/tools/types'
export const renewWatchTool: ToolConfig<
GoogleFormsRenewWatchParams,
GoogleFormsRenewWatchResponse
> = {
id: 'google_forms_renew_watch',
name: 'Google Forms: Renew Watch',
description: 'Renew a notification watch for another 7 days',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-forms',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token',
},
formId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Forms form ID',
},
watchId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Watch ID to renew',
},
},
request: {
url: (params: GoogleFormsRenewWatchParams) => buildRenewWatchUrl(params.formId, params.watchId),
method: 'POST',
headers: (params: GoogleFormsRenewWatchParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = (await response.json()) as GoogleFormsWatch
if (!response.ok) {
return {
success: false,
output: {
id: '',
eventType: null,
expireTime: null,
state: null,
},
error: getGoogleFormsErrorMessage(data, 'Failed to renew watch'),
}
}
return {
success: true,
output: {
id: data.id ?? '',
eventType: data.eventType ?? null,
expireTime: data.expireTime ?? null,
state: data.state ?? null,
},
}
},
outputs: {
id: { type: 'string', description: 'The watch ID' },
eventType: { type: 'string', description: 'The event type being watched', optional: true },
expireTime: { type: 'string', description: 'The new expiration time', optional: true },
state: { type: 'string', description: 'The watch state', optional: true },
},
}
@@ -0,0 +1,118 @@
import type {
GoogleFormsPublishSettings,
GoogleFormsSetPublishSettingsParams,
GoogleFormsSetPublishSettingsResponse,
} from '@/tools/google_forms/types'
import { buildSetPublishSettingsUrl, getGoogleFormsErrorMessage } from '@/tools/google_forms/utils'
import type { ToolConfig } from '@/tools/types'
interface SetPublishSettingsApiResponse {
formId?: string
publishSettings?: GoogleFormsPublishSettings
}
export const setPublishSettingsTool: ToolConfig<
GoogleFormsSetPublishSettingsParams,
GoogleFormsSetPublishSettingsResponse
> = {
id: 'google_forms_set_publish_settings',
name: 'Google Forms: Set Publish Settings',
description: 'Update the publish settings of a form (publish/unpublish, accept responses)',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-forms',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token',
},
formId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Forms form ID',
},
isPublished: {
type: 'boolean',
required: true,
visibility: 'user-or-llm',
description: 'Whether the form is published and visible to others',
},
isAcceptingResponses: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the form accepts responses (forced to false if isPublished is false)',
},
},
request: {
url: (params: GoogleFormsSetPublishSettingsParams) => buildSetPublishSettingsUrl(params.formId),
method: 'POST',
headers: (params: GoogleFormsSetPublishSettingsParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params: GoogleFormsSetPublishSettingsParams) => ({
publishSettings: {
publishState: {
isPublished: params.isPublished,
...(params.isAcceptingResponses !== undefined
? { isAcceptingResponses: params.isAcceptingResponses }
: {}),
},
},
updateMask: 'publishState',
}),
},
transformResponse: async (response: Response) => {
const data = (await response.json()) as SetPublishSettingsApiResponse
if (!response.ok) {
return {
success: false,
output: {
formId: '',
publishSettings: {},
},
error: getGoogleFormsErrorMessage(data, 'Failed to set publish settings'),
}
}
return {
success: true,
output: {
formId: data.formId ?? '',
publishSettings: data.publishSettings ?? {},
},
}
},
outputs: {
formId: { type: 'string', description: 'The form ID' },
publishSettings: {
type: 'json',
description: 'The updated publish settings',
properties: {
publishState: {
type: 'object',
description: 'The publish state',
properties: {
isPublished: { type: 'boolean', description: 'Whether the form is published' },
isAcceptingResponses: {
type: 'boolean',
description: 'Whether the form accepts responses',
},
},
},
},
},
},
}
+270
View File
@@ -0,0 +1,270 @@
import type { ToolResponse } from '@/tools/types'
// ============================================
// Common Types
// ============================================
export interface GoogleFormsResponse {
responseId?: string
createTime?: string
lastSubmittedTime?: string
answers?: Record<string, unknown>
respondentEmail?: string
totalScore?: number
[key: string]: unknown
}
export interface GoogleFormsResponseList {
responses?: GoogleFormsResponse[]
nextPageToken?: string
}
interface GoogleFormsInfo {
title?: string
description?: string
documentTitle?: string
}
interface GoogleFormsSettings {
quizSettings?: {
isQuiz?: boolean
}
emailCollectionType?:
| 'EMAIL_COLLECTION_TYPE_UNSPECIFIED'
| 'DO_NOT_COLLECT'
| 'VERIFIED'
| 'RESPONDER_INPUT'
[key: string]: unknown
}
interface GoogleFormsPublishState {
isPublished?: boolean
isAcceptingResponses?: boolean
}
export interface GoogleFormsPublishSettings {
publishState?: GoogleFormsPublishState
}
interface GoogleFormsItem {
itemId?: string
title?: string
description?: string
questionItem?: Record<string, unknown>
questionGroupItem?: Record<string, unknown>
pageBreakItem?: Record<string, unknown>
textItem?: Record<string, unknown>
imageItem?: Record<string, unknown>
videoItem?: Record<string, unknown>
}
export interface GoogleForm {
formId?: string
info?: GoogleFormsInfo
settings?: GoogleFormsSettings
items?: GoogleFormsItem[]
revisionId?: string
responderUri?: string
linkedSheetId?: string
publishSettings?: GoogleFormsPublishSettings
}
export interface GoogleFormsWatch {
id?: string
target?: {
topic?: {
topicName?: string
}
}
eventType?: 'EVENT_TYPE_UNSPECIFIED' | 'SCHEMA' | 'RESPONSES'
createTime?: string
expireTime?: string
state?: 'STATE_UNSPECIFIED' | 'ACTIVE' | 'SUSPENDED'
errorType?: string
}
// ============================================
// Get Responses Params
// ============================================
export interface GoogleFormsGetResponsesParams {
accessToken: string
formId: string
responseId?: string
pageSize?: number
pageToken?: string
filter?: string
}
// ============================================
// Get Form Params & Response
// ============================================
export interface GoogleFormsGetFormParams {
accessToken: string
formId: string
}
export interface GoogleFormsGetFormResponse extends ToolResponse {
output: {
formId: string
title: string | null
description: string | null
documentTitle: string | null
responderUri: string | null
linkedSheetId: string | null
revisionId: string | null
items: GoogleFormsItem[]
settings: GoogleFormsSettings | null
publishSettings: GoogleFormsPublishSettings | null
}
}
// ============================================
// Create Form Params & Response
// ============================================
export interface GoogleFormsCreateFormParams {
accessToken: string
title: string
documentTitle?: string
unpublished?: boolean
}
export interface GoogleFormsCreateFormResponse extends ToolResponse {
output: {
formId: string
title: string | null
documentTitle: string | null
responderUri: string | null
revisionId: string | null
}
}
// ============================================
// Batch Update Params & Response
// ============================================
interface GoogleFormsBatchUpdateRequest {
updateFormInfo?: {
info: Partial<GoogleFormsInfo>
updateMask: string
}
updateSettings?: {
settings: Partial<GoogleFormsSettings>
updateMask: string
}
createItem?: {
item: GoogleFormsItem
location: { index: number }
}
updateItem?: {
item: GoogleFormsItem
location: { index: number }
updateMask: string
}
moveItem?: {
originalLocation: { index: number }
newLocation: { index: number }
}
deleteItem?: {
location: { index: number }
}
}
export interface GoogleFormsBatchUpdateParams {
accessToken: string
formId: string
requests: GoogleFormsBatchUpdateRequest[]
includeFormInResponse?: boolean
}
export interface GoogleFormsBatchUpdateResponse extends ToolResponse {
output: {
replies: Record<string, unknown>[]
writeControl: {
requiredRevisionId?: string
targetRevisionId?: string
} | null
form: GoogleForm | null
}
}
// ============================================
// Set Publish Settings Params & Response
// ============================================
export interface GoogleFormsSetPublishSettingsParams {
accessToken: string
formId: string
isPublished: boolean
isAcceptingResponses?: boolean
}
export interface GoogleFormsSetPublishSettingsResponse extends ToolResponse {
output: {
formId: string
publishSettings: GoogleFormsPublishSettings
}
}
// ============================================
// Watch Params & Responses
// ============================================
export interface GoogleFormsCreateWatchParams {
accessToken: string
formId: string
eventType: 'SCHEMA' | 'RESPONSES'
topicName: string
watchId?: string
}
export interface GoogleFormsCreateWatchResponse extends ToolResponse {
output: {
id: string
eventType: string
topicName: string | null
createTime: string | null
expireTime: string | null
state: string | null
}
}
export interface GoogleFormsListWatchesParams {
accessToken: string
formId: string
}
export interface GoogleFormsListWatchesResponse extends ToolResponse {
output: {
watches: GoogleFormsWatch[]
}
}
export interface GoogleFormsDeleteWatchParams {
accessToken: string
formId: string
watchId: string
}
export interface GoogleFormsDeleteWatchResponse extends ToolResponse {
output: {
deleted: boolean
}
}
export interface GoogleFormsRenewWatchParams {
accessToken: string
formId: string
watchId: string
}
export interface GoogleFormsRenewWatchResponse extends ToolResponse {
output: {
id: string
eventType: string | null
expireTime: string | null
state: string | null
}
}
+101
View File
@@ -0,0 +1,101 @@
import { createLogger } from '@sim/logger'
export const FORMS_API_BASE = 'https://forms.googleapis.com/v1'
const logger = createLogger('GoogleFormsUtils')
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object'
}
export function getGoogleFormsErrorMessage(data: unknown, fallback: string): string {
if (!isRecord(data)) return fallback
const { error } = data
if (!isRecord(error)) return fallback
const { message } = error
return typeof message === 'string' ? message : fallback
}
export function buildListResponsesUrl(params: {
formId: string
pageSize?: number
pageToken?: string
filter?: string
}): string {
const { formId, pageSize, pageToken, filter } = params
const url = new URL(`${FORMS_API_BASE}/forms/${encodeURIComponent(formId)}/responses`)
if (pageSize && pageSize > 0) {
const limited = Math.min(pageSize, 5000)
url.searchParams.set('pageSize', String(limited))
}
if (pageToken) {
url.searchParams.set('pageToken', pageToken)
}
if (filter) {
url.searchParams.set('filter', filter)
}
const finalUrl = url.toString()
logger.debug('Built Google Forms list responses URL', { finalUrl })
return finalUrl
}
export function buildGetResponseUrl(params: { formId: string; responseId: string }): string {
const { formId, responseId } = params
const finalUrl = `${FORMS_API_BASE}/forms/${encodeURIComponent(formId)}/responses/${encodeURIComponent(responseId)}`
logger.debug('Built Google Forms get response URL', { finalUrl })
return finalUrl
}
export function buildGetFormUrl(formId: string): string {
const finalUrl = `${FORMS_API_BASE}/forms/${encodeURIComponent(formId)}`
logger.debug('Built Google Forms get form URL', { finalUrl })
return finalUrl
}
export function buildCreateFormUrl(unpublished?: boolean): string {
const url = new URL(`${FORMS_API_BASE}/forms`)
if (unpublished) {
url.searchParams.set('unpublished', 'true')
}
const finalUrl = url.toString()
logger.debug('Built Google Forms create form URL', { finalUrl })
return finalUrl
}
export function buildBatchUpdateUrl(formId: string): string {
const finalUrl = `${FORMS_API_BASE}/forms/${encodeURIComponent(formId)}:batchUpdate`
logger.debug('Built Google Forms batch update URL', { finalUrl })
return finalUrl
}
export function buildSetPublishSettingsUrl(formId: string): string {
const finalUrl = `${FORMS_API_BASE}/forms/${encodeURIComponent(formId)}:setPublishSettings`
logger.debug('Built Google Forms set publish settings URL', { finalUrl })
return finalUrl
}
export function buildListWatchesUrl(formId: string): string {
const finalUrl = `${FORMS_API_BASE}/forms/${encodeURIComponent(formId)}/watches`
logger.debug('Built Google Forms list watches URL', { finalUrl })
return finalUrl
}
export function buildCreateWatchUrl(formId: string): string {
const finalUrl = `${FORMS_API_BASE}/forms/${encodeURIComponent(formId)}/watches`
logger.debug('Built Google Forms create watch URL', { finalUrl })
return finalUrl
}
export function buildDeleteWatchUrl(formId: string, watchId: string): string {
const finalUrl = `${FORMS_API_BASE}/forms/${encodeURIComponent(formId)}/watches/${encodeURIComponent(watchId)}`
logger.debug('Built Google Forms delete watch URL', { finalUrl })
return finalUrl
}
export function buildRenewWatchUrl(formId: string, watchId: string): string {
const finalUrl = `${FORMS_API_BASE}/forms/${encodeURIComponent(formId)}/watches/${encodeURIComponent(watchId)}:renew`
logger.debug('Built Google Forms renew watch URL', { finalUrl })
return finalUrl
}