fix(integrations): harden jira, jsm, ashby, google drive, slack, confluence, notion (#4345)

* fix(integrations): validate and harden jira, jsm, ashby, google drive, slack, confluence, notion

Audit and fix contract drift, input validation, and error handling across integrations:

- Jira: NaN guards on worklog seconds, JSON.parse try/catch on internal API responses,
  domain normalization (strip leading https://), JQL injection prevention via project
  key validation, ADF helper consolidation, /search/jql nextPageToken pagination,
  defensive .trim() on ID path params, encodeURIComponent on watcher account IDs,
  resolveAssigneeAccountId helper, parent-as-object wrapping, summary fallback,
  add read-bulk operation. Restored total field (always null) to preserve contract.
- JSM: customer/organization route validation
- Ashby: types and tool output cleanup across all 30+ tools
- Google Drive: tighter response handling across read/write/share tools
- Slack: types and tool fixes (canvas, reactions, messaging, members)
- Confluence: update tool and types
- Docs: regenerated mdx for all touched integrations

* fix(ashby): add subblock migrations for removed expand form definition fields

* fix(slack): restore canvas_id fallback to data.id for backwards compat

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(jsm): explicit 400 when deprecated `emails` param is sent

Address greptile review on PR #4345: instead of silently dropping
`emails` and falling through to list-customers, return a 400 telling
the caller to use `accountIds`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(google_drive): include HTTP status in fallback error messages

Address greptile review on PR #4345: when Google Drive returns a non-JSON
error body, surface the response status/statusText so failures are
diagnosable instead of falling through to a generic message.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(ashby): drop stray websiteUrl→website remap for update_candidate

The update_candidate tool reads params.websiteUrl directly; mapping it
to result.website added a confusing dead field. The websiteUrl subBlock
auto-passes through with the matching name.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(google_drive): rename canonical params to avoid subBlock ID clash

`mimeType`, `query`, and `pageSize` canonical IDs collided with existing
subBlock IDs in the same block (failing the canonical-param validation
test). Drop the canonicalParamId from search/get_content single-input
fields and route them to tool params explicitly in tools.config.params.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(ashby): remove filterCandidateId from removed-subblock migrations

The candidate-id filter was reintroduced as a valid Ashby subBlock, but
the migration map still rewrote it to _removed_filterCandidateId on every
workflow load, silently breaking the field. Drop the entry so user values
persist.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(ashby): restore documented response fields dropped during refactor

Restore three fields that exist in Ashby's API responses but were dropped
during the recent refactor: applicationLimitCalloutHtml on /jobPosting.info,
compensation on /job.info (and add the `compensation` expand), and managerId
on /user.list.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* correctness

* updated types

* fix(ashby): gate operation-specific param mappings to prevent stale overwrites

Multiple subBlocks share the same target tool param (createdAt is set by
appCreatedAt/candidateCreatedAt/noteCreatedAt; candidateId by appCandidateId/
filterCandidateId). Because subBlock values persist across operation switches,
a stale value from a prior operation could silently overwrite the correct one.
Guard each mapping with an explicit operation check.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(ashby): gate offerApplicationId mapping by operation

Same shared-target hazard as the prior fix: offerApplicationId maps to
result.applicationId without an operation guard, so a stale value from
list_offers could overwrite the active applicationId on get_application,
change_application_stage, or list_interviews.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(ashby): include list_locations in includeArchived condition

Ashby's /location.list accepts includeArchived per the API docs, and the
docs page already documents the toggle for list_locations. Add the missing
operation value so the toggle renders.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(jira): forward explicit notifyUsers=true query param on issue update

Block now distinguishes true/false/undefined for notifyUsers, but the route
collapsed true and undefined into a no-param request. Forward the explicit
true intent so it survives any future API default change or proxy override.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(jira): quote project key in JQL to defend against injection

* fix(jira): quote project key in bulk_read JQL for defense in depth

The alphanumeric regex check above already blocks injection, but quoting
the project key matches the pattern used elsewhere (issues/route.ts) and
hardens the path against future regex changes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Waleed
2026-04-29 19:08:23 -07:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 36742740aa
commit 0c69302369
104 changed files with 2389 additions and 782 deletions
+117 -63
View File
@@ -53,7 +53,7 @@ Adds a tag to a candidate in Ashby and returns the updated candidate.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `candidates` | json | List of candidates with rich fields \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], linkedInUrl, githubUrl, profileUrl, position, company, school, timezone, location with locationComponents\[\], tags\[\], applicationIds\[\], customFields\[\], resumeFileHandle, fileHandles\[\], source with sourceType, creditedToUser, fraudStatus, createdAt, updatedAt\) |
| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion, compensation with compensationTiers\[\]\) |
| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion\) |
| `applications` | json | List of applications \(id, status, customFields\[\], candidate summary, currentInterviewStage, source with sourceType, archiveReason with customFields\[\], archivedAt, job summary, creditedToUser, hiringTeam\[\], appliedViaJobPostingId, submitterClientIp, submitterUserAgent, createdAt, updatedAt\) |
| `notes` | json | List of notes \(id, content, author, isPrivate, createdAt\) |
| `offers` | json | List of offers \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion with id/startDate/salary/createdAt/openingId/customFields\[\]/fileHandles\[\]/author/approvalStatus\) |
@@ -64,20 +64,20 @@ Adds a tag to a candidate in Ashby and returns the updated candidate.
| `locations` | json | List of locations \(id, name, externalName, isArchived, isRemote, workplaceType, parentLocationId, type, address with addressCountry/Region/Locality/postalCode/streetAddress\) |
| `jobPostings` | json | List of job postings \(id, title, jobId, departmentName, teamName, locationName, locationIds, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensationTierSummary, shouldDisplayCompensationOnJobBoard, updatedAt\) |
| `openings` | json | List of openings \(id, openedAt, closedAt, isArchived, archivedAt, closeReasonId, openingState, latestVersion with identifier/description/authorId/createdAt/teamId/jobIds\[\]/targetHireDate/targetStartDate/isBackfill/employmentType/locationIds\[\]/hiringTeam\[\]/customFields\[\]\) |
| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId\) |
| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt\) |
| `interviewSchedules` | json | List of interview schedules \(id, applicationId, interviewStageId, interviewEvents\[\] with interviewerUserIds/startTime/endTime/feedbackLink/location/meetingLink/hasSubmittedFeedback, status, scheduledBy, createdAt, updatedAt\) |
| `tags` | json | List of candidate tags \(id, title, isArchived\) |
| `id` | string | Resource UUID |
| `name` | string | Resource name |
| `title` | string | Job title or job posting title |
| `status` | string | Status |
| `candidate` | json | Candidate details \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], customFields\[\], source, creditedToUser, createdAt, updatedAt\) |
| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], compensation, createdAt, updatedAt\) |
| `candidate` | json | Candidate summary \(id, name, primaryEmailAddress, primaryPhoneNumber\). For full candidate fields use the candidates list output or the get/create/update candidate operations. |
| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], createdAt, updatedAt\) |
| `application` | json | Application details \(id, status, customFields\[\], candidate, currentInterviewStage, source, archiveReason, job, hiringTeam\[\], createdAt, updatedAt\) |
| `offer` | json | Offer details \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion\) |
| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt\) |
| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt, job \[included when expandJob=true\]\) |
| `content` | string | Note content |
| `author` | json | Note author \(id, firstName, lastName, email, globalRole, isEnabled\) |
| `author` | json | Note author \(id, firstName, lastName, email\) |
| `isPrivate` | boolean | Whether the note is private |
| `createdAt` | string | ISO 8601 creation timestamp |
| `moreDataAvailable` | boolean | Whether more pages exist |
@@ -102,7 +102,7 @@ Moves an application to a different interview stage. Requires an archive reason
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `candidates` | json | List of candidates with rich fields \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], linkedInUrl, githubUrl, profileUrl, position, company, school, timezone, location with locationComponents\[\], tags\[\], applicationIds\[\], customFields\[\], resumeFileHandle, fileHandles\[\], source with sourceType, creditedToUser, fraudStatus, createdAt, updatedAt\) |
| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion, compensation with compensationTiers\[\]\) |
| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion\) |
| `applications` | json | List of applications \(id, status, customFields\[\], candidate summary, currentInterviewStage, source with sourceType, archiveReason with customFields\[\], archivedAt, job summary, creditedToUser, hiringTeam\[\], appliedViaJobPostingId, submitterClientIp, submitterUserAgent, createdAt, updatedAt\) |
| `notes` | json | List of notes \(id, content, author, isPrivate, createdAt\) |
| `offers` | json | List of offers \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion with id/startDate/salary/createdAt/openingId/customFields\[\]/fileHandles\[\]/author/approvalStatus\) |
@@ -113,20 +113,20 @@ Moves an application to a different interview stage. Requires an archive reason
| `locations` | json | List of locations \(id, name, externalName, isArchived, isRemote, workplaceType, parentLocationId, type, address with addressCountry/Region/Locality/postalCode/streetAddress\) |
| `jobPostings` | json | List of job postings \(id, title, jobId, departmentName, teamName, locationName, locationIds, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensationTierSummary, shouldDisplayCompensationOnJobBoard, updatedAt\) |
| `openings` | json | List of openings \(id, openedAt, closedAt, isArchived, archivedAt, closeReasonId, openingState, latestVersion with identifier/description/authorId/createdAt/teamId/jobIds\[\]/targetHireDate/targetStartDate/isBackfill/employmentType/locationIds\[\]/hiringTeam\[\]/customFields\[\]\) |
| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId\) |
| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt\) |
| `interviewSchedules` | json | List of interview schedules \(id, applicationId, interviewStageId, interviewEvents\[\] with interviewerUserIds/startTime/endTime/feedbackLink/location/meetingLink/hasSubmittedFeedback, status, scheduledBy, createdAt, updatedAt\) |
| `tags` | json | List of candidate tags \(id, title, isArchived\) |
| `id` | string | Resource UUID |
| `name` | string | Resource name |
| `title` | string | Job title or job posting title |
| `status` | string | Status |
| `candidate` | json | Candidate details \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], customFields\[\], source, creditedToUser, createdAt, updatedAt\) |
| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], compensation, createdAt, updatedAt\) |
| `candidate` | json | Candidate summary \(id, name, primaryEmailAddress, primaryPhoneNumber\). For full candidate fields use the candidates list output or the get/create/update candidate operations. |
| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], createdAt, updatedAt\) |
| `application` | json | Application details \(id, status, customFields\[\], candidate, currentInterviewStage, source, archiveReason, job, hiringTeam\[\], createdAt, updatedAt\) |
| `offer` | json | Offer details \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion\) |
| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt\) |
| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt, job \[included when expandJob=true\]\) |
| `content` | string | Note content |
| `author` | json | Note author \(id, firstName, lastName, email, globalRole, isEnabled\) |
| `author` | json | Note author \(id, firstName, lastName, email\) |
| `isPrivate` | boolean | Whether the note is private |
| `createdAt` | string | ISO 8601 creation timestamp |
| `moreDataAvailable` | boolean | Whether more pages exist |
@@ -155,7 +155,7 @@ Creates a new application for a candidate on a job. Optionally specify interview
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `candidates` | json | List of candidates with rich fields \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], linkedInUrl, githubUrl, profileUrl, position, company, school, timezone, location with locationComponents\[\], tags\[\], applicationIds\[\], customFields\[\], resumeFileHandle, fileHandles\[\], source with sourceType, creditedToUser, fraudStatus, createdAt, updatedAt\) |
| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion, compensation with compensationTiers\[\]\) |
| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion\) |
| `applications` | json | List of applications \(id, status, customFields\[\], candidate summary, currentInterviewStage, source with sourceType, archiveReason with customFields\[\], archivedAt, job summary, creditedToUser, hiringTeam\[\], appliedViaJobPostingId, submitterClientIp, submitterUserAgent, createdAt, updatedAt\) |
| `notes` | json | List of notes \(id, content, author, isPrivate, createdAt\) |
| `offers` | json | List of offers \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion with id/startDate/salary/createdAt/openingId/customFields\[\]/fileHandles\[\]/author/approvalStatus\) |
@@ -166,20 +166,20 @@ Creates a new application for a candidate on a job. Optionally specify interview
| `locations` | json | List of locations \(id, name, externalName, isArchived, isRemote, workplaceType, parentLocationId, type, address with addressCountry/Region/Locality/postalCode/streetAddress\) |
| `jobPostings` | json | List of job postings \(id, title, jobId, departmentName, teamName, locationName, locationIds, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensationTierSummary, shouldDisplayCompensationOnJobBoard, updatedAt\) |
| `openings` | json | List of openings \(id, openedAt, closedAt, isArchived, archivedAt, closeReasonId, openingState, latestVersion with identifier/description/authorId/createdAt/teamId/jobIds\[\]/targetHireDate/targetStartDate/isBackfill/employmentType/locationIds\[\]/hiringTeam\[\]/customFields\[\]\) |
| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId\) |
| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt\) |
| `interviewSchedules` | json | List of interview schedules \(id, applicationId, interviewStageId, interviewEvents\[\] with interviewerUserIds/startTime/endTime/feedbackLink/location/meetingLink/hasSubmittedFeedback, status, scheduledBy, createdAt, updatedAt\) |
| `tags` | json | List of candidate tags \(id, title, isArchived\) |
| `id` | string | Resource UUID |
| `name` | string | Resource name |
| `title` | string | Job title or job posting title |
| `status` | string | Status |
| `candidate` | json | Candidate details \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], customFields\[\], source, creditedToUser, createdAt, updatedAt\) |
| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], compensation, createdAt, updatedAt\) |
| `candidate` | json | Candidate summary \(id, name, primaryEmailAddress, primaryPhoneNumber\). For full candidate fields use the candidates list output or the get/create/update candidate operations. |
| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], createdAt, updatedAt\) |
| `application` | json | Application details \(id, status, customFields\[\], candidate, currentInterviewStage, source, archiveReason, job, hiringTeam\[\], createdAt, updatedAt\) |
| `offer` | json | Offer details \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion\) |
| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt\) |
| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt, job \[included when expandJob=true\]\) |
| `content` | string | Note content |
| `author` | json | Note author \(id, firstName, lastName, email, globalRole, isEnabled\) |
| `author` | json | Note author \(id, firstName, lastName, email\) |
| `isPrivate` | boolean | Whether the note is private |
| `createdAt` | string | ISO 8601 creation timestamp |
| `moreDataAvailable` | boolean | Whether more pages exist |
@@ -200,14 +200,18 @@ Creates a new candidate record in Ashby.
| `phoneNumber` | string | No | Primary phone number for the candidate |
| `linkedInUrl` | string | No | LinkedIn profile URL |
| `githubUrl` | string | No | GitHub profile URL |
| `website` | string | No | Personal website URL |
| `sourceId` | string | No | UUID of the source to attribute the candidate to |
| `creditedToUserId` | string | No | UUID of the Ashby user to credit with sourcing this candidate |
| `createdAt` | string | No | Backdated creation timestamp in ISO 8601 \(e.g. 2024-01-01T00:00:00Z\). Defaults to now. |
| `alternateEmailAddresses` | json | No | Array of additional email address strings to add to the candidate, e.g. \["a@x.com","b@y.com"\] |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `candidates` | json | List of candidates with rich fields \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], linkedInUrl, githubUrl, profileUrl, position, company, school, timezone, location with locationComponents\[\], tags\[\], applicationIds\[\], customFields\[\], resumeFileHandle, fileHandles\[\], source with sourceType, creditedToUser, fraudStatus, createdAt, updatedAt\) |
| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion, compensation with compensationTiers\[\]\) |
| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion\) |
| `applications` | json | List of applications \(id, status, customFields\[\], candidate summary, currentInterviewStage, source with sourceType, archiveReason with customFields\[\], archivedAt, job summary, creditedToUser, hiringTeam\[\], appliedViaJobPostingId, submitterClientIp, submitterUserAgent, createdAt, updatedAt\) |
| `notes` | json | List of notes \(id, content, author, isPrivate, createdAt\) |
| `offers` | json | List of offers \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion with id/startDate/salary/createdAt/openingId/customFields\[\]/fileHandles\[\]/author/approvalStatus\) |
@@ -218,20 +222,20 @@ Creates a new candidate record in Ashby.
| `locations` | json | List of locations \(id, name, externalName, isArchived, isRemote, workplaceType, parentLocationId, type, address with addressCountry/Region/Locality/postalCode/streetAddress\) |
| `jobPostings` | json | List of job postings \(id, title, jobId, departmentName, teamName, locationName, locationIds, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensationTierSummary, shouldDisplayCompensationOnJobBoard, updatedAt\) |
| `openings` | json | List of openings \(id, openedAt, closedAt, isArchived, archivedAt, closeReasonId, openingState, latestVersion with identifier/description/authorId/createdAt/teamId/jobIds\[\]/targetHireDate/targetStartDate/isBackfill/employmentType/locationIds\[\]/hiringTeam\[\]/customFields\[\]\) |
| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId\) |
| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt\) |
| `interviewSchedules` | json | List of interview schedules \(id, applicationId, interviewStageId, interviewEvents\[\] with interviewerUserIds/startTime/endTime/feedbackLink/location/meetingLink/hasSubmittedFeedback, status, scheduledBy, createdAt, updatedAt\) |
| `tags` | json | List of candidate tags \(id, title, isArchived\) |
| `id` | string | Resource UUID |
| `name` | string | Resource name |
| `title` | string | Job title or job posting title |
| `status` | string | Status |
| `candidate` | json | Candidate details \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], customFields\[\], source, creditedToUser, createdAt, updatedAt\) |
| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], compensation, createdAt, updatedAt\) |
| `candidate` | json | Candidate summary \(id, name, primaryEmailAddress, primaryPhoneNumber\). For full candidate fields use the candidates list output or the get/create/update candidate operations. |
| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], createdAt, updatedAt\) |
| `application` | json | Application details \(id, status, customFields\[\], candidate, currentInterviewStage, source, archiveReason, job, hiringTeam\[\], createdAt, updatedAt\) |
| `offer` | json | Offer details \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion\) |
| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt\) |
| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt, job \[included when expandJob=true\]\) |
| `content` | string | Note content |
| `author` | json | Note author \(id, firstName, lastName, email, globalRole, isEnabled\) |
| `author` | json | Note author \(id, firstName, lastName, email\) |
| `isPrivate` | boolean | Whether the note is private |
| `createdAt` | string | ISO 8601 creation timestamp |
| `moreDataAvailable` | boolean | Whether more pages exist |
@@ -251,6 +255,8 @@ Creates a note on a candidate in Ashby. Supports plain text and HTML content (bo
| `note` | string | Yes | The note content. If noteType is text/html, supports: &lt;b&gt;, &lt;i&gt;, &lt;u&gt;, &lt;a&gt;, &lt;ul&gt;, &lt;ol&gt;, &lt;li&gt;, &lt;code&gt;, &lt;pre&gt; |
| `noteType` | string | No | Content type of the note: text/plain \(default\) or text/html |
| `sendNotifications` | boolean | No | Whether to send notifications to subscribed users \(default false\) |
| `isPrivate` | boolean | No | Whether the note is private \(only visible to the author\) |
| `createdAt` | string | No | Backdated creation timestamp in ISO 8601 \(e.g. 2024-01-01T00:00:00Z\). Defaults to now. |
#### Output
@@ -282,7 +288,7 @@ Retrieves full details about a single application by its ID.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `candidates` | json | List of candidates with rich fields \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], linkedInUrl, githubUrl, profileUrl, position, company, school, timezone, location with locationComponents\[\], tags\[\], applicationIds\[\], customFields\[\], resumeFileHandle, fileHandles\[\], source with sourceType, creditedToUser, fraudStatus, createdAt, updatedAt\) |
| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion, compensation with compensationTiers\[\]\) |
| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion\) |
| `applications` | json | List of applications \(id, status, customFields\[\], candidate summary, currentInterviewStage, source with sourceType, archiveReason with customFields\[\], archivedAt, job summary, creditedToUser, hiringTeam\[\], appliedViaJobPostingId, submitterClientIp, submitterUserAgent, createdAt, updatedAt\) |
| `notes` | json | List of notes \(id, content, author, isPrivate, createdAt\) |
| `offers` | json | List of offers \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion with id/startDate/salary/createdAt/openingId/customFields\[\]/fileHandles\[\]/author/approvalStatus\) |
@@ -293,20 +299,20 @@ Retrieves full details about a single application by its ID.
| `locations` | json | List of locations \(id, name, externalName, isArchived, isRemote, workplaceType, parentLocationId, type, address with addressCountry/Region/Locality/postalCode/streetAddress\) |
| `jobPostings` | json | List of job postings \(id, title, jobId, departmentName, teamName, locationName, locationIds, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensationTierSummary, shouldDisplayCompensationOnJobBoard, updatedAt\) |
| `openings` | json | List of openings \(id, openedAt, closedAt, isArchived, archivedAt, closeReasonId, openingState, latestVersion with identifier/description/authorId/createdAt/teamId/jobIds\[\]/targetHireDate/targetStartDate/isBackfill/employmentType/locationIds\[\]/hiringTeam\[\]/customFields\[\]\) |
| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId\) |
| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt\) |
| `interviewSchedules` | json | List of interview schedules \(id, applicationId, interviewStageId, interviewEvents\[\] with interviewerUserIds/startTime/endTime/feedbackLink/location/meetingLink/hasSubmittedFeedback, status, scheduledBy, createdAt, updatedAt\) |
| `tags` | json | List of candidate tags \(id, title, isArchived\) |
| `id` | string | Resource UUID |
| `name` | string | Resource name |
| `title` | string | Job title or job posting title |
| `status` | string | Status |
| `candidate` | json | Candidate details \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], customFields\[\], source, creditedToUser, createdAt, updatedAt\) |
| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], compensation, createdAt, updatedAt\) |
| `candidate` | json | Candidate summary \(id, name, primaryEmailAddress, primaryPhoneNumber\). For full candidate fields use the candidates list output or the get/create/update candidate operations. |
| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], createdAt, updatedAt\) |
| `application` | json | Application details \(id, status, customFields\[\], candidate, currentInterviewStage, source, archiveReason, job, hiringTeam\[\], createdAt, updatedAt\) |
| `offer` | json | Offer details \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion\) |
| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt\) |
| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt, job \[included when expandJob=true\]\) |
| `content` | string | Note content |
| `author` | json | Note author \(id, firstName, lastName, email, globalRole, isEnabled\) |
| `author` | json | Note author \(id, firstName, lastName, email\) |
| `isPrivate` | boolean | Whether the note is private |
| `createdAt` | string | ISO 8601 creation timestamp |
| `moreDataAvailable` | boolean | Whether more pages exist |
@@ -329,7 +335,7 @@ Retrieves full details about a single candidate by their ID.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `candidates` | json | List of candidates with rich fields \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], linkedInUrl, githubUrl, profileUrl, position, company, school, timezone, location with locationComponents\[\], tags\[\], applicationIds\[\], customFields\[\], resumeFileHandle, fileHandles\[\], source with sourceType, creditedToUser, fraudStatus, createdAt, updatedAt\) |
| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion, compensation with compensationTiers\[\]\) |
| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion\) |
| `applications` | json | List of applications \(id, status, customFields\[\], candidate summary, currentInterviewStage, source with sourceType, archiveReason with customFields\[\], archivedAt, job summary, creditedToUser, hiringTeam\[\], appliedViaJobPostingId, submitterClientIp, submitterUserAgent, createdAt, updatedAt\) |
| `notes` | json | List of notes \(id, content, author, isPrivate, createdAt\) |
| `offers` | json | List of offers \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion with id/startDate/salary/createdAt/openingId/customFields\[\]/fileHandles\[\]/author/approvalStatus\) |
@@ -340,20 +346,20 @@ Retrieves full details about a single candidate by their ID.
| `locations` | json | List of locations \(id, name, externalName, isArchived, isRemote, workplaceType, parentLocationId, type, address with addressCountry/Region/Locality/postalCode/streetAddress\) |
| `jobPostings` | json | List of job postings \(id, title, jobId, departmentName, teamName, locationName, locationIds, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensationTierSummary, shouldDisplayCompensationOnJobBoard, updatedAt\) |
| `openings` | json | List of openings \(id, openedAt, closedAt, isArchived, archivedAt, closeReasonId, openingState, latestVersion with identifier/description/authorId/createdAt/teamId/jobIds\[\]/targetHireDate/targetStartDate/isBackfill/employmentType/locationIds\[\]/hiringTeam\[\]/customFields\[\]\) |
| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId\) |
| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt\) |
| `interviewSchedules` | json | List of interview schedules \(id, applicationId, interviewStageId, interviewEvents\[\] with interviewerUserIds/startTime/endTime/feedbackLink/location/meetingLink/hasSubmittedFeedback, status, scheduledBy, createdAt, updatedAt\) |
| `tags` | json | List of candidate tags \(id, title, isArchived\) |
| `id` | string | Resource UUID |
| `name` | string | Resource name |
| `title` | string | Job title or job posting title |
| `status` | string | Status |
| `candidate` | json | Candidate details \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], customFields\[\], source, creditedToUser, createdAt, updatedAt\) |
| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], compensation, createdAt, updatedAt\) |
| `candidate` | json | Candidate summary \(id, name, primaryEmailAddress, primaryPhoneNumber\). For full candidate fields use the candidates list output or the get/create/update candidate operations. |
| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], createdAt, updatedAt\) |
| `application` | json | Application details \(id, status, customFields\[\], candidate, currentInterviewStage, source, archiveReason, job, hiringTeam\[\], createdAt, updatedAt\) |
| `offer` | json | Offer details \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion\) |
| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt\) |
| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt, job \[included when expandJob=true\]\) |
| `content` | string | Note content |
| `author` | json | Note author \(id, firstName, lastName, email, globalRole, isEnabled\) |
| `author` | json | Note author \(id, firstName, lastName, email\) |
| `isPrivate` | boolean | Whether the note is private |
| `createdAt` | string | ISO 8601 creation timestamp |
| `moreDataAvailable` | boolean | Whether more pages exist |
@@ -376,7 +382,7 @@ Retrieves full details about a single job by its ID.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `candidates` | json | List of candidates with rich fields \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], linkedInUrl, githubUrl, profileUrl, position, company, school, timezone, location with locationComponents\[\], tags\[\], applicationIds\[\], customFields\[\], resumeFileHandle, fileHandles\[\], source with sourceType, creditedToUser, fraudStatus, createdAt, updatedAt\) |
| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion, compensation with compensationTiers\[\]\) |
| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion\) |
| `applications` | json | List of applications \(id, status, customFields\[\], candidate summary, currentInterviewStage, source with sourceType, archiveReason with customFields\[\], archivedAt, job summary, creditedToUser, hiringTeam\[\], appliedViaJobPostingId, submitterClientIp, submitterUserAgent, createdAt, updatedAt\) |
| `notes` | json | List of notes \(id, content, author, isPrivate, createdAt\) |
| `offers` | json | List of offers \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion with id/startDate/salary/createdAt/openingId/customFields\[\]/fileHandles\[\]/author/approvalStatus\) |
@@ -387,20 +393,20 @@ Retrieves full details about a single job by its ID.
| `locations` | json | List of locations \(id, name, externalName, isArchived, isRemote, workplaceType, parentLocationId, type, address with addressCountry/Region/Locality/postalCode/streetAddress\) |
| `jobPostings` | json | List of job postings \(id, title, jobId, departmentName, teamName, locationName, locationIds, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensationTierSummary, shouldDisplayCompensationOnJobBoard, updatedAt\) |
| `openings` | json | List of openings \(id, openedAt, closedAt, isArchived, archivedAt, closeReasonId, openingState, latestVersion with identifier/description/authorId/createdAt/teamId/jobIds\[\]/targetHireDate/targetStartDate/isBackfill/employmentType/locationIds\[\]/hiringTeam\[\]/customFields\[\]\) |
| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId\) |
| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt\) |
| `interviewSchedules` | json | List of interview schedules \(id, applicationId, interviewStageId, interviewEvents\[\] with interviewerUserIds/startTime/endTime/feedbackLink/location/meetingLink/hasSubmittedFeedback, status, scheduledBy, createdAt, updatedAt\) |
| `tags` | json | List of candidate tags \(id, title, isArchived\) |
| `id` | string | Resource UUID |
| `name` | string | Resource name |
| `title` | string | Job title or job posting title |
| `status` | string | Status |
| `candidate` | json | Candidate details \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], customFields\[\], source, creditedToUser, createdAt, updatedAt\) |
| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], compensation, createdAt, updatedAt\) |
| `candidate` | json | Candidate summary \(id, name, primaryEmailAddress, primaryPhoneNumber\). For full candidate fields use the candidates list output or the get/create/update candidate operations. |
| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], createdAt, updatedAt\) |
| `application` | json | Application details \(id, status, customFields\[\], candidate, currentInterviewStage, source, archiveReason, job, hiringTeam\[\], createdAt, updatedAt\) |
| `offer` | json | Offer details \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion\) |
| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt\) |
| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt, job \[included when expandJob=true\]\) |
| `content` | string | Note content |
| `author` | json | Note author \(id, firstName, lastName, email, globalRole, isEnabled\) |
| `author` | json | Note author \(id, firstName, lastName, email\) |
| `isPrivate` | boolean | Whether the note is private |
| `createdAt` | string | ISO 8601 creation timestamp |
| `moreDataAvailable` | boolean | Whether more pages exist |
@@ -417,8 +423,8 @@ Retrieves full details about a single job posting by its ID.
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Ashby API Key |
| `jobPostingId` | string | Yes | The UUID of the job posting to fetch |
| `expandApplicationFormDefinition` | boolean | No | Include application form definition in the response |
| `expandSurveyFormDefinitions` | boolean | No | Include survey form definitions in the response |
| `jobBoardId` | string | No | Optional job board UUID. If omitted, returns posting for the external job board. |
| `expandJob` | boolean | No | Whether to expand and include the related job object in the response |
#### Output
@@ -474,8 +480,9 @@ Retrieves full details about a single job posting by its ID.
| ↳ `minValue` | number | Minimum value |
| ↳ `maxValue` | number | Maximum value |
| ↳ `shouldDisplayCompensationOnJobBoard` | boolean | Whether compensation is shown on the job board |
| `applicationLimitCalloutHtml` | string | HTML callout shown when application limit is reached |
| `applicationLimitCalloutHtml` | string | HTML callout shown when the application limit is reached |
| `updatedAt` | string | ISO 8601 last update timestamp |
| `job` | object | The expanded job object, only present when the request was made with expandJob=true |
### `ashby_get_offer`
@@ -493,7 +500,7 @@ Retrieves full details about a single offer by its ID.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `candidates` | json | List of candidates with rich fields \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], linkedInUrl, githubUrl, profileUrl, position, company, school, timezone, location with locationComponents\[\], tags\[\], applicationIds\[\], customFields\[\], resumeFileHandle, fileHandles\[\], source with sourceType, creditedToUser, fraudStatus, createdAt, updatedAt\) |
| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion, compensation with compensationTiers\[\]\) |
| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion\) |
| `applications` | json | List of applications \(id, status, customFields\[\], candidate summary, currentInterviewStage, source with sourceType, archiveReason with customFields\[\], archivedAt, job summary, creditedToUser, hiringTeam\[\], appliedViaJobPostingId, submitterClientIp, submitterUserAgent, createdAt, updatedAt\) |
| `notes` | json | List of notes \(id, content, author, isPrivate, createdAt\) |
| `offers` | json | List of offers \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion with id/startDate/salary/createdAt/openingId/customFields\[\]/fileHandles\[\]/author/approvalStatus\) |
@@ -504,20 +511,20 @@ Retrieves full details about a single offer by its ID.
| `locations` | json | List of locations \(id, name, externalName, isArchived, isRemote, workplaceType, parentLocationId, type, address with addressCountry/Region/Locality/postalCode/streetAddress\) |
| `jobPostings` | json | List of job postings \(id, title, jobId, departmentName, teamName, locationName, locationIds, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensationTierSummary, shouldDisplayCompensationOnJobBoard, updatedAt\) |
| `openings` | json | List of openings \(id, openedAt, closedAt, isArchived, archivedAt, closeReasonId, openingState, latestVersion with identifier/description/authorId/createdAt/teamId/jobIds\[\]/targetHireDate/targetStartDate/isBackfill/employmentType/locationIds\[\]/hiringTeam\[\]/customFields\[\]\) |
| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId\) |
| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt\) |
| `interviewSchedules` | json | List of interview schedules \(id, applicationId, interviewStageId, interviewEvents\[\] with interviewerUserIds/startTime/endTime/feedbackLink/location/meetingLink/hasSubmittedFeedback, status, scheduledBy, createdAt, updatedAt\) |
| `tags` | json | List of candidate tags \(id, title, isArchived\) |
| `id` | string | Resource UUID |
| `name` | string | Resource name |
| `title` | string | Job title or job posting title |
| `status` | string | Status |
| `candidate` | json | Candidate details \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], customFields\[\], source, creditedToUser, createdAt, updatedAt\) |
| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], compensation, createdAt, updatedAt\) |
| `candidate` | json | Candidate summary \(id, name, primaryEmailAddress, primaryPhoneNumber\). For full candidate fields use the candidates list output or the get/create/update candidate operations. |
| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], createdAt, updatedAt\) |
| `application` | json | Application details \(id, status, customFields\[\], candidate, currentInterviewStage, source, archiveReason, job, hiringTeam\[\], createdAt, updatedAt\) |
| `offer` | json | Offer details \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion\) |
| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt\) |
| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt, job \[included when expandJob=true\]\) |
| `content` | string | Note content |
| `author` | json | Note author \(id, firstName, lastName, email, globalRole, isEnabled\) |
| `author` | json | Note author \(id, firstName, lastName, email\) |
| `isPrivate` | boolean | Whether the note is private |
| `createdAt` | string | ISO 8601 creation timestamp |
| `moreDataAvailable` | boolean | Whether more pages exist |
@@ -537,6 +544,7 @@ Lists all applications in an Ashby organization with pagination and optional fil
| `perPage` | number | No | Number of results per page \(default 100\) |
| `status` | string | No | Filter by application status: Active, Hired, Archived, or Lead |
| `jobId` | string | No | Filter applications by a specific job UUID |
| `candidateId` | string | No | Filter applications by a specific candidate UUID |
| `createdAfter` | string | No | Filter to applications created after this ISO 8601 timestamp \(e.g. 2024-01-01T00:00:00Z\) |
#### Output
@@ -605,6 +613,7 @@ Lists all candidates in an Ashby organization with cursor-based pagination.
| `apiKey` | string | Yes | Ashby API Key |
| `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value |
| `perPage` | number | No | Number of results per page \(default 100\) |
| `createdAfter` | string | No | Only return candidates created after this ISO 8601 timestamp \(e.g. 2024-01-01T00:00:00Z\) |
#### Output
@@ -623,6 +632,10 @@ Lists all custom field definitions configured in Ashby.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Ashby API Key |
| `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value |
| `perPage` | number | No | Number of results per page \(default and max 100\) |
| `syncToken` | string | No | Opaque token from a prior sync to fetch only items changed since then |
| `includeArchived` | boolean | No | When true, includes archived custom fields in results \(default false\) |
#### Output
@@ -640,6 +653,9 @@ Lists all custom field definitions configured in Ashby.
| ↳ `label` | string | Display label |
| ↳ `value` | string | Stored value |
| ↳ `isArchived` | boolean | Whether archived |
| `moreDataAvailable` | boolean | Whether more pages of results exist |
| `nextCursor` | string | Opaque cursor for fetching the next page |
| `syncToken` | string | Opaque sync token returned after the last page; pass on next sync |
### `ashby_list_departments`
@@ -650,6 +666,10 @@ Lists all departments in Ashby.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Ashby API Key |
| `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value |
| `perPage` | number | No | Number of results per page \(default and max 100\) |
| `syncToken` | string | No | Opaque token from a prior sync to fetch only items changed since then |
| `includeArchived` | boolean | No | When true, includes archived departments in results \(default false\) |
#### Output
@@ -663,6 +683,10 @@ Lists all departments in Ashby.
| ↳ `parentId` | string | Parent department UUID |
| ↳ `createdAt` | string | ISO 8601 creation timestamp |
| ↳ `updatedAt` | string | ISO 8601 last update timestamp |
| ↳ `extraData` | json | Free-form key-value metadata |
| `moreDataAvailable` | boolean | Whether more pages of results exist |
| `nextCursor` | string | Opaque cursor for fetching the next page |
| `syncToken` | string | Opaque sync token returned after the last page; pass on next sync |
### `ashby_list_interviews`
@@ -677,6 +701,7 @@ Lists interview schedules in Ashby, optionally filtered by application or interv
| `interviewStageId` | string | No | The UUID of the interview stage to list interview schedules for |
| `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value |
| `perPage` | number | No | Number of results per page \(default 100\) |
| `createdAfter` | string | No | Only return interview schedules created after this ISO 8601 timestamp \(e.g. 2024-01-01T00:00:00Z\) |
#### Output
@@ -714,6 +739,10 @@ Lists all job postings in Ashby.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Ashby API Key |
| `location` | string | No | Filter by location name \(case sensitive\) |
| `department` | string | No | Filter by department name \(case sensitive\) |
| `listedOnly` | boolean | No | When true, only returns listed \(publicly visible\) job postings \(default false\) |
| `jobBoardId` | string | No | UUID of a specific job board to filter postings to. If omitted, returns postings on the primary external job board. |
#### Output
@@ -752,6 +781,11 @@ Lists all jobs in an Ashby organization. By default returns Open, Closed, and Ar
| `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value |
| `perPage` | number | No | Number of results per page \(default 100\) |
| `status` | string | No | Filter by job status: Open, Closed, Archived, or Draft |
| `createdAfter` | string | No | Only return jobs created after this ISO 8601 timestamp \(e.g. 2024-01-01T00:00:00Z\) |
| `openedAfter` | string | No | Only return jobs opened after this ISO 8601 timestamp |
| `openedBefore` | string | No | Only return jobs opened before this ISO 8601 timestamp |
| `closedAfter` | string | No | Only return jobs closed after this ISO 8601 timestamp |
| `closedBefore` | string | No | Only return jobs closed before this ISO 8601 timestamp |
#### Output
@@ -770,6 +804,11 @@ Lists all locations configured in Ashby.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Ashby API Key |
| `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value |
| `perPage` | number | No | Number of results per page \(default and max 100\) |
| `syncToken` | string | No | Opaque token from a prior sync to fetch only items changed since then |
| `includeArchived` | boolean | No | When true, includes archived locations in results \(default false\) |
| `includeLocationHierarchy` | boolean | No | When true, includes location hierarchy components/regions \(default false\) |
#### Output
@@ -790,6 +829,10 @@ Lists all locations configured in Ashby.
| ↳ `addressLocality` | string | City or locality |
| ↳ `postalCode` | string | Postal code |
| ↳ `streetAddress` | string | Street address |
| ↳ `extraData` | json | Free-form key-value metadata |
| `moreDataAvailable` | boolean | Whether more pages of results exist |
| `nextCursor` | string | Opaque cursor for fetching the next page |
| `syncToken` | string | Opaque sync token returned after the last page; pass on next sync |
### `ashby_list_notes`
@@ -832,6 +875,9 @@ Lists all offers with their latest version in an Ashby organization.
| `apiKey` | string | Yes | Ashby API Key |
| `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value |
| `perPage` | number | No | Number of results per page |
| `createdAfter` | string | No | Only return offers created after this ISO 8601 timestamp \(e.g. 2024-01-01T00:00:00Z\) |
| `syncToken` | string | No | Opaque token from a prior sync to fetch only items changed since then |
| `applicationId` | string | No | Return only offers for the specified application UUID |
#### Output
@@ -852,6 +898,7 @@ Lists all openings in Ashby with pagination.
| `apiKey` | string | Yes | Ashby API Key |
| `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value |
| `perPage` | number | No | Number of results per page \(default 100\) |
| `createdAfter` | string | No | Only return openings created after this ISO 8601 timestamp \(e.g. 2024-01-01T00:00:00Z\) |
#### Output
@@ -869,6 +916,7 @@ Lists all candidate sources configured in Ashby.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Ashby API Key |
| `includeArchived` | boolean | No | When true, includes archived sources in results \(default false\) |
#### Output
@@ -894,6 +942,7 @@ Lists all users in Ashby with pagination.
| `apiKey` | string | Yes | Ashby API Key |
| `cursor` | string | No | Opaque pagination cursor from a previous response nextCursor value |
| `perPage` | number | No | Number of results per page \(default 100\) |
| `includeDeactivated` | boolean | No | When true, includes deactivated users in results \(default false\) |
#### Output
@@ -920,7 +969,7 @@ Removes a tag from a candidate in Ashby and returns the updated candidate.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `candidates` | json | List of candidates with rich fields \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], linkedInUrl, githubUrl, profileUrl, position, company, school, timezone, location with locationComponents\[\], tags\[\], applicationIds\[\], customFields\[\], resumeFileHandle, fileHandles\[\], source with sourceType, creditedToUser, fraudStatus, createdAt, updatedAt\) |
| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion, compensation with compensationTiers\[\]\) |
| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion\) |
| `applications` | json | List of applications \(id, status, customFields\[\], candidate summary, currentInterviewStage, source with sourceType, archiveReason with customFields\[\], archivedAt, job summary, creditedToUser, hiringTeam\[\], appliedViaJobPostingId, submitterClientIp, submitterUserAgent, createdAt, updatedAt\) |
| `notes` | json | List of notes \(id, content, author, isPrivate, createdAt\) |
| `offers` | json | List of offers \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion with id/startDate/salary/createdAt/openingId/customFields\[\]/fileHandles\[\]/author/approvalStatus\) |
@@ -931,20 +980,20 @@ Removes a tag from a candidate in Ashby and returns the updated candidate.
| `locations` | json | List of locations \(id, name, externalName, isArchived, isRemote, workplaceType, parentLocationId, type, address with addressCountry/Region/Locality/postalCode/streetAddress\) |
| `jobPostings` | json | List of job postings \(id, title, jobId, departmentName, teamName, locationName, locationIds, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensationTierSummary, shouldDisplayCompensationOnJobBoard, updatedAt\) |
| `openings` | json | List of openings \(id, openedAt, closedAt, isArchived, archivedAt, closeReasonId, openingState, latestVersion with identifier/description/authorId/createdAt/teamId/jobIds\[\]/targetHireDate/targetStartDate/isBackfill/employmentType/locationIds\[\]/hiringTeam\[\]/customFields\[\]\) |
| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId\) |
| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt\) |
| `interviewSchedules` | json | List of interview schedules \(id, applicationId, interviewStageId, interviewEvents\[\] with interviewerUserIds/startTime/endTime/feedbackLink/location/meetingLink/hasSubmittedFeedback, status, scheduledBy, createdAt, updatedAt\) |
| `tags` | json | List of candidate tags \(id, title, isArchived\) |
| `id` | string | Resource UUID |
| `name` | string | Resource name |
| `title` | string | Job title or job posting title |
| `status` | string | Status |
| `candidate` | json | Candidate details \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], customFields\[\], source, creditedToUser, createdAt, updatedAt\) |
| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], compensation, createdAt, updatedAt\) |
| `candidate` | json | Candidate summary \(id, name, primaryEmailAddress, primaryPhoneNumber\). For full candidate fields use the candidates list output or the get/create/update candidate operations. |
| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], createdAt, updatedAt\) |
| `application` | json | Application details \(id, status, customFields\[\], candidate, currentInterviewStage, source, archiveReason, job, hiringTeam\[\], createdAt, updatedAt\) |
| `offer` | json | Offer details \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion\) |
| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt\) |
| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt, job \[included when expandJob=true\]\) |
| `content` | string | Note content |
| `author` | json | Note author \(id, firstName, lastName, email, globalRole, isEnabled\) |
| `author` | json | Note author \(id, firstName, lastName, email\) |
| `isPrivate` | boolean | Whether the note is private |
| `createdAt` | string | ISO 8601 creation timestamp |
| `moreDataAvailable` | boolean | Whether more pages exist |
@@ -985,14 +1034,19 @@ Updates an existing candidate record in Ashby. Only provided fields are changed.
| `linkedInUrl` | string | No | LinkedIn profile URL |
| `githubUrl` | string | No | GitHub profile URL |
| `websiteUrl` | string | No | Personal website URL |
| `alternateEmail` | string | No | An additional email address to add to the candidate |
| `sourceId` | string | No | UUID of the source to attribute the candidate to |
| `creditedToUserId` | string | No | UUID of the Ashby user to credit with sourcing this candidate |
| `createdAt` | string | No | Backdated creation timestamp in ISO 8601. Only updatable if originally backdated. |
| `sendNotifications` | boolean | No | Whether to send a notification when the source is updated \(default true\) |
| `socialLinks` | json | No | Array of social link objects to set on the candidate, e.g. \[\{"type":"LinkedIn","url":"https://..."\}\]. Replaces existing social links. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `candidates` | json | List of candidates with rich fields \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], linkedInUrl, githubUrl, profileUrl, position, company, school, timezone, location with locationComponents\[\], tags\[\], applicationIds\[\], customFields\[\], resumeFileHandle, fileHandles\[\], source with sourceType, creditedToUser, fraudStatus, createdAt, updatedAt\) |
| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion, compensation with compensationTiers\[\]\) |
| `jobs` | json | List of jobs \(id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds\[\], customFields\[\], jobPostingIds\[\], customRequisitionId, brandId, hiringTeam\[\], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings\[\] with latestVersion\) |
| `applications` | json | List of applications \(id, status, customFields\[\], candidate summary, currentInterviewStage, source with sourceType, archiveReason with customFields\[\], archivedAt, job summary, creditedToUser, hiringTeam\[\], appliedViaJobPostingId, submitterClientIp, submitterUserAgent, createdAt, updatedAt\) |
| `notes` | json | List of notes \(id, content, author, isPrivate, createdAt\) |
| `offers` | json | List of offers \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion with id/startDate/salary/createdAt/openingId/customFields\[\]/fileHandles\[\]/author/approvalStatus\) |
@@ -1003,20 +1057,20 @@ Updates an existing candidate record in Ashby. Only provided fields are changed.
| `locations` | json | List of locations \(id, name, externalName, isArchived, isRemote, workplaceType, parentLocationId, type, address with addressCountry/Region/Locality/postalCode/streetAddress\) |
| `jobPostings` | json | List of job postings \(id, title, jobId, departmentName, teamName, locationName, locationIds, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensationTierSummary, shouldDisplayCompensationOnJobBoard, updatedAt\) |
| `openings` | json | List of openings \(id, openedAt, closedAt, isArchived, archivedAt, closeReasonId, openingState, latestVersion with identifier/description/authorId/createdAt/teamId/jobIds\[\]/targetHireDate/targetStartDate/isBackfill/employmentType/locationIds\[\]/hiringTeam\[\]/customFields\[\]\) |
| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId\) |
| `users` | json | List of users \(id, firstName, lastName, email, globalRole, isEnabled, updatedAt\) |
| `interviewSchedules` | json | List of interview schedules \(id, applicationId, interviewStageId, interviewEvents\[\] with interviewerUserIds/startTime/endTime/feedbackLink/location/meetingLink/hasSubmittedFeedback, status, scheduledBy, createdAt, updatedAt\) |
| `tags` | json | List of candidate tags \(id, title, isArchived\) |
| `id` | string | Resource UUID |
| `name` | string | Resource name |
| `title` | string | Job title or job posting title |
| `status` | string | Status |
| `candidate` | json | Candidate details \(id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses\[\], phoneNumbers\[\], socialLinks\[\], customFields\[\], source, creditedToUser, createdAt, updatedAt\) |
| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], compensation, createdAt, updatedAt\) |
| `candidate` | json | Candidate summary \(id, name, primaryEmailAddress, primaryPhoneNumber\). For full candidate fields use the candidates list output or the get/create/update candidate operations. |
| `job` | json | Job details \(id, title, status, employmentType, locationId, departmentId, hiringTeam\[\], author, location, openings\[\], createdAt, updatedAt\) |
| `application` | json | Application details \(id, status, customFields\[\], candidate, currentInterviewStage, source, archiveReason, job, hiringTeam\[\], createdAt, updatedAt\) |
| `offer` | json | Offer details \(id, decidedAt, applicationId, acceptanceStatus, offerStatus, latestVersion\) |
| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt\) |
| `jobPosting` | json | Job posting details \(id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy\[\], jobId, locationName, locationIds, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt, job \[included when expandJob=true\]\) |
| `content` | string | Note content |
| `author` | json | Note author \(id, firstName, lastName, email, globalRole, isEnabled\) |
| `author` | json | Note author \(id, firstName, lastName, email\) |
| `isPrivate` | boolean | Whether the note is private |
| `createdAt` | string | ISO 8601 creation timestamp |
| `moreDataAvailable` | boolean | Whether more pages exist |
@@ -81,7 +81,6 @@ Update a Confluence page using the Confluence API.
| `pageId` | string | Yes | Confluence page ID to update \(numeric ID from page URL or API\) |
| `title` | string | No | New title for the page |
| `content` | string | No | New content for the page in Confluence storage format |
| `version` | number | No | Version number of the page \(required for preventing conflicts\) |
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
#### Output
@@ -1091,6 +1090,8 @@ Delete a Confluence space.
| `ts` | string | ISO 8601 timestamp of the operation |
| `spaceId` | string | Deleted space ID |
| `deleted` | boolean | Deletion status |
| `longTaskId` | string | ID of the long-running deletion task; poll Confluence long-task API to track completion |
| `longTaskStatusLink` | string | Relative link to the long-task status endpoint |
### `confluence_list_spaces`
@@ -254,6 +254,8 @@ Parse uploaded documents (PDF, DOCX, HTML, etc.) into clean markdown using Firec
| `proxy` | string | No | Proxy mode: "basic" or "auto" |
| `zeroDataRetention` | boolean | No | Enable zero data retention. Defaults to false. |
| `apiKey` | string | Yes | Firecrawl API key |
| `pricing` | custom | No | No description |
| `metadata` | string | No | No description |
| `rateLimit` | string | No | No description |
#### Output
@@ -146,6 +146,78 @@ Get metadata for a specific file in Google Drive by its ID
| ↳ `md5Checksum` | string | MD5 hash |
| ↳ `version` | string | Version number |
### `google_drive_get_content`
Get content from a file in Google Drive with complete metadata (exports Google Workspace files automatically)
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `fileId` | string | Yes | The ID of the file to get content from |
| `mimeType` | string | No | The MIME type to export Google Workspace files to \(optional\) |
| `includeRevisions` | boolean | No | Whether to include revision history in the metadata \(default: true, returns first 100 revisions\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | File content as text \(Google Workspace files are exported\) |
| `metadata` | object | Complete file metadata from Google Drive |
| ↳ `id` | string | Google Drive file ID |
| ↳ `kind` | string | Resource type identifier |
| ↳ `name` | string | File name |
| ↳ `mimeType` | string | MIME type |
| ↳ `description` | string | File description |
| ↳ `originalFilename` | string | Original uploaded filename |
| ↳ `fullFileExtension` | string | Full file extension |
| ↳ `fileExtension` | string | File extension |
| ↳ `owners` | json | List of file owners |
| ↳ `permissions` | json | File permissions |
| ↳ `permissionIds` | json | Permission IDs |
| ↳ `shared` | boolean | Whether file is shared |
| ↳ `ownedByMe` | boolean | Whether owned by current user |
| ↳ `writersCanShare` | boolean | Whether writers can share |
| ↳ `viewersCanCopyContent` | boolean | Whether viewers can copy |
| ↳ `copyRequiresWriterPermission` | boolean | Whether copy requires writer permission |
| ↳ `sharingUser` | json | User who shared the file |
| ↳ `starred` | boolean | Whether file is starred |
| ↳ `trashed` | boolean | Whether file is in trash |
| ↳ `explicitlyTrashed` | boolean | Whether explicitly trashed |
| ↳ `appProperties` | json | App-specific properties |
| ↳ `createdTime` | string | File creation time |
| ↳ `modifiedTime` | string | Last modification time |
| ↳ `modifiedByMeTime` | string | When modified by current user |
| ↳ `viewedByMeTime` | string | When last viewed by current user |
| ↳ `sharedWithMeTime` | string | When shared with current user |
| ↳ `lastModifyingUser` | json | User who last modified the file |
| ↳ `viewedByMe` | boolean | Whether viewed by current user |
| ↳ `modifiedByMe` | boolean | Whether modified by current user |
| ↳ `webViewLink` | string | URL to view in browser |
| ↳ `webContentLink` | string | Direct download URL |
| ↳ `iconLink` | string | URL to file icon |
| ↳ `thumbnailLink` | string | URL to thumbnail |
| ↳ `exportLinks` | json | Export format links |
| ↳ `size` | string | File size in bytes |
| ↳ `quotaBytesUsed` | string | Storage quota used |
| ↳ `md5Checksum` | string | MD5 hash |
| ↳ `sha1Checksum` | string | SHA-1 hash |
| ↳ `sha256Checksum` | string | SHA-256 hash |
| ↳ `parents` | json | Parent folder IDs |
| ↳ `spaces` | json | Spaces containing file |
| ↳ `driveId` | string | Shared drive ID |
| ↳ `capabilities` | json | User capabilities on file |
| ↳ `version` | string | Version number |
| ↳ `headRevisionId` | string | Head revision ID |
| ↳ `hasThumbnail` | boolean | Whether has thumbnail |
| ↳ `thumbnailVersion` | string | Thumbnail version |
| ↳ `imageMediaMetadata` | json | Image-specific metadata |
| ↳ `videoMediaMetadata` | json | Video-specific metadata |
| ↳ `isAppAuthorized` | boolean | Whether created by requesting app |
| ↳ `contentRestrictions` | json | Content restrictions |
| ↳ `linkShareMetadata` | json | Link share metadata |
| ↳ `revisions` | json | File revision history \(first 100 revisions only\) |
### `google_drive_create_folder`
Create a new folder in Google Drive with complete metadata returned
@@ -375,6 +447,79 @@ Create a copy of a file in Google Drive
| ↳ `owners` | json | List of file owners |
| ↳ `size` | string | File size in bytes |
### `google_drive_move`
Move a file or folder to a different folder in Google Drive
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `fileId` | string | Yes | The ID of the file or folder to move |
| `destinationFolderId` | string | Yes | The ID of the destination folder |
| `removeFromCurrent` | boolean | No | Whether to remove the file from its current parent folder \(default: true\). Set to false to add the file to the destination without removing it from the current location. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `file` | json | The moved file metadata |
| ↳ `id` | string | Google Drive file ID |
| ↳ `kind` | string | Resource type identifier |
| ↳ `name` | string | File name |
| ↳ `mimeType` | string | MIME type |
| ↳ `webViewLink` | string | URL to view in browser |
| ↳ `parents` | json | Parent folder IDs |
| ↳ `createdTime` | string | File creation time |
| ↳ `modifiedTime` | string | Last modification time |
| ↳ `owners` | json | List of file owners |
| ↳ `size` | string | File size in bytes |
### `google_drive_search`
Search for files in Google Drive using advanced query syntax (e.g., fullText contains, mimeType, modifiedTime, etc.)
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `query` | string | Yes | Google Drive query string using advanced search syntax \(e.g., "fullText contains \'budget\'", "mimeType = \'application/pdf\'", "modifiedTime &gt; \'2024-01-01\'"\) |
| `pageSize` | number | No | Maximum number of files to return \(default: 100\) |
| `pageToken` | string | No | Token for fetching the next page of results |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `files` | array | Array of file metadata objects matching the search query |
| ↳ `id` | string | Google Drive file ID |
| ↳ `kind` | string | Resource type identifier |
| ↳ `name` | string | File name |
| ↳ `mimeType` | string | MIME type |
| ↳ `description` | string | File description |
| ↳ `originalFilename` | string | Original uploaded filename |
| ↳ `fullFileExtension` | string | Full file extension |
| ↳ `fileExtension` | string | File extension |
| ↳ `owners` | json | List of file owners |
| ↳ `permissions` | json | File permissions |
| ↳ `shared` | boolean | Whether file is shared |
| ↳ `ownedByMe` | boolean | Whether owned by current user |
| ↳ `starred` | boolean | Whether file is starred |
| ↳ `trashed` | boolean | Whether file is in trash |
| ↳ `createdTime` | string | File creation time |
| ↳ `modifiedTime` | string | Last modification time |
| ↳ `lastModifyingUser` | json | User who last modified the file |
| ↳ `webViewLink` | string | URL to view in browser |
| ↳ `webContentLink` | string | Direct download URL |
| ↳ `iconLink` | string | URL to file icon |
| ↳ `thumbnailLink` | string | URL to thumbnail |
| ↳ `size` | string | File size in bytes |
| ↳ `parents` | json | Parent folder IDs |
| ↳ `driveId` | string | Shared drive ID |
| ↳ `capabilities` | json | User capabilities on file |
| ↳ `version` | string | Version number |
| `nextPageToken` | string | Token for fetching the next page of results |
### `google_drive_update`
Update file metadata in Google Drive (rename, move, star, add description)
@@ -428,6 +573,29 @@ Move a file to the trash in Google Drive (can be restored later)
| ↳ `trashedTime` | string | When file was trashed |
| ↳ `webViewLink` | string | URL to view in browser |
### `google_drive_untrash`
Restore a file from the trash in Google Drive
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `fileId` | string | Yes | The ID of the file to restore from trash |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `file` | json | The restored file metadata |
| ↳ `id` | string | Google Drive file ID |
| ↳ `kind` | string | Resource type identifier |
| ↳ `name` | string | File name |
| ↳ `mimeType` | string | MIME type |
| ↳ `trashed` | boolean | Whether file is in trash \(should be false\) |
| ↳ `webViewLink` | string | URL to view in browser |
| ↳ `parents` | json | Parent folder IDs |
### `google_drive_delete`
Permanently delete a file from Google Drive (bypasses trash)
@@ -505,6 +673,7 @@ List all permissions (who has access) for a file in Google Drive
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `fileId` | string | Yes | The ID of the file to list permissions for |
| `pageToken` | string | No | The page token to use for pagination |
#### Output
+3 -3
View File
@@ -384,7 +384,7 @@ Assign a Jira issue to a user
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `issueKey` | string | Yes | Jira issue key to assign \(e.g., PROJ-123\) |
| `accountId` | string | Yes | Account ID of the user to assign the issue to. Use "-1" for automatic assignment or null to unassign. |
| `accountId` | string | Yes | Account ID of the user to assign the issue to. Use "-1" for automatic assignment, or leave empty / pass "null" to unassign. |
| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. |
#### Output
@@ -436,7 +436,7 @@ Search for Jira issues using JQL (Jira Query Language)
| `jql` | string | Yes | JQL query string to search for issues \(e.g., "project = PROJ AND status = Open"\) |
| `nextPageToken` | string | No | Cursor token for the next page of results. Omit for the first page. |
| `maxResults` | number | No | Maximum number of results to return per page \(default: 50\) |
| `fields` | array | No | Array of field names to return \(default: all navigable\). Use "*all" for every field. |
| `fields` | array | No | Array of field names to return \(default: all fields\). |
| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. |
#### Output
@@ -506,7 +506,7 @@ Search for Jira issues using JQL (Jira Query Language)
| ↳ `updated` | string | ISO 8601 timestamp when the issue was last updated |
| `nextPageToken` | string | Cursor token for the next page. Null when no more results. |
| `isLast` | boolean | Whether this is the last page of results |
| `total` | number | Total number of matching issues \(may not always be available\) |
| `total` | number | Always null. The Jira /search/jql endpoint does not return a total count; use isLast and nextPageToken for pagination. |
### `jira_add_comment`
@@ -331,8 +331,7 @@ Add customers to a service desk in Jira Service Management
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `cloudId` | string | No | Jira Cloud ID for the instance |
| `serviceDeskId` | string | Yes | Service Desk ID \(e.g., "1", "2"\) |
| `accountIds` | string | No | Comma-separated Atlassian account IDs to add as customers |
| `emails` | string | No | Comma-separated email addresses to add as customers |
| `accountIds` | string | Yes | Comma-separated Atlassian account IDs to add as customers |
#### Output
+6 -3
View File
@@ -170,8 +170,6 @@ Create and share Slack canvases in channels. Canvases are collaborative document
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `canvas_id` | string | Unique canvas identifier |
| `channel` | string | Channel where canvas was created |
| `title` | string | Canvas title |
### `slack_message_reader`
@@ -522,6 +520,7 @@ List all channels in a Slack workspace. Returns public and private channels the
| `includePrivate` | boolean | No | Include private channels the bot is a member of \(default: true\) |
| `excludeArchived` | boolean | No | Exclude archived channels \(default: true\) |
| `limit` | number | No | Maximum number of channels to return \(default: 100, max: 200\) |
| `cursor` | string | No | Pagination cursor from a previous response.next_cursor |
#### Output
@@ -547,6 +546,7 @@ List all channels in a Slack workspace. Returns public and private channels the
| `ids` | array | Array of channel IDs for easy access |
| `names` | array | Array of channel names for easy access |
| `count` | number | Total number of channels returned |
| `nextCursor` | string | Cursor for the next page; null if no more pages |
### `slack_list_members`
@@ -560,6 +560,7 @@ List all members (user IDs) in a Slack channel. Use with Get User Info to resolv
| `botToken` | string | No | Bot token for Custom Bot |
| `channel` | string | Yes | Channel ID to list members from |
| `limit` | number | No | Maximum number of members to return \(default: 100, max: 200\) |
| `cursor` | string | No | Pagination cursor from a previous response.next_cursor |
#### Output
@@ -567,6 +568,7 @@ List all members (user IDs) in a Slack channel. Use with Get User Info to resolv
| --------- | ---- | ----------- |
| `members` | array | Array of user IDs who are members of the channel \(e.g., U1234567890\) |
| `count` | number | Total number of members returned |
| `nextCursor` | string | Cursor for the next page; null if no more pages |
### `slack_list_users`
@@ -580,6 +582,7 @@ List all users in a Slack workspace. Returns user profiles with names and avatar
| `botToken` | string | No | Bot token for Custom Bot |
| `includeDeleted` | boolean | No | Include deactivated/deleted users \(default: false\) |
| `limit` | number | No | Maximum number of users to return \(default: 100, max: 200\) |
| `cursor` | string | No | Pagination cursor from a previous response.next_cursor |
#### Output
@@ -602,6 +605,7 @@ List all users in a Slack workspace. Returns user profiles with names and avatar
| `ids` | array | Array of user IDs for easy access |
| `names` | array | Array of usernames for easy access |
| `count` | number | Total number of users returned |
| `nextCursor` | string | Cursor for the next page; null if no more pages |
### `slack_get_user`
@@ -638,7 +642,6 @@ Get detailed information about a specific Slack user by their user ID.
| ↳ `is_restricted` | boolean | Whether the user is a guest \(restricted\) |
| ↳ `is_ultra_restricted` | boolean | Whether the user is a single-channel guest |
| ↳ `is_app_user` | boolean | Whether user is an app user |
| ↳ `is_stranger` | boolean | Whether user is from different workspace |
| ↳ `deleted` | boolean | Whether the user is deactivated |
| ↳ `color` | string | User color for display |
| ↳ `timezone` | string | Timezone identifier \(e.g., America/Los_Angeles\) |
@@ -5077,10 +5077,18 @@
"name": "List Files",
"description": "List Google Drive files"
},
{
"name": "Search Files",
"description": "Search for files in Google Drive using advanced query syntax (e.g., fullText contains, mimeType, modifiedTime, etc.)"
},
{
"name": "Get File Info",
"description": "Get metadata for a specific file in Google Drive by its ID"
},
{
"name": "Get File Content",
"description": "Get content from a file in Google Drive with complete metadata (exports Google Workspace files automatically)"
},
{
"name": "Create Folder",
"description": "Create a new folder in Google Drive with complete metadata returned"
@@ -5101,6 +5109,10 @@
"name": "Copy File",
"description": "Create a copy of a file in Google Drive"
},
{
"name": "Move File",
"description": "Move a file or folder to a different folder in Google Drive"
},
{
"name": "Update File",
"description": "Update file metadata in Google Drive (rename, move, star, add description)"
@@ -5109,6 +5121,10 @@
"name": "Move to Trash",
"description": "Move a file to the trash in Google Drive (can be restored later)"
},
{
"name": "Restore from Trash",
"description": "Restore a file from the trash in Google Drive"
},
{
"name": "Delete Permanently",
"description": "Permanently delete a file from Google Drive (bypasses trash)"
@@ -5130,7 +5146,7 @@
"description": "Get information about the user and their Google Drive (storage quota, capabilities)"
}
],
"operationCount": 14,
"operationCount": 18,
"triggers": [
{
"id": "google_drive_poller",
@@ -6962,6 +6978,10 @@
"name": "Read Issue",
"description": "Retrieve detailed information about a specific Jira issue"
},
{
"name": "Read Bulk Issues",
"description": "Retrieve multiple Jira issues from a project in bulk"
},
{
"name": "Update Issue",
"description": "Update a Jira issue"
@@ -7055,7 +7075,7 @@
"description": "Search for Jira users by email address or display name. Returns matching users with their accountId, displayName, and emailAddress."
}
],
"operationCount": 24,
"operationCount": 25,
"triggers": [
{
"id": "jira_issue_created",
@@ -73,15 +73,26 @@ export const PUT = withRouteHandler(async (request: NextRequest) => {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
// Get current comment version
const getUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/footer-comments/${commentId}`
const getResponse = await fetch(getUrl, {
// Detect comment type — try footer-comments first, fall back to inline-comments
const apiBase = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2`
let commentEndpoint = 'footer-comments'
let getResponse = await fetch(`${apiBase}/footer-comments/${commentId}`, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${accessToken}`,
},
})
if (getResponse.status === 404) {
commentEndpoint = 'inline-comments'
getResponse = await fetch(`${apiBase}/inline-comments/${commentId}`, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${accessToken}`,
},
})
}
if (!getResponse.ok) {
const errorText = await getResponse.text()
throw new Error(
@@ -92,7 +103,7 @@ export const PUT = withRouteHandler(async (request: NextRequest) => {
const currentComment = await getResponse.json()
const currentVersion = currentComment.version?.number || 1
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/footer-comments/${commentId}`
const url = `${apiBase}/${commentEndpoint}/${commentId}`
const updateBody = {
body: {
@@ -164,9 +175,48 @@ export const DELETE = withRouteHandler(async (request: NextRequest) => {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/footer-comments/${commentId}`
const apiBase = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2`
const response = await fetch(url, {
// Detect comment type with a non-destructive GET so a 404 from a prior
// deletion isn't masked by a second DELETE attempt against the wrong endpoint.
let commentEndpoint = 'footer-comments'
let detectResponse = await fetch(`${apiBase}/footer-comments/${commentId}`, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${accessToken}`,
},
})
if (detectResponse.status === 404) {
commentEndpoint = 'inline-comments'
detectResponse = await fetch(`${apiBase}/inline-comments/${commentId}`, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${accessToken}`,
},
})
}
if (!detectResponse.ok) {
const errorText = await detectResponse.text()
logger.error('Confluence API error response:', {
status: detectResponse.status,
statusText: detectResponse.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(
detectResponse.status,
detectResponse.statusText,
errorText
),
},
{ status: detectResponse.status }
)
}
const response = await fetch(`${apiBase}/${commentEndpoint}/${commentId}`, {
method: 'DELETE',
headers: {
Accept: 'application/json',
@@ -28,7 +28,7 @@ const updatePropertySchema = z.object({
propertyId: z.string().min(1, 'Property ID is required'),
key: z.string().min(1, 'Property key is required'),
value: z.any(),
versionNumber: z.number().min(1, 'Version number is required'),
versionNumber: z.number().min(1).optional(),
})
const deletePropertySchema = z.object({
@@ -256,6 +256,39 @@ export const PUT = withRouteHandler(async (request: NextRequest) => {
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/properties/${propertyId}`
let nextVersion = versionNumber
if (nextVersion === undefined) {
const lookupResponse = await fetch(url, {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${accessToken}`,
},
})
if (!lookupResponse.ok) {
const errorText = await lookupResponse.text()
return NextResponse.json(
{
error: parseAtlassianErrorMessage(
lookupResponse.status,
lookupResponse.statusText,
errorText
),
},
{ status: lookupResponse.status }
)
}
const current = await lookupResponse.json()
const currentNumber = current?.version?.number
if (typeof currentNumber !== 'number') {
return NextResponse.json(
{ error: 'Could not determine current property version' },
{ status: 500 }
)
}
nextVersion = currentNumber + 1
}
const response = await fetch(url, {
method: 'PUT',
headers: {
@@ -266,7 +299,7 @@ export const PUT = withRouteHandler(async (request: NextRequest) => {
body: JSON.stringify({
key,
value,
version: { number: versionNumber },
version: { number: nextVersion },
}),
})
@@ -200,8 +200,6 @@ export const PUT = withRouteHandler(async (request: NextRequest) => {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}`
if (!name && description === undefined) {
return NextResponse.json(
{ error: 'At least one of name or description is required for update' },
@@ -209,39 +207,38 @@ export const PUT = withRouteHandler(async (request: NextRequest) => {
)
}
const updateBody: Record<string, unknown> = {}
if (name) {
updateBody.name = name
} else {
const currentResponse = await fetch(url, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${accessToken}`,
const lookupUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}`
const lookupResponse = await fetch(lookupUrl, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${accessToken}`,
},
})
if (!lookupResponse.ok) {
const errorText = await lookupResponse.text()
return NextResponse.json(
{
error: parseAtlassianErrorMessage(
lookupResponse.status,
lookupResponse.statusText,
errorText
),
},
})
if (!currentResponse.ok) {
const errorText = await currentResponse.text()
return NextResponse.json(
{
error: parseAtlassianErrorMessage(
currentResponse.status,
currentResponse.statusText,
errorText
),
},
{ status: currentResponse.status }
)
}
const currentSpace = await currentResponse.json()
updateBody.name = currentSpace.name
{ status: lookupResponse.status }
)
}
const currentSpace = await lookupResponse.json()
const spaceKey = currentSpace.key
const updateBody: Record<string, unknown> = {
name: name || currentSpace.name,
}
if (description !== undefined) {
updateBody.description = { value: description, representation: 'plain' }
updateBody.description = { plain: { value: description, representation: 'plain' } }
}
logger.info(`Updating space ${spaceId}`)
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/rest/api/space/${encodeURIComponent(spaceKey)}`
logger.info(`Updating space ${spaceKey}`)
const response = await fetch(url, {
method: 'PUT',
@@ -315,9 +312,32 @@ export const DELETE = withRouteHandler(async (request: NextRequest) => {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}`
const lookupUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}`
const lookupResponse = await fetch(lookupUrl, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${accessToken}`,
},
})
if (!lookupResponse.ok) {
const errorText = await lookupResponse.text()
return NextResponse.json(
{
error: parseAtlassianErrorMessage(
lookupResponse.status,
lookupResponse.statusText,
errorText
),
},
{ status: lookupResponse.status }
)
}
const currentSpace = await lookupResponse.json()
const spaceKey = currentSpace.key
logger.info(`Deleting space ${spaceId}`)
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/rest/api/space/${encodeURIComponent(spaceKey)}`
logger.info(`Deleting space ${spaceKey}`)
const response = await fetch(url, {
method: 'DELETE',
@@ -340,7 +360,26 @@ export const DELETE = withRouteHandler(async (request: NextRequest) => {
)
}
return NextResponse.json({ spaceId, deleted: true })
let longTask: { id?: string; statusLink?: string } = {}
try {
const text = await response.text()
if (text) {
const data = JSON.parse(text)
longTask = {
id: data?.id,
statusLink: data?.links?.status,
}
}
} catch {
// 204 No Content or non-JSON body — ignore
}
return NextResponse.json({
spaceId,
deleted: true,
longTaskId: longTask.id,
longTaskStatusLink: longTask.statusLink,
})
} catch (error) {
logger.error('Error deleting Confluence space:', error)
return NextResponse.json(
+26 -18
View File
@@ -41,6 +41,20 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
return NextResponse.json({ issues: [] })
}
const ISSUE_KEY_RE = /^[A-Za-z][A-Za-z0-9_]*-\d+$/
const sanitizedKeys: string[] = []
for (const k of issueKeys) {
if (typeof k !== 'string') continue
const trimmed = k.trim()
if (!ISSUE_KEY_RE.test(trimmed)) {
return NextResponse.json({ error: `Invalid Jira issue key: "${trimmed}"` }, { status: 400 })
}
sanitizedKeys.push(trimmed)
}
if (sanitizedKeys.length === 0) {
return NextResponse.json({ issues: [] })
}
const cloudId = providedCloudId || (await getJiraCloudId(domain!, accessToken!))
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
@@ -49,11 +63,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
}
// Use search/jql endpoint (GET) with URL parameters
const jql = `issueKey in (${issueKeys.map((k: string) => k.trim()).join(',')})`
const jql = `issueKey in (${sanitizedKeys.join(',')})`
const params = new URLSearchParams({
jql,
fields: 'summary,status,assignee,updated,project',
maxResults: String(Math.min(issueKeys.length, 100)),
maxResults: String(Math.min(sanitizedKeys.length, 100)),
})
const searchUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/search/jql?${params.toString()}`
@@ -154,14 +168,13 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
const target = Math.min(all ? limit || SAFETY_CAP : 25, SAFETY_CAP)
const projectKey = (projectId || manualProjectId || '').trim()
const escapeJql = (s: string) => s.replace(/"/g, '\\"')
const escapeJql = (s: string) => s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
const buildJql = (startAt: number) => {
const buildUrl = (token?: string) => {
const jqlParts: string[] = []
if (projectKey) jqlParts.push(`project = ${projectKey}`)
if (projectKey) jqlParts.push(`project = "${escapeJql(projectKey)}"`)
if (query) {
const q = escapeJql(query)
// Match by key prefix or summary text
jqlParts.push(`(key ~ "${q}" OR summary ~ "${q}")`)
}
const jql = `${jqlParts.length ? `${jqlParts.join(' AND ')} ` : ''}ORDER BY updated DESC`
@@ -170,20 +183,15 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
fields: 'summary,key,updated',
maxResults: String(Math.min(PAGE_SIZE, target)),
})
if (startAt > 0) {
params.set('startAt', String(startAt))
}
return {
url: `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/search/jql?${params.toString()}`,
}
if (token) params.set('nextPageToken', token)
return `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/search/jql?${params.toString()}`
}
let startAt = 0
let nextPageToken: string | undefined
let collected: any[] = []
let total = 0
do {
const { url: apiUrl } = buildJql(startAt)
const apiUrl = buildUrl(nextPageToken)
const response = await fetch(apiUrl, {
method: 'GET',
headers: {
@@ -209,10 +217,10 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
const page = await response.json()
const issues = page.issues || []
total = page.total || issues.length
collected = collected.concat(issues)
startAt += PAGE_SIZE
} while (all && collected.length < Math.min(total, target))
nextPageToken = page.nextPageToken
if (!nextPageToken || issues.length === 0) break
} while (all && collected.length < target)
const issues = collected.slice(0, target).map((it: any) => ({
key: it.key,
+5 -3
View File
@@ -80,7 +80,8 @@ export const PUT = withRouteHandler(async (request: NextRequest) => {
return NextResponse.json({ error: issueKeyValidation.error }, { status: 400 })
}
const notifyParam = notifyUsers === false ? '?notifyUsers=false' : ''
const notifyParam =
notifyUsers === false ? '?notifyUsers=false' : notifyUsers === true ? '?notifyUsers=true' : ''
const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${issueKey}${notifyParam}`
logger.info('Updating Jira issue at:', url)
@@ -170,7 +171,8 @@ export const PUT = withRouteHandler(async (request: NextRequest) => {
)
}
const responseData = response.status === 204 ? {} : await response.json()
const responseData =
response.status === 204 ? {} : await response.json().catch(() => ({}) as Record<string, any>)
logger.info('Successfully updated Jira issue:', issueKey)
return NextResponse.json({
@@ -178,7 +180,7 @@ export const PUT = withRouteHandler(async (request: NextRequest) => {
output: {
ts: new Date().toISOString(),
issueKey: responseData.key || issueKey,
summary: responseData.fields?.summary || 'Issue updated',
summary: responseData.fields?.summary || summaryValue || 'Issue updated',
success: true,
},
})
+5 -1
View File
@@ -91,7 +91,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
}
if (parent !== undefined && parent !== null && parent !== '') {
fields.parent = parent
if (typeof parent === 'string') {
fields.parent = /^\d+$/.test(parent) ? { id: parent } : { key: parent }
} else if (typeof parent === 'object') {
fields.parent = parent
}
}
if (priority !== undefined && priority !== null && priority !== '') {
+26 -19
View File
@@ -28,7 +28,6 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
start,
limit,
accountIds,
emails,
} = body
if (!domain) {
@@ -46,6 +45,16 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
return NextResponse.json({ error: 'Service Desk ID is required' }, { status: 400 })
}
if (body.emails !== undefined) {
return NextResponse.json(
{
error:
'The `emails` parameter is no longer supported. Use `accountIds` (Atlassian account IDs) instead.',
},
{ status: 400 }
)
}
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
@@ -60,33 +69,31 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
const baseUrl = getJsmApiBaseUrl(cloudId)
const rawIds = accountIds || emails
const parsedAccountIds = rawIds
? typeof rawIds === 'string'
? rawIds
.split(',')
.map((id: string) => id.trim())
.filter((id: string) => id)
: Array.isArray(rawIds)
? rawIds
: []
: []
const splitCsv = (value: unknown): string[] =>
value
? typeof value === 'string'
? value
.split(',')
.map((v: string) => v.trim())
.filter((v: string) => v)
: Array.isArray(value)
? (value as string[])
: []
: []
const isAddOperation = parsedAccountIds.length > 0
const parsedAccountIds = splitCsv(accountIds)
if (isAddOperation) {
if (parsedAccountIds.length > 0) {
const url = `${baseUrl}/servicedesk/${serviceDeskId}/customer`
logger.info('Adding customers to:', url, { accountIds: parsedAccountIds })
const requestBody: Record<string, unknown> = {
logger.info('Adding customers to:', url, {
accountIds: parsedAccountIds,
}
})
const response = await fetch(url, {
method: 'POST',
headers: getJsmHeaders(accessToken),
body: JSON.stringify(requestBody),
body: JSON.stringify({ accountIds: parsedAccountIds }),
})
if (!response.ok) {
@@ -130,6 +130,14 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
return NextResponse.json({ error: organizationIdValidation.error }, { status: 400 })
}
const orgIdNumeric = Number.parseInt(String(organizationId).trim(), 10)
if (!Number.isFinite(orgIdNumeric) || orgIdNumeric <= 0) {
return NextResponse.json(
{ error: 'organizationId must be a positive integer' },
{ status: 400 }
)
}
const url = `${baseUrl}/servicedesk/${serviceDeskId}/organization`
logger.info('Adding organization to service desk:', { serviceDeskId, organizationId })
@@ -137,7 +145,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
const response = await fetch(url, {
method: 'POST',
headers: getJsmHeaders(accessToken),
body: JSON.stringify({ organizationId: Number.parseInt(organizationId, 10) }),
body: JSON.stringify({ organizationId: orgIdNumeric }),
})
if (response.status === 204 || response.ok) {
+311 -32
View File
@@ -151,6 +151,40 @@ export const AshbyBlock: BlockConfig = {
},
mode: 'advanced',
},
{
id: 'website',
title: 'Website URL',
type: 'short-input',
placeholder: 'https://example.com',
condition: { field: 'operation', value: 'create_candidate' },
mode: 'advanced',
},
{
id: 'alternateEmail',
title: 'Alternate Email',
type: 'short-input',
placeholder: 'Additional email address',
condition: { field: 'operation', value: 'update_candidate' },
mode: 'advanced',
},
{
id: 'candidateCreatedAt',
title: 'Created At',
type: 'short-input',
placeholder: 'e.g. 2024-01-01T00:00:00Z',
condition: { field: 'operation', value: ['create_candidate', 'update_candidate'] },
mode: 'advanced',
wandConfig: {
enabled: true,
prompt: `Generate an ISO 8601 timestamp based on the user's description.
Examples:
- "last week" -> One week ago from today at 00:00:00Z
- "January 1st 2024" -> 2024-01-01T00:00:00Z
- "30 days ago" -> 30 days before today at 00:00:00Z
Output only the ISO 8601 timestamp string, nothing else.`,
generationType: 'timestamp',
},
},
{
id: 'updateName',
title: 'Name',
@@ -234,8 +268,11 @@ export const AshbyBlock: BlockConfig = {
id: 'creditedToUserId',
title: 'Credited To User ID',
type: 'short-input',
placeholder: 'User UUID the application is credited to',
condition: { field: 'operation', value: 'create_application' },
placeholder: 'User UUID credited as the source of this record',
condition: {
field: 'operation',
value: ['create_application', 'create_candidate', 'update_candidate'],
},
mode: 'advanced',
},
{
@@ -281,9 +318,33 @@ Output only the ISO 8601 timestamp string, nothing else.`,
id: 'sendNotifications',
title: 'Send Notifications',
type: 'switch',
condition: { field: 'operation', value: ['create_note', 'update_candidate'] },
mode: 'advanced',
},
{
id: 'isPrivate',
title: 'Private Note',
type: 'switch',
condition: { field: 'operation', value: 'create_note' },
mode: 'advanced',
},
{
id: 'noteCreatedAt',
title: 'Created At',
type: 'short-input',
placeholder: 'e.g. 2024-01-01T00:00:00Z',
condition: { field: 'operation', value: 'create_note' },
mode: 'advanced',
wandConfig: {
enabled: true,
prompt: `Generate an ISO 8601 timestamp based on the user's description.
Examples:
- "yesterday" -> Yesterday at 00:00:00Z
- "January 1st 2024" -> 2024-01-01T00:00:00Z
Output only the ISO 8601 timestamp string, nothing else.`,
generationType: 'timestamp',
},
},
{
id: 'filterStatus',
title: 'Status Filter',
@@ -307,12 +368,30 @@ Output only the ISO 8601 timestamp string, nothing else.`,
condition: { field: 'operation', value: 'list_applications' },
mode: 'advanced',
},
{
id: 'filterCandidateId',
title: 'Candidate ID Filter',
type: 'short-input',
placeholder: 'Filter by candidate UUID',
condition: { field: 'operation', value: 'list_applications' },
mode: 'advanced',
},
{
id: 'createdAfter',
title: 'Created After',
type: 'short-input',
placeholder: 'e.g. 2024-01-01T00:00:00Z',
condition: { field: 'operation', value: 'list_applications' },
condition: {
field: 'operation',
value: [
'list_applications',
'list_candidates',
'list_jobs',
'list_offers',
'list_openings',
'list_interviews',
],
},
mode: 'advanced',
wandConfig: {
enabled: true,
@@ -322,6 +401,62 @@ Examples:
- "January 1st 2024" -> 2024-01-01T00:00:00Z
- "30 days ago" -> 30 days before today at 00:00:00Z
- "start of this month" -> First day of current month at 00:00:00Z
Output only the ISO 8601 timestamp string, nothing else.`,
generationType: 'timestamp',
},
},
{
id: 'openedAfter',
title: 'Opened After',
type: 'short-input',
placeholder: 'e.g. 2024-01-01T00:00:00Z',
condition: { field: 'operation', value: 'list_jobs' },
mode: 'advanced',
wandConfig: {
enabled: true,
prompt: `Generate an ISO 8601 timestamp based on the user's description.
Output only the ISO 8601 timestamp string, nothing else.`,
generationType: 'timestamp',
},
},
{
id: 'openedBefore',
title: 'Opened Before',
type: 'short-input',
placeholder: 'e.g. 2024-12-31T23:59:59Z',
condition: { field: 'operation', value: 'list_jobs' },
mode: 'advanced',
wandConfig: {
enabled: true,
prompt: `Generate an ISO 8601 timestamp based on the user's description.
Output only the ISO 8601 timestamp string, nothing else.`,
generationType: 'timestamp',
},
},
{
id: 'closedAfter',
title: 'Closed After',
type: 'short-input',
placeholder: 'e.g. 2024-01-01T00:00:00Z',
condition: { field: 'operation', value: 'list_jobs' },
mode: 'advanced',
wandConfig: {
enabled: true,
prompt: `Generate an ISO 8601 timestamp based on the user's description.
Output only the ISO 8601 timestamp string, nothing else.`,
generationType: 'timestamp',
},
},
{
id: 'closedBefore',
title: 'Closed Before',
type: 'short-input',
placeholder: 'e.g. 2024-12-31T23:59:59Z',
condition: { field: 'operation', value: 'list_jobs' },
mode: 'advanced',
wandConfig: {
enabled: true,
prompt: `Generate an ISO 8601 timestamp based on the user's description.
Output only the ISO 8601 timestamp string, nothing else.`,
generationType: 'timestamp',
},
@@ -358,6 +493,9 @@ Output only the ISO 8601 timestamp string, nothing else.`,
'list_users',
'list_interviews',
'list_candidate_tags',
'list_locations',
'list_departments',
'list_custom_fields',
],
},
mode: 'advanced',
@@ -379,6 +517,9 @@ Output only the ISO 8601 timestamp string, nothing else.`,
'list_users',
'list_interviews',
'list_candidate_tags',
'list_locations',
'list_departments',
'list_custom_fields',
],
},
mode: 'advanced',
@@ -388,7 +529,47 @@ Output only the ISO 8601 timestamp string, nothing else.`,
title: 'Sync Token',
type: 'short-input',
placeholder: 'Sync token for incremental updates',
condition: { field: 'operation', value: 'list_candidate_tags' },
condition: {
field: 'operation',
value: [
'list_candidate_tags',
'list_locations',
'list_departments',
'list_custom_fields',
'list_offers',
],
},
mode: 'advanced',
},
{
id: 'includeLocationHierarchy',
title: 'Include Location Hierarchy',
type: 'switch',
condition: { field: 'operation', value: 'list_locations' },
mode: 'advanced',
},
{
id: 'offerApplicationId',
title: 'Application ID Filter',
type: 'short-input',
placeholder: 'Filter offers by application UUID',
condition: { field: 'operation', value: 'list_offers' },
mode: 'advanced',
},
{
id: 'alternateEmailAddresses',
title: 'Alternate Email Addresses',
type: 'long-input',
placeholder: 'Comma-separated or JSON array (e.g. ["a@x.com","b@x.com"])',
condition: { field: 'operation', value: 'create_candidate' },
mode: 'advanced',
},
{
id: 'socialLinks',
title: 'Social Links',
type: 'long-input',
placeholder: 'JSON array (e.g. [{"type":"Twitter","url":"https://twitter.com/x"}])',
condition: { field: 'operation', value: 'update_candidate' },
mode: 'advanced',
},
{
@@ -397,20 +578,58 @@ Output only the ISO 8601 timestamp string, nothing else.`,
type: 'switch',
condition: {
field: 'operation',
value: ['list_candidate_tags', 'list_archive_reasons'],
value: [
'list_candidate_tags',
'list_archive_reasons',
'list_sources',
'list_departments',
'list_custom_fields',
'list_locations',
],
},
mode: 'advanced',
},
{
id: 'expandApplicationFormDefinition',
title: 'Include Application Form Definition',
id: 'includeDeactivated',
title: 'Include Deactivated',
type: 'switch',
condition: { field: 'operation', value: 'get_job_posting' },
condition: { field: 'operation', value: 'list_users' },
mode: 'advanced',
},
{
id: 'expandSurveyFormDefinitions',
title: 'Include Survey Form Definitions',
id: 'jobBoardId',
title: 'Job Board ID',
type: 'short-input',
placeholder: 'Optional job board UUID (defaults to external)',
condition: { field: 'operation', value: ['get_job_posting', 'list_job_postings'] },
mode: 'advanced',
},
{
id: 'postingLocation',
title: 'Location Filter',
type: 'short-input',
placeholder: 'Filter by location name (case sensitive)',
condition: { field: 'operation', value: 'list_job_postings' },
mode: 'advanced',
},
{
id: 'postingDepartment',
title: 'Department Filter',
type: 'short-input',
placeholder: 'Filter by department name (case sensitive)',
condition: { field: 'operation', value: 'list_job_postings' },
mode: 'advanced',
},
{
id: 'listedOnly',
title: 'Listed Postings Only',
type: 'switch',
condition: { field: 'operation', value: 'list_job_postings' },
mode: 'advanced',
},
{
id: 'expandJob',
title: 'Include Job',
type: 'switch',
condition: { field: 'operation', value: 'get_job_posting' },
mode: 'advanced',
@@ -501,6 +720,9 @@ Output only the ISO 8601 timestamp string, nothing else.`,
if (params.searchEmail) result.email = params.searchEmail
if (params.filterStatus) result.status = params.filterStatus
if (params.filterJobId) result.jobId = params.filterJobId
if (params.operation === 'list_applications' && params.filterCandidateId) {
result.candidateId = params.filterCandidateId
}
if (params.jobStatus) result.status = params.jobStatus
if (params.sendNotifications === 'true' || params.sendNotifications === true) {
result.sendNotifications = true
@@ -508,21 +730,51 @@ Output only the ISO 8601 timestamp string, nothing else.`,
if (params.includeArchived === 'true' || params.includeArchived === true) {
result.includeArchived = true
}
if (
params.expandApplicationFormDefinition === 'true' ||
params.expandApplicationFormDefinition === true
) {
result.expandApplicationFormDefinition = true
if (params.includeDeactivated === 'true' || params.includeDeactivated === true) {
result.includeDeactivated = true
}
if (params.isPrivate === 'true' || params.isPrivate === true) {
result.isPrivate = true
}
if (params.listedOnly === 'true' || params.listedOnly === true) {
result.listedOnly = true
}
if (params.expandJob === 'true' || params.expandJob === true) {
result.expandJob = true
}
if (params.operation === 'create_application' && params.appCandidateId) {
result.candidateId = params.appCandidateId
}
if (params.operation === 'create_application' && params.appCreatedAt) {
result.createdAt = params.appCreatedAt
}
if (
params.expandSurveyFormDefinitions === 'true' ||
params.expandSurveyFormDefinitions === true
(params.operation === 'create_candidate' || params.operation === 'update_candidate') &&
params.candidateCreatedAt
) {
result.expandSurveyFormDefinitions = true
result.createdAt = params.candidateCreatedAt
}
if (params.operation === 'create_note' && params.noteCreatedAt) {
result.createdAt = params.noteCreatedAt
}
if (params.appCandidateId) result.candidateId = params.appCandidateId
if (params.appCreatedAt) result.createdAt = params.appCreatedAt
if (params.updateName) result.name = params.updateName
if (params.website) result.website = params.website
if (params.alternateEmail) result.alternateEmail = params.alternateEmail
if (params.postingLocation) result.location = params.postingLocation
if (params.postingDepartment) result.department = params.postingDepartment
if (
params.includeLocationHierarchy === 'true' ||
params.includeLocationHierarchy === true
) {
result.includeLocationHierarchy = true
}
if (params.operation === 'list_offers' && params.offerApplicationId) {
result.applicationId = params.offerApplicationId
}
if (params.alternateEmailAddresses) {
result.alternateEmailAddresses = params.alternateEmailAddresses
}
if (params.socialLinks) result.socialLinks = params.socialLinks
return result
},
},
@@ -554,24 +806,51 @@ Output only the ISO 8601 timestamp string, nothing else.`,
sendNotifications: { type: 'boolean', description: 'Send notifications' },
filterStatus: { type: 'string', description: 'Application status filter' },
filterJobId: { type: 'string', description: 'Job UUID filter' },
filterCandidateId: { type: 'string', description: 'Candidate UUID filter' },
createdAfter: { type: 'string', description: 'Filter by creation date' },
openedAfter: { type: 'string', description: 'Filter jobs opened after this timestamp' },
openedBefore: { type: 'string', description: 'Filter jobs opened before this timestamp' },
closedAfter: { type: 'string', description: 'Filter jobs closed after this timestamp' },
closedBefore: { type: 'string', description: 'Filter jobs closed before this timestamp' },
jobStatus: { type: 'string', description: 'Job status filter' },
cursor: { type: 'string', description: 'Pagination cursor' },
perPage: { type: 'number', description: 'Results per page' },
syncToken: { type: 'string', description: 'Sync token for incremental updates' },
includeArchived: { type: 'boolean', description: 'Include archived records' },
expandApplicationFormDefinition: {
includeDeactivated: { type: 'boolean', description: 'Include deactivated users' },
website: { type: 'string', description: 'Personal website URL for new candidate' },
alternateEmail: { type: 'string', description: 'Additional email to add to candidate' },
candidateCreatedAt: { type: 'string', description: 'Candidate creation timestamp override' },
noteCreatedAt: { type: 'string', description: 'Note creation timestamp override' },
isPrivate: { type: 'boolean', description: 'Whether the note is private' },
postingLocation: { type: 'string', description: 'Filter job postings by location name' },
postingDepartment: { type: 'string', description: 'Filter job postings by department name' },
listedOnly: { type: 'boolean', description: 'Only return publicly listed job postings' },
jobBoardId: { type: 'string', description: 'Job board UUID for job posting lookup' },
expandJob: {
type: 'boolean',
description: 'Include application form definition in job posting',
},
expandSurveyFormDefinitions: {
type: 'boolean',
description: 'Include survey form definitions in job posting',
description: 'Include the related job object in job posting response',
},
tagId: { type: 'string', description: 'Tag UUID' },
offerId: { type: 'string', description: 'Offer UUID' },
jobPostingId: { type: 'string', description: 'Job posting UUID' },
archiveReasonId: { type: 'string', description: 'Archive reason UUID' },
includeLocationHierarchy: {
type: 'boolean',
description: 'Include hierarchical location data when listing locations',
},
offerApplicationId: {
type: 'string',
description: 'Application UUID filter for list_offers',
},
alternateEmailAddresses: {
type: 'string',
description: 'Alternate email addresses (comma-separated or JSON array)',
},
socialLinks: {
type: 'string',
description: 'Social links as JSON array',
},
},
outputs: {
@@ -583,7 +862,7 @@ Output only the ISO 8601 timestamp string, nothing else.`,
jobs: {
type: 'json',
description:
'List of jobs (id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds[], customFields[], jobPostingIds[], customRequisitionId, brandId, hiringTeam[], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings[] with latestVersion, compensation with compensationTiers[])',
'List of jobs (id, title, confidential, status, employmentType, locationId, departmentId, defaultInterviewPlanId, interviewPlanIds[], customFields[], jobPostingIds[], customRequisitionId, brandId, hiringTeam[], author, createdAt, updatedAt, openedAt, closedAt, location with address, openings[] with latestVersion)',
},
applications: {
type: 'json',
@@ -636,7 +915,7 @@ Output only the ISO 8601 timestamp string, nothing else.`,
users: {
type: 'json',
description:
'List of users (id, firstName, lastName, email, globalRole, isEnabled, updatedAt, managerId)',
'List of users (id, firstName, lastName, email, globalRole, isEnabled, updatedAt)',
},
interviewSchedules: {
type: 'json',
@@ -654,12 +933,12 @@ Output only the ISO 8601 timestamp string, nothing else.`,
candidate: {
type: 'json',
description:
'Candidate details (id, name, primaryEmailAddress, primaryPhoneNumber, emailAddresses[], phoneNumbers[], socialLinks[], customFields[], source, creditedToUser, createdAt, updatedAt)',
'Candidate summary (id, name, primaryEmailAddress, primaryPhoneNumber). For full candidate fields use the candidates list output or the get/create/update candidate operations.',
},
job: {
type: 'json',
description:
'Job details (id, title, status, employmentType, locationId, departmentId, hiringTeam[], author, location, openings[], compensation, createdAt, updatedAt)',
'Job details (id, title, status, employmentType, locationId, departmentId, hiringTeam[], author, location, openings[], createdAt, updatedAt)',
},
application: {
type: 'json',
@@ -674,12 +953,12 @@ Output only the ISO 8601 timestamp string, nothing else.`,
jobPosting: {
type: 'json',
description:
'Job posting details (id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy[], jobId, locationName, locationIds, linkedData, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt)',
'Job posting details (id, title, descriptionPlain, descriptionHtml, descriptionSocial, descriptionParts, departmentName, teamName, teamNameHierarchy[], jobId, locationName, locationIds, address, isRemote, workplaceType, employmentType, isListed, publishedDate, applicationDeadline, externalLink, applyLink, compensation, updatedAt, job [included when expandJob=true])',
},
content: { type: 'string', description: 'Note content' },
author: {
type: 'json',
description: 'Note author (id, firstName, lastName, email, globalRole, isEnabled)',
description: 'Note author (id, firstName, lastName, email)',
},
isPrivate: { type: 'boolean', description: 'Whether the note is private' },
createdAt: { type: 'string', description: 'ISO 8601 creation timestamp' },
+267 -6
View File
@@ -27,14 +27,18 @@ export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
type: 'dropdown',
options: [
{ label: 'List Files', id: 'list' },
{ label: 'Search Files', id: 'search' },
{ label: 'Get File Info', id: 'get_file' },
{ label: 'Get File Content', id: 'get_content' },
{ label: 'Create Folder', id: 'create_folder' },
{ label: 'Create File', id: 'create_file' },
{ label: 'Upload File', id: 'upload' },
{ label: 'Download File', id: 'download' },
{ label: 'Copy File', id: 'copy' },
{ label: 'Move File', id: 'move' },
{ label: 'Update File', id: 'update' },
{ label: 'Move to Trash', id: 'trash' },
{ label: 'Restore from Trash', id: 'untrash' },
{ label: 'Delete Permanently', id: 'delete' },
{ label: 'Share File', id: 'share' },
{ label: 'Remove Sharing', id: 'unshare' },
@@ -281,7 +285,7 @@ Return ONLY the query string - no explanations, no quotes around the whole thing
id: 'pageSize',
title: 'Results Per Page',
type: 'short-input',
placeholder: 'Number of results (default: 100, max: 1000)',
placeholder: 'Number of results (default: 100, max: 100)',
condition: { field: 'operation', value: 'list' },
},
// Download File Fields - File Selector (basic mode)
@@ -721,6 +725,198 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr
condition: { field: 'operation', value: 'list_permissions' },
required: true,
},
// Get File Content Fields
{
id: 'getContentFileSelector',
title: 'Select File',
type: 'file-selector',
canonicalParamId: 'getContentFileId',
serviceId: 'google-drive',
selectorKey: 'google.drive',
requiredScopes: getScopesForService('google-drive'),
placeholder: 'Select a file to get content from',
mode: 'basic',
dependsOn: ['credential'],
condition: { field: 'operation', value: 'get_content' },
required: true,
},
{
id: 'getContentManualFileId',
title: 'File ID',
type: 'short-input',
canonicalParamId: 'getContentFileId',
placeholder: 'Enter file ID',
mode: 'advanced',
condition: { field: 'operation', value: 'get_content' },
required: true,
},
{
id: 'getContentExportMimeType',
title: 'Export Format',
type: 'dropdown',
options: [
{ label: 'Auto (best format for file type)', id: 'auto' },
{ label: 'Plain Text (text/plain)', id: 'text/plain' },
{ label: 'HTML (text/html)', id: 'text/html' },
{ label: 'PDF (application/pdf)', id: 'application/pdf' },
{
label: 'DOCX (MS Word)',
id: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
},
{
label: 'XLSX (MS Excel)',
id: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
},
{
label: 'PPTX (MS PowerPoint)',
id: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
},
{ label: 'CSV (text/csv)', id: 'text/csv' },
],
value: () => 'auto',
placeholder: 'Export format for Google Workspace files',
condition: { field: 'operation', value: 'get_content' },
},
{
id: 'getContentIncludeRevisions',
title: 'Include Revisions',
type: 'dropdown',
canonicalParamId: 'includeRevisions',
options: [
{ label: 'No', id: 'false' },
{ label: 'Yes (full revision history)', id: 'true' },
],
value: () => 'false',
placeholder: 'Include revision history',
mode: 'advanced',
condition: { field: 'operation', value: 'get_content' },
},
// Move File Fields
{
id: 'moveFileSelector',
title: 'Select File to Move',
type: 'file-selector',
canonicalParamId: 'moveFileId',
serviceId: 'google-drive',
selectorKey: 'google.drive',
requiredScopes: getScopesForService('google-drive'),
placeholder: 'Select a file to move',
mode: 'basic',
dependsOn: ['credential'],
condition: { field: 'operation', value: 'move' },
required: true,
},
{
id: 'moveManualFileId',
title: 'File ID',
type: 'short-input',
canonicalParamId: 'moveFileId',
placeholder: 'Enter file ID to move',
mode: 'advanced',
condition: { field: 'operation', value: 'move' },
required: true,
},
{
id: 'moveDestFolderSelector',
title: 'Destination Folder',
type: 'file-selector',
canonicalParamId: 'moveDestFolderId',
serviceId: 'google-drive',
selectorKey: 'google.drive',
requiredScopes: getScopesForService('google-drive'),
mimeType: 'application/vnd.google-apps.folder',
placeholder: 'Select destination folder',
mode: 'basic',
dependsOn: ['credential'],
condition: { field: 'operation', value: 'move' },
required: true,
},
{
id: 'moveManualDestFolderId',
title: 'Destination Folder ID',
type: 'short-input',
canonicalParamId: 'moveDestFolderId',
placeholder: 'Enter destination folder ID',
mode: 'advanced',
condition: { field: 'operation', value: 'move' },
required: true,
},
{
id: 'moveRemoveFromCurrent',
title: 'Remove from Current Folder',
type: 'dropdown',
canonicalParamId: 'removeFromCurrent',
options: [
{ label: 'Yes (default)', id: 'true' },
{ label: 'No (add to destination, keep in current)', id: 'false' },
],
placeholder: 'Remove from current folder',
mode: 'advanced',
condition: { field: 'operation', value: 'move' },
},
// Search Files Fields
{
id: 'searchQuery',
title: 'Search Query',
type: 'long-input',
placeholder:
"Drive query syntax (e.g., fullText contains 'budget' and mimeType = 'application/pdf')",
condition: { field: 'operation', value: 'search' },
required: true,
wandConfig: {
enabled: true,
prompt: `Generate a Google Drive search query based on the user's description.
Use Google Drive query syntax:
- name contains 'term' - search by filename
- fullText contains 'term' - search file contents
- mimeType = 'type' - filter by file type (e.g., 'application/pdf', 'application/vnd.google-apps.document')
- modifiedTime > 'YYYY-MM-DDTHH:MM:SS' - filter by date
- 'email' in owners - filter by owner
- trashed = false - exclude trashed files
- starred = true - only starred files
- 'folderId' in parents - files in a specific folder
Combine with 'and' / 'or' / 'not'. Example:
- "PDFs about budget modified this year" -> mimeType = 'application/pdf' and fullText contains 'budget' and modifiedTime > '2024-01-01T00:00:00'
- "starred Google Docs" -> mimeType = 'application/vnd.google-apps.document' and starred = true
Return ONLY the query string - no explanations, no quotes around the whole thing, no extra text.`,
placeholder: 'Describe the files you want to find...',
},
},
{
id: 'searchPageSize',
title: 'Results Per Page',
type: 'short-input',
placeholder: 'Number of results (default: 100, max: 100)',
mode: 'advanced',
condition: { field: 'operation', value: 'search' },
},
// Untrash File Fields
{
id: 'untrashFileSelector',
title: 'Select File to Restore',
type: 'file-selector',
canonicalParamId: 'untrashFileId',
serviceId: 'google-drive',
selectorKey: 'google.drive',
requiredScopes: getScopesForService('google-drive'),
placeholder: 'Select a file to restore from trash',
mode: 'basic',
dependsOn: ['credential'],
condition: { field: 'operation', value: 'untrash' },
required: true,
},
{
id: 'untrashManualFileId',
title: 'File ID',
type: 'short-input',
canonicalParamId: 'untrashFileId',
placeholder: 'Enter file ID to restore',
mode: 'advanced',
condition: { field: 'operation', value: 'untrash' },
required: true,
},
// Get Drive Info has no additional fields (just needs credential)
...getTrigger('google_drive_poller').subBlocks,
],
@@ -728,12 +924,16 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr
access: [
'google_drive_list',
'google_drive_get_file',
'google_drive_get_content',
'google_drive_create_folder',
'google_drive_upload',
'google_drive_download',
'google_drive_copy',
'google_drive_move',
'google_drive_search',
'google_drive_update',
'google_drive_trash',
'google_drive_untrash',
'google_drive_delete',
'google_drive_share',
'google_drive_unshare',
@@ -745,8 +945,12 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr
switch (params.operation) {
case 'list':
return 'google_drive_list'
case 'search':
return 'google_drive_search'
case 'get_file':
return 'google_drive_get_file'
case 'get_content':
return 'google_drive_get_content'
case 'create_folder':
return 'google_drive_create_folder'
case 'create_file':
@@ -756,10 +960,14 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr
return 'google_drive_download'
case 'copy':
return 'google_drive_copy'
case 'move':
return 'google_drive_move'
case 'update':
return 'google_drive_update'
case 'trash':
return 'google_drive_trash'
case 'untrash':
return 'google_drive_untrash'
case 'delete':
return 'google_drive_delete'
case 'share':
@@ -782,12 +990,16 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr
createFolderParentId,
listFolderId,
copyDestFolderId,
moveDestFolderId,
// File canonical params (per-operation)
downloadFileId,
getFileId,
getContentFileId,
copyFileId,
moveFileId,
updateFileId,
trashFileId,
untrashFileId,
deleteFileId,
shareFileId,
unshareFileId,
@@ -798,6 +1010,13 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr
shareType,
starred,
sendNotification,
removeFromCurrent,
includeRevisions,
pageSize,
query,
searchQuery,
searchPageSize,
getContentExportMimeType,
...rest
} = params
@@ -828,15 +1047,24 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr
case 'get_file':
effectiveFileId = getFileId?.trim() || undefined
break
case 'get_content':
effectiveFileId = getContentFileId?.trim() || undefined
break
case 'copy':
effectiveFileId = copyFileId?.trim() || undefined
break
case 'move':
effectiveFileId = moveFileId?.trim() || undefined
break
case 'update':
effectiveFileId = updateFileId?.trim() || undefined
break
case 'trash':
effectiveFileId = trashFileId?.trim() || undefined
break
case 'untrash':
effectiveFileId = untrashFileId?.trim() || undefined
break
case 'delete':
effectiveFileId = deleteFileId?.trim() || undefined
break
@@ -851,9 +1079,13 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr
break
}
// Resolve destinationFolderId for copy operation
const effectiveDestinationFolderId =
params.operation === 'copy' ? copyDestFolderId?.trim() || undefined : undefined
// Resolve destinationFolderId for copy/move operations
let effectiveDestinationFolderId: string | undefined
if (params.operation === 'copy') {
effectiveDestinationFolderId = copyDestFolderId?.trim() || undefined
} else if (params.operation === 'move') {
effectiveDestinationFolderId = moveDestFolderId?.trim() || undefined
}
// Convert starred dropdown to boolean
const starredValue = starred === 'true' ? true : starred === 'false' ? false : undefined
@@ -862,17 +1094,35 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr
const sendNotificationValue =
sendNotification === 'true' ? true : sendNotification === 'false' ? false : undefined
// Convert removeFromCurrent dropdown to boolean
const removeFromCurrentValue =
removeFromCurrent === 'true' ? true : removeFromCurrent === 'false' ? false : undefined
// Convert includeRevisions dropdown to boolean
const includeRevisionsValue =
includeRevisions === 'true' ? true : includeRevisions === 'false' ? false : undefined
const effectivePageSize = params.operation === 'search' ? searchPageSize : pageSize
const effectiveQuery = params.operation === 'search' ? searchQuery : query
const effectiveMimeType =
params.operation === 'get_content' ? getContentExportMimeType : mimeType
return {
oauthCredential,
folderId: effectiveFolderId,
fileId: effectiveFileId,
destinationFolderId: effectiveDestinationFolderId,
file: normalizedFile,
pageSize: rest.pageSize ? Number.parseInt(rest.pageSize as string, 10) : undefined,
mimeType: mimeType === 'auto' ? undefined : mimeType,
pageSize: effectivePageSize
? Number.parseInt(effectivePageSize as string, 10)
: undefined,
query: effectiveQuery,
mimeType: effectiveMimeType === 'auto' ? undefined : effectiveMimeType,
type: shareType, // Map shareType to type for share tool
starred: starredValue,
sendNotification: sendNotificationValue,
removeFromCurrent: removeFromCurrentValue,
includeRevisions: includeRevisionsValue,
transferOwnership: rest.role === 'owner' ? true : undefined,
...rest,
}
@@ -887,16 +1137,27 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr
createFolderParentId: { type: 'string', description: 'Parent folder for create folder' },
listFolderId: { type: 'string', description: 'Folder to list files from' },
copyDestFolderId: { type: 'string', description: 'Destination folder for copy' },
moveDestFolderId: { type: 'string', description: 'Destination folder for move' },
// File canonical params (per-operation)
downloadFileId: { type: 'string', description: 'File to download' },
getFileId: { type: 'string', description: 'File to get info for' },
getContentFileId: { type: 'string', description: 'File to get content from' },
copyFileId: { type: 'string', description: 'File to copy' },
moveFileId: { type: 'string', description: 'File to move' },
updateFileId: { type: 'string', description: 'File to update' },
trashFileId: { type: 'string', description: 'File to trash' },
untrashFileId: { type: 'string', description: 'File to restore from trash' },
deleteFileId: { type: 'string', description: 'File to delete' },
shareFileId: { type: 'string', description: 'File to share' },
unshareFileId: { type: 'string', description: 'File to unshare' },
listPermissionsFileId: { type: 'string', description: 'File to list permissions for' },
// Move operation inputs
removeFromCurrent: {
type: 'string',
description: 'Whether to remove from current folder when moving',
},
// Get content operation inputs
includeRevisions: { type: 'string', description: 'Whether to include revision history' },
// Upload and Create inputs
fileName: { type: 'string', description: 'File or folder name' },
file: { type: 'json', description: 'File to upload (UserFile object)' },
+47 -15
View File
@@ -27,6 +27,7 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
type: 'dropdown',
options: [
{ label: 'Read Issue', id: 'read' },
{ label: 'Read Bulk Issues', id: 'read-bulk' },
{ label: 'Update Issue', id: 'update' },
{ label: 'Write Issue', id: 'write' },
{ label: 'Delete Issue', id: 'delete' },
@@ -270,6 +271,7 @@ Return ONLY the description text - no explanations.`,
placeholder: 'Parent issue key for subtasks (e.g., PROJ-123)',
dependsOn: ['projectId'],
condition: { field: 'operation', value: 'write' },
mode: 'advanced',
},
// Write/Update Issue additional fields
{
@@ -320,6 +322,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
placeholder: 'Reporter account ID',
dependsOn: ['projectId'],
condition: { field: 'operation', value: 'write' },
mode: 'advanced',
},
{
id: 'environment',
@@ -327,6 +330,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
type: 'long-input',
placeholder: 'Environment information (e.g., Production, Staging)',
condition: { field: 'operation', value: ['write', 'update'] },
mode: 'advanced',
},
{
id: 'customFieldId',
@@ -334,6 +338,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
type: 'short-input',
placeholder: 'e.g., customfield_10001 or 10001',
condition: { field: 'operation', value: ['write', 'update'] },
mode: 'advanced',
},
{
id: 'customFieldValue',
@@ -341,6 +346,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
type: 'short-input',
placeholder: 'Value for the custom field',
condition: { field: 'operation', value: ['write', 'update'] },
mode: 'advanced',
},
{
id: 'components',
@@ -348,6 +354,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
type: 'short-input',
placeholder: 'Comma-separated component names',
condition: { field: 'operation', value: ['write', 'update'] },
mode: 'advanced',
},
{
id: 'fixVersions',
@@ -355,6 +362,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
type: 'short-input',
placeholder: 'Comma-separated fix version names',
condition: { field: 'operation', value: ['write', 'update'] },
mode: 'advanced',
},
{
id: 'notifyUsers',
@@ -366,6 +374,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
],
value: () => 'true',
condition: { field: 'operation', value: 'update' },
mode: 'advanced',
},
// Delete Issue fields
{
@@ -378,6 +387,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
],
value: () => 'false',
condition: { field: 'operation', value: 'delete' },
mode: 'advanced',
},
// Assign Issue fields
{
@@ -421,6 +431,7 @@ Return ONLY the comment text - no explanations.`,
type: 'short-input',
placeholder: 'Resolution name (e.g., "Fixed", "Won\'t Fix")',
condition: { field: 'operation', value: 'transition' },
mode: 'advanced',
},
// Search Issues fields
{
@@ -453,6 +464,7 @@ Return ONLY the JQL query - no explanations or markdown formatting.`,
type: 'short-input',
placeholder: 'Cursor token for next page (omit for first page)',
condition: { field: 'operation', value: 'search' },
mode: 'advanced',
},
{
id: 'startAt',
@@ -460,6 +472,7 @@ Return ONLY the JQL query - no explanations or markdown formatting.`,
type: 'short-input',
placeholder: 'Pagination start index (default: 0)',
condition: { field: 'operation', value: ['get_comments', 'get_worklogs'] },
mode: 'advanced',
},
{
id: 'maxResults',
@@ -467,6 +480,7 @@ Return ONLY the JQL query - no explanations or markdown formatting.`,
type: 'short-input',
placeholder: 'Maximum results to return (default: 50)',
condition: { field: 'operation', value: ['search', 'get_comments', 'get_worklogs'] },
mode: 'advanced',
},
{
id: 'fields',
@@ -474,6 +488,7 @@ Return ONLY the JQL query - no explanations or markdown formatting.`,
type: 'short-input',
placeholder: 'Comma-separated fields to return (e.g., key,summary,status)',
condition: { field: 'operation', value: 'search' },
mode: 'advanced',
},
// Comment fields
{
@@ -629,6 +644,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
type: 'long-input',
placeholder: 'Add optional comment for the link',
condition: { field: 'operation', value: 'create_link' },
mode: 'advanced',
wandConfig: {
enabled: true,
prompt: `Generate a comment for a Jira issue link based on the user's description.
@@ -657,6 +673,7 @@ Return ONLY the comment text - no explanations.`,
type: 'short-input',
placeholder: 'Enter account ID for specific user',
condition: { field: 'operation', value: 'get_users' },
mode: 'advanced',
},
{
id: 'usersStartAt',
@@ -664,6 +681,7 @@ Return ONLY the comment text - no explanations.`,
type: 'short-input',
placeholder: 'Pagination start index (default: 0)',
condition: { field: 'operation', value: 'get_users' },
mode: 'advanced',
},
{
id: 'usersMaxResults',
@@ -671,6 +689,7 @@ Return ONLY the comment text - no explanations.`,
type: 'short-input',
placeholder: 'Maximum users to return (default: 50)',
condition: { field: 'operation', value: 'get_users' },
mode: 'advanced',
},
// Search Users fields
{
@@ -744,16 +763,8 @@ Return ONLY the comment text - no explanations.`,
],
config: {
tool: (params) => {
// Use canonical param IDs (raw subBlock IDs are deleted after serialization)
const effectiveProjectId = params.projectId ? String(params.projectId).trim() : ''
const effectiveIssueKey = params.issueKey ? String(params.issueKey).trim() : ''
switch (params.operation) {
case 'read':
// If a project is selected but no issue is chosen, route to bulk read
if (effectiveProjectId && !effectiveIssueKey) {
return 'jira_bulk_read'
}
return 'jira_retrieve'
case 'update':
return 'jira_update'
@@ -877,7 +888,12 @@ Return ONLY the comment text - no explanations.`,
environment: params.environment || undefined,
customFieldId: params.customFieldId || undefined,
customFieldValue: params.customFieldValue || undefined,
notifyUsers: params.notifyUsers === 'false' ? false : undefined,
notifyUsers:
params.notifyUsers === 'false'
? false
: params.notifyUsers === 'true'
? true
: undefined,
}
return {
...baseParams,
@@ -989,12 +1005,20 @@ Return ONLY the comment text - no explanations.`,
}
}
case 'add_worklog': {
const rawTime = params.timeSpentSeconds
const parsedTime =
rawTime !== undefined && rawTime !== null && String(rawTime).trim() !== ''
? Number(String(rawTime).trim())
: undefined
if (parsedTime !== undefined && (!Number.isFinite(parsedTime) || parsedTime <= 0)) {
throw new Error(
'Time Spent (seconds) must be a positive number of seconds (e.g., 3600 for 1 hour)'
)
}
return {
...baseParams,
issueKey: effectiveIssueKey,
timeSpentSeconds: params.timeSpentSeconds
? Number.parseInt(params.timeSpentSeconds)
: undefined,
timeSpentSeconds: parsedTime,
comment: params.worklogComment,
started: params.started,
}
@@ -1012,9 +1036,17 @@ Return ONLY the comment text - no explanations.`,
...baseParams,
issueKey: effectiveIssueKey,
worklogId: params.worklogId,
timeSpentSeconds: params.timeSpentSecondsUpdate
? Number.parseInt(params.timeSpentSecondsUpdate)
: undefined,
timeSpentSeconds: (() => {
const raw = params.timeSpentSecondsUpdate
if (raw === undefined || raw === null || String(raw).trim() === '') return undefined
const n = Number(String(raw).trim())
if (!Number.isFinite(n) || n <= 0) {
throw new Error(
'Time Spent (seconds) must be a positive number of seconds (e.g., 3600 for 1 hour)'
)
}
return n
})(),
comment: params.worklogComment,
started: params.started,
}
@@ -798,14 +798,13 @@ Return ONLY the comment text - no explanations.`,
if (!params.serviceDeskId) {
throw new Error('Service Desk ID is required')
}
if (!params.accountIds && !params.emails) {
throw new Error('Account IDs or emails are required')
if (!params.accountIds) {
throw new Error('Account IDs are required')
}
return {
...baseParams,
serviceDeskId: params.serviceDeskId,
accountIds: params.accountIds,
emails: params.emails,
}
}
case 'get_organizations':
@@ -1238,7 +1237,8 @@ Return ONLY the comment text - no explanations.`,
status: { type: 'string', description: 'Form status (open, submitted, locked)' },
answers: {
type: 'json',
description: 'Form answers as key-value pairs (question ID to answer)',
description:
'Array of simplified form answers, each with label, answer, fieldKey, and choice',
},
deleted: { type: 'boolean', description: 'Whether the form was successfully deleted' },
visibility: {
+44 -7
View File
@@ -494,6 +494,18 @@ Do not include any explanations, markdown formatting, or other text outside the
value: 'list_users',
},
},
// Pagination cursor (shared across list_channels, list_members, list_users)
{
id: 'paginationCursor',
title: 'Pagination Cursor',
type: 'short-input',
placeholder: 'next_cursor from a previous response',
condition: {
field: 'operation',
value: ['list_channels', 'list_members', 'list_users'],
},
mode: 'advanced',
},
// Get User specific fields
{
id: 'userId',
@@ -1331,6 +1343,9 @@ Do not include any explanations, markdown formatting, or other text outside the
viewHash,
publishUserId,
viewPayload,
fileId,
fileName,
paginationCursor,
...rest
} = params
@@ -1421,17 +1436,26 @@ Do not include any explanations, markdown formatting, or other text outside the
baseParams.includePrivate = includePrivate !== 'false'
baseParams.excludeArchived = true
baseParams.limit = channelLimit ? Number.parseInt(channelLimit, 10) : 100
if (paginationCursor) {
baseParams.cursor = String(paginationCursor).trim()
}
break
}
case 'list_members': {
baseParams.limit = memberLimit ? Number.parseInt(memberLimit, 10) : 100
if (paginationCursor) {
baseParams.cursor = String(paginationCursor).trim()
}
break
}
case 'list_users': {
baseParams.includeDeleted = includeDeleted === 'true'
baseParams.limit = userLimit ? Number.parseInt(userLimit, 10) : 100
if (paginationCursor) {
baseParams.cursor = String(paginationCursor).trim()
}
break
}
@@ -1440,8 +1464,6 @@ Do not include any explanations, markdown formatting, or other text outside the
break
case 'download': {
const fileId = (rest as any).fileId
const fileName = (rest as any).fileName
baseParams.fileId = fileId
if (fileName) {
baseParams.fileName = fileName
@@ -1561,18 +1583,24 @@ Do not include any explanations, markdown formatting, or other text outside the
baseParams.view = viewPayload
break
case 'update_view':
if (viewId) {
baseParams.viewId = viewId
case 'update_view': {
const trimmedViewId = viewId ? String(viewId).trim() : ''
const trimmedExternalId = viewExternalId ? String(viewExternalId).trim() : ''
if (!trimmedViewId && !trimmedExternalId) {
throw new Error('update_view requires either View ID or External ID')
}
if (viewExternalId) {
baseParams.externalId = viewExternalId
if (trimmedViewId) {
baseParams.viewId = trimmedViewId
}
if (trimmedExternalId) {
baseParams.externalId = trimmedExternalId
}
if (viewHash) {
baseParams.hash = viewHash
}
baseParams.view = viewPayload
break
}
case 'push_view':
baseParams.triggerId = viewTriggerId
@@ -1630,6 +1658,11 @@ Do not include any explanations, markdown formatting, or other text outside the
// List Users inputs
includeDeleted: { type: 'string', description: 'Include deactivated users (true/false)' },
userLimit: { type: 'string', description: 'Maximum number of users to return' },
// Shared pagination input
paginationCursor: {
type: 'string',
description: 'Pagination cursor (next_cursor) for list_channels/list_members/list_users',
},
// Ephemeral message inputs
ephemeralUser: { type: 'string', description: 'User ID who will see the ephemeral message' },
blocks: { type: 'json', description: 'Block Kit layout blocks as a JSON array' },
@@ -1779,6 +1812,10 @@ Do not include any explanations, markdown formatting, or other text outside the
type: 'number',
description: 'Total number of items returned (channels, members, or users)',
},
nextCursor: {
type: 'string',
description: 'Cursor for the next page (null when there are no more pages)',
},
// slack_list_members outputs (list_members operation)
members: {
@@ -34,7 +34,8 @@ export const SUBBLOCK_ID_MIGRATIONS: Record<string, Record<string, string>> = {
ashby: {
emailType: '_removed_emailType',
phoneType: '_removed_phoneType',
filterCandidateId: '_removed_filterCandidateId',
expandApplicationFormDefinition: '_removed_expandApplicationFormDefinition',
expandSurveyFormDefinitions: '_removed_expandSurveyFormDefinitions',
},
rippling: {
action: '_removed_action',
+8 -6
View File
@@ -1,5 +1,10 @@
import type { AshbyCandidate } from '@/tools/ashby/types'
import { CANDIDATE_OUTPUTS, mapCandidate } from '@/tools/ashby/utils'
import {
ashbyAuthHeaders,
ashbyErrorMessage,
CANDIDATE_OUTPUTS,
mapCandidate,
} from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyAddCandidateTagParams {
@@ -45,10 +50,7 @@ export const addCandidateTagTool: ToolConfig<
request: {
url: 'https://api.ashbyhq.com/candidate.addTag',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
}),
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => ({
candidateId: params.candidateId.trim(),
tagId: params.tagId.trim(),
@@ -59,7 +61,7 @@ export const addCandidateTagTool: ToolConfig<
const data = await response.json()
if (!data.success) {
throw new Error(data.errorInfo?.message || 'Failed to add tag to candidate')
throw new Error(ashbyErrorMessage(data, 'Failed to add tag to candidate'))
}
return {
@@ -1,5 +1,10 @@
import type { AshbyApplication } from '@/tools/ashby/types'
import { APPLICATION_OUTPUTS, mapApplication } from '@/tools/ashby/utils'
import {
APPLICATION_OUTPUTS,
ashbyAuthHeaders,
ashbyErrorMessage,
mapApplication,
} from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyChangeApplicationStageParams {
@@ -54,10 +59,7 @@ export const changeApplicationStageTool: ToolConfig<
request: {
url: 'https://api.ashbyhq.com/application.changeStage',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
}),
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {
applicationId: params.applicationId.trim(),
@@ -72,7 +74,7 @@ export const changeApplicationStageTool: ToolConfig<
const data = await response.json()
if (!data.success) {
throw new Error(data.errorInfo?.message || 'Failed to change application stage')
throw new Error(ashbyErrorMessage(data, 'Failed to change application stage'))
}
return {
+8 -6
View File
@@ -1,5 +1,10 @@
import type { AshbyApplication } from '@/tools/ashby/types'
import { APPLICATION_OUTPUTS, mapApplication } from '@/tools/ashby/utils'
import {
APPLICATION_OUTPUTS,
ashbyAuthHeaders,
ashbyErrorMessage,
mapApplication,
} from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyCreateApplicationParams {
@@ -82,10 +87,7 @@ export const createApplicationTool: ToolConfig<
request: {
url: 'https://api.ashbyhq.com/application.create',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
}),
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {
candidateId: params.candidateId.trim(),
@@ -104,7 +106,7 @@ export const createApplicationTool: ToolConfig<
const data = await response.json()
if (!data.success) {
throw new Error(data.errorInfo?.message || 'Failed to create application')
throw new Error(ashbyErrorMessage(data, 'Failed to create application'))
}
return {
+42 -6
View File
@@ -1,5 +1,10 @@
import type { AshbyCreateCandidateParams, AshbyCreateCandidateResponse } from '@/tools/ashby/types'
import { CANDIDATE_OUTPUTS, mapCandidate } from '@/tools/ashby/utils'
import {
ashbyAuthHeaders,
ashbyErrorMessage,
CANDIDATE_OUTPUTS,
mapCandidate,
} from '@/tools/ashby/utils'
import type { ToolConfig } from '@/tools/types'
export const createCandidateTool: ToolConfig<
@@ -48,21 +53,44 @@ export const createCandidateTool: ToolConfig<
visibility: 'user-or-llm',
description: 'GitHub profile URL',
},
website: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Personal website URL',
},
sourceId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'UUID of the source to attribute the candidate to',
},
creditedToUserId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'UUID of the Ashby user to credit with sourcing this candidate',
},
createdAt: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Backdated creation timestamp in ISO 8601 (e.g. 2024-01-01T00:00:00Z). Defaults to now.',
},
alternateEmailAddresses: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description:
'Array of additional email address strings to add to the candidate, e.g. ["a@x.com","b@y.com"]',
},
},
request: {
url: 'https://api.ashbyhq.com/candidate.create',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
}),
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {
name: params.name,
@@ -71,7 +99,15 @@ export const createCandidateTool: ToolConfig<
if (params.phoneNumber) body.phoneNumber = params.phoneNumber
if (params.linkedInUrl) body.linkedInUrl = params.linkedInUrl
if (params.githubUrl) body.githubUrl = params.githubUrl
if (params.website) body.website = params.website
if (params.sourceId) body.sourceId = params.sourceId.trim()
if (params.creditedToUserId) body.creditedToUserId = params.creditedToUserId.trim()
if (params.createdAt) body.createdAt = params.createdAt
if (
Array.isArray(params.alternateEmailAddresses) &&
params.alternateEmailAddresses.length > 0
)
body.alternateEmailAddresses = params.alternateEmailAddresses
return body
},
},
@@ -80,7 +116,7 @@ export const createCandidateTool: ToolConfig<
const data = await response.json()
if (!data.success) {
throw new Error(data.errorInfo?.message || 'Failed to create candidate')
throw new Error(ashbyErrorMessage(data, 'Failed to create candidate'))
}
return {
+18 -5
View File
@@ -1,4 +1,5 @@
import type { AshbyCreateNoteParams, AshbyCreateNoteResponse } from '@/tools/ashby/types'
import { ashbyAuthHeaders, ashbyErrorMessage } from '@/tools/ashby/utils'
import type { ToolConfig } from '@/tools/types'
export const createNoteTool: ToolConfig<AshbyCreateNoteParams, AshbyCreateNoteResponse> = {
@@ -40,15 +41,25 @@ export const createNoteTool: ToolConfig<AshbyCreateNoteParams, AshbyCreateNoteRe
visibility: 'user-or-llm',
description: 'Whether to send notifications to subscribed users (default false)',
},
isPrivate: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the note is private (only visible to the author)',
},
createdAt: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Backdated creation timestamp in ISO 8601 (e.g. 2024-01-01T00:00:00Z). Defaults to now.',
},
},
request: {
url: 'https://api.ashbyhq.com/candidate.createNote',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
}),
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {
candidateId: params.candidateId.trim(),
@@ -62,6 +73,8 @@ export const createNoteTool: ToolConfig<AshbyCreateNoteParams, AshbyCreateNoteRe
} else {
body.note = params.note
}
if (params.isPrivate !== undefined) body.isPrivate = params.isPrivate
if (params.createdAt) body.createdAt = params.createdAt
return body
},
},
@@ -70,7 +83,7 @@ export const createNoteTool: ToolConfig<AshbyCreateNoteParams, AshbyCreateNoteRe
const data = await response.json()
if (!data.success) {
throw new Error(data.errorInfo?.message || 'Failed to create note')
throw new Error(ashbyErrorMessage(data, 'Failed to create note'))
}
const r = data.results
+8 -6
View File
@@ -1,5 +1,10 @@
import type { AshbyApplication } from '@/tools/ashby/types'
import { APPLICATION_OUTPUTS, mapApplication } from '@/tools/ashby/utils'
import {
APPLICATION_OUTPUTS,
ashbyAuthHeaders,
ashbyErrorMessage,
mapApplication,
} from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyGetApplicationParams {
@@ -38,10 +43,7 @@ export const getApplicationTool: ToolConfig<
request: {
url: 'https://api.ashbyhq.com/application.info',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
}),
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => ({
applicationId: params.applicationId.trim(),
}),
@@ -51,7 +53,7 @@ export const getApplicationTool: ToolConfig<
const data = await response.json()
if (!data.success) {
throw new Error(data.errorInfo?.message || 'Failed to get application')
throw new Error(ashbyErrorMessage(data, 'Failed to get application'))
}
return {
+8 -6
View File
@@ -1,5 +1,10 @@
import type { AshbyGetCandidateParams, AshbyGetCandidateResponse } from '@/tools/ashby/types'
import { CANDIDATE_OUTPUTS, mapCandidate } from '@/tools/ashby/utils'
import {
ashbyAuthHeaders,
ashbyErrorMessage,
CANDIDATE_OUTPUTS,
mapCandidate,
} from '@/tools/ashby/utils'
import type { ToolConfig } from '@/tools/types'
export const getCandidateTool: ToolConfig<AshbyGetCandidateParams, AshbyGetCandidateResponse> = {
@@ -26,10 +31,7 @@ export const getCandidateTool: ToolConfig<AshbyGetCandidateParams, AshbyGetCandi
request: {
url: 'https://api.ashbyhq.com/candidate.info',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
}),
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => ({
id: params.candidateId.trim(),
}),
@@ -39,7 +41,7 @@ export const getCandidateTool: ToolConfig<AshbyGetCandidateParams, AshbyGetCandi
const data = await response.json()
if (!data.success) {
throw new Error(data.errorInfo?.message || 'Failed to get candidate')
throw new Error(ashbyErrorMessage(data, 'Failed to get candidate'))
}
return {
+4 -6
View File
@@ -1,5 +1,5 @@
import type { AshbyGetJobParams, AshbyGetJobResponse } from '@/tools/ashby/types'
import { JOB_OUTPUTS, mapJob } from '@/tools/ashby/utils'
import { ashbyAuthHeaders, ashbyErrorMessage, JOB_OUTPUTS, mapJob } from '@/tools/ashby/utils'
import type { ToolConfig } from '@/tools/types'
export const getJobTool: ToolConfig<AshbyGetJobParams, AshbyGetJobResponse> = {
@@ -26,12 +26,10 @@ export const getJobTool: ToolConfig<AshbyGetJobParams, AshbyGetJobResponse> = {
request: {
url: 'https://api.ashbyhq.com/job.info',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
}),
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => ({
id: params.jobId.trim(),
expand: ['openings', 'location', 'compensation'],
}),
},
@@ -39,7 +37,7 @@ export const getJobTool: ToolConfig<AshbyGetJobParams, AshbyGetJobResponse> = {
const data = await response.json()
if (!data.success) {
throw new Error(data.errorInfo?.message || 'Failed to get job')
throw new Error(ashbyErrorMessage(data, 'Failed to get job'))
}
return {
+22 -19
View File
@@ -1,10 +1,11 @@
import { ashbyAuthHeaders, ashbyErrorMessage } from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyGetJobPostingParams {
apiKey: string
jobPostingId: string
expandApplicationFormDefinition?: boolean
expandSurveyFormDefinitions?: boolean
jobBoardId?: string
expandJob?: boolean
}
interface AshbyDescriptionPart {
@@ -65,6 +66,7 @@ interface AshbyJobPosting {
} | null
applicationLimitCalloutHtml: string | null
updatedAt: string | null
job: Record<string, unknown> | null
}
interface AshbyGetJobPostingResponse extends ToolResponse {
@@ -99,37 +101,31 @@ export const getJobPostingTool: ToolConfig<AshbyGetJobPostingParams, AshbyGetJob
visibility: 'user-or-llm',
description: 'The UUID of the job posting to fetch',
},
expandApplicationFormDefinition: {
type: 'boolean',
jobBoardId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Include application form definition in the response',
description:
'Optional job board UUID. If omitted, returns posting for the external job board.',
},
expandSurveyFormDefinitions: {
expandJob: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Include survey form definitions in the response',
description: 'Whether to expand and include the related job object in the response',
},
},
request: {
url: 'https://api.ashbyhq.com/jobPosting.info',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
}),
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {
jobPostingId: params.jobPostingId.trim(),
}
if (params.expandApplicationFormDefinition !== undefined) {
body.expandApplicationFormDefinition = params.expandApplicationFormDefinition
}
if (params.expandSurveyFormDefinitions !== undefined) {
body.expandSurveyFormDefinitions = params.expandSurveyFormDefinitions
}
if (params.jobBoardId) body.jobBoardId = params.jobBoardId.trim()
if (params.expandJob) body.expand = ['job']
return body
},
},
@@ -138,7 +134,7 @@ export const getJobPostingTool: ToolConfig<AshbyGetJobPostingParams, AshbyGetJob
const data = await response.json()
if (!data.success) {
throw new Error(data.errorInfo?.message || 'Failed to get job posting')
throw new Error(ashbyErrorMessage(data, 'Failed to get job posting'))
}
const r = (data.results ?? {}) as Record<string, unknown> & {
@@ -225,6 +221,7 @@ export const getJobPostingTool: ToolConfig<AshbyGetJobPostingParams, AshbyGetJob
: null,
applicationLimitCalloutHtml: (r.applicationLimitCalloutHtml as string) ?? null,
updatedAt: (r.updatedAt as string) ?? null,
job: (r.job as Record<string, unknown>) ?? null,
},
}
},
@@ -407,7 +404,7 @@ export const getJobPostingTool: ToolConfig<AshbyGetJobPostingParams, AshbyGetJob
},
applicationLimitCalloutHtml: {
type: 'string',
description: 'HTML callout shown when application limit is reached',
description: 'HTML callout shown when the application limit is reached',
optional: true,
},
updatedAt: {
@@ -415,5 +412,11 @@ export const getJobPostingTool: ToolConfig<AshbyGetJobPostingParams, AshbyGetJob
description: 'ISO 8601 last update timestamp',
optional: true,
},
job: {
type: 'object',
description:
'The expanded job object, only present when the request was made with expandJob=true',
optional: true,
},
},
}
+3 -6
View File
@@ -1,5 +1,5 @@
import type { AshbyOffer } from '@/tools/ashby/types'
import { mapOffer, OFFER_OUTPUTS } from '@/tools/ashby/utils'
import { ashbyAuthHeaders, ashbyErrorMessage, mapOffer, OFFER_OUTPUTS } from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyGetOfferParams {
@@ -35,10 +35,7 @@ export const getOfferTool: ToolConfig<AshbyGetOfferParams, AshbyGetOfferResponse
request: {
url: 'https://api.ashbyhq.com/offer.info',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
}),
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => ({
offerId: params.offerId.trim(),
}),
@@ -48,7 +45,7 @@ export const getOfferTool: ToolConfig<AshbyGetOfferParams, AshbyGetOfferResponse
const data = await response.json()
if (!data.success) {
throw new Error(data.errorInfo?.message || 'Failed to get offer')
throw new Error(ashbyErrorMessage(data, 'Failed to get offer'))
}
return {
+15 -6
View File
@@ -2,7 +2,12 @@ import type {
AshbyListApplicationsParams,
AshbyListApplicationsResponse,
} from '@/tools/ashby/types'
import { APPLICATION_OUTPUTS, mapApplication } from '@/tools/ashby/utils'
import {
APPLICATION_OUTPUTS,
ashbyAuthHeaders,
ashbyErrorMessage,
mapApplication,
} from '@/tools/ashby/utils'
import type { ToolConfig } from '@/tools/types'
export const listApplicationsTool: ToolConfig<
@@ -46,6 +51,12 @@ export const listApplicationsTool: ToolConfig<
visibility: 'user-or-llm',
description: 'Filter applications by a specific job UUID',
},
candidateId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter applications by a specific candidate UUID',
},
createdAfter: {
type: 'string',
required: false,
@@ -58,16 +69,14 @@ export const listApplicationsTool: ToolConfig<
request: {
url: 'https://api.ashbyhq.com/application.list',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
}),
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.cursor) body.cursor = params.cursor
if (params.perPage) body.limit = params.perPage
if (params.status) body.status = params.status
if (params.jobId) body.jobId = params.jobId.trim()
if (params.candidateId) body.candidateId = params.candidateId.trim()
if (params.createdAfter) {
const ms = new Date(params.createdAfter).getTime()
if (!Number.isNaN(ms)) body.createdAfter = ms
@@ -80,7 +89,7 @@ export const listApplicationsTool: ToolConfig<
const data = await response.json()
if (!data.success) {
throw new Error(data.errorInfo?.message || 'Failed to list applications')
throw new Error(ashbyErrorMessage(data, 'Failed to list applications'))
}
return {
+3 -5
View File
@@ -1,3 +1,4 @@
import { ashbyAuthHeaders, ashbyErrorMessage } from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyListArchiveReasonsParams {
@@ -45,10 +46,7 @@ export const listArchiveReasonsTool: ToolConfig<
request: {
url: 'https://api.ashbyhq.com/archiveReason.list',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
}),
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.includeArchived !== undefined) body.includeArchived = params.includeArchived
@@ -60,7 +58,7 @@ export const listArchiveReasonsTool: ToolConfig<
const data = await response.json()
if (!data.success) {
throw new Error(data.errorInfo?.message || 'Failed to list archive reasons')
throw new Error(ashbyErrorMessage(data, 'Failed to list archive reasons'))
}
return {
+3 -5
View File
@@ -1,3 +1,4 @@
import { ashbyAuthHeaders, ashbyErrorMessage } from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyListCandidateTagsParams {
@@ -68,10 +69,7 @@ export const listCandidateTagsTool: ToolConfig<
request: {
url: 'https://api.ashbyhq.com/candidateTag.list',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
}),
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.includeArchived !== undefined) body.includeArchived = params.includeArchived
@@ -86,7 +84,7 @@ export const listCandidateTagsTool: ToolConfig<
const data = await response.json()
if (!data.success) {
throw new Error(data.errorInfo?.message || 'Failed to list candidate tags')
throw new Error(ashbyErrorMessage(data, 'Failed to list candidate tags'))
}
return {
+19 -6
View File
@@ -1,5 +1,10 @@
import type { AshbyListCandidatesParams, AshbyListCandidatesResponse } from '@/tools/ashby/types'
import { CANDIDATE_OUTPUTS, mapCandidate } from '@/tools/ashby/utils'
import {
ashbyAuthHeaders,
ashbyErrorMessage,
CANDIDATE_OUTPUTS,
mapCandidate,
} from '@/tools/ashby/utils'
import type { ToolConfig } from '@/tools/types'
export const listCandidatesTool: ToolConfig<
@@ -30,19 +35,27 @@ export const listCandidatesTool: ToolConfig<
visibility: 'user-or-llm',
description: 'Number of results per page (default 100)',
},
createdAfter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Only return candidates created after this ISO 8601 timestamp (e.g. 2024-01-01T00:00:00Z)',
},
},
request: {
url: 'https://api.ashbyhq.com/candidate.list',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
}),
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.cursor) body.cursor = params.cursor
if (params.perPage) body.limit = params.perPage
if (params.createdAfter) {
const ms = new Date(params.createdAfter).getTime()
if (!Number.isNaN(ms)) body.createdAfter = ms
}
return body
},
},
@@ -51,7 +64,7 @@ export const listCandidatesTool: ToolConfig<
const data = await response.json()
if (!data.success) {
throw new Error(data.errorInfo?.message || 'Failed to list candidates')
throw new Error(ashbyErrorMessage(data, 'Failed to list candidates'))
}
return {
+59 -6
View File
@@ -1,7 +1,12 @@
import { ashbyAuthHeaders, ashbyErrorMessage } from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyListCustomFieldsParams {
apiKey: string
cursor?: string
perPage?: number
syncToken?: string
includeArchived?: boolean
}
interface AshbyCustomFieldDefinition {
@@ -22,6 +27,9 @@ interface AshbyCustomFieldDefinition {
interface AshbyListCustomFieldsResponse extends ToolResponse {
output: {
customFields: AshbyCustomFieldDefinition[]
moreDataAvailable: boolean
nextCursor: string | null
syncToken: string | null
}
}
@@ -41,28 +49,59 @@ export const listCustomFieldsTool: ToolConfig<
visibility: 'user-only',
description: 'Ashby API Key',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opaque pagination cursor from a previous response nextCursor value',
},
perPage: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results per page (default and max 100)',
},
syncToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opaque token from a prior sync to fetch only items changed since then',
},
includeArchived: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'When true, includes archived custom fields in results (default false)',
},
},
request: {
url: 'https://api.ashbyhq.com/customField.list',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
}),
body: () => ({}),
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.cursor) body.cursor = params.cursor
if (params.perPage) body.limit = params.perPage
if (params.syncToken) body.syncToken = params.syncToken
if (params.includeArchived !== undefined) body.includeArchived = params.includeArchived
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(data.errorInfo?.message || 'Failed to list custom fields')
throw new Error(ashbyErrorMessage(data, 'Failed to list custom fields'))
}
return {
success: true,
output: {
moreDataAvailable: data.moreDataAvailable ?? false,
nextCursor: data.nextCursor ?? null,
syncToken: data.syncToken ?? null,
customFields: (data.results ?? []).map(
(f: Record<string, unknown> & { selectableValues?: Array<Record<string, unknown>> }) => ({
id: (f.id as string) ?? '',
@@ -126,5 +165,19 @@ export const listCustomFieldsTool: ToolConfig<
},
},
},
moreDataAvailable: {
type: 'boolean',
description: 'Whether more pages of results exist',
},
nextCursor: {
type: 'string',
description: 'Opaque cursor for fetching the next page',
optional: true,
},
syncToken: {
type: 'string',
description: 'Opaque sync token returned after the last page; pass on next sync',
optional: true,
},
},
}
+66 -6
View File
@@ -1,7 +1,12 @@
import { ashbyAuthHeaders, ashbyErrorMessage } from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyListDepartmentsParams {
apiKey: string
cursor?: string
perPage?: number
syncToken?: string
includeArchived?: boolean
}
interface AshbyDepartment {
@@ -12,11 +17,15 @@ interface AshbyDepartment {
parentId: string | null
createdAt: string | null
updatedAt: string | null
extraData: Record<string, unknown> | null
}
interface AshbyListDepartmentsResponse extends ToolResponse {
output: {
departments: AshbyDepartment[]
moreDataAvailable: boolean
nextCursor: string | null
syncToken: string | null
}
}
@@ -36,23 +45,51 @@ export const listDepartmentsTool: ToolConfig<
visibility: 'user-only',
description: 'Ashby API Key',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opaque pagination cursor from a previous response nextCursor value',
},
perPage: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results per page (default and max 100)',
},
syncToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opaque token from a prior sync to fetch only items changed since then',
},
includeArchived: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'When true, includes archived departments in results (default false)',
},
},
request: {
url: 'https://api.ashbyhq.com/department.list',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
}),
body: () => ({}),
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.cursor) body.cursor = params.cursor
if (params.perPage) body.limit = params.perPage
if (params.syncToken) body.syncToken = params.syncToken
if (params.includeArchived !== undefined) body.includeArchived = params.includeArchived
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(data.errorInfo?.message || 'Failed to list departments')
throw new Error(ashbyErrorMessage(data, 'Failed to list departments'))
}
return {
@@ -66,7 +103,11 @@ export const listDepartmentsTool: ToolConfig<
parentId: (d.parentId as string) ?? null,
createdAt: (d.createdAt as string) ?? null,
updatedAt: (d.updatedAt as string) ?? null,
extraData: (d.extraData as Record<string, unknown>) ?? null,
})),
moreDataAvailable: data.moreDataAvailable ?? false,
nextCursor: data.nextCursor ?? null,
syncToken: data.syncToken ?? null,
},
}
},
@@ -101,8 +142,27 @@ export const listDepartmentsTool: ToolConfig<
description: 'ISO 8601 last update timestamp',
optional: true,
},
extraData: {
type: 'json',
description: 'Free-form key-value metadata',
optional: true,
},
},
},
},
moreDataAvailable: {
type: 'boolean',
description: 'Whether more pages of results exist',
},
nextCursor: {
type: 'string',
description: 'Opaque cursor for fetching the next page',
optional: true,
},
syncToken: {
type: 'string',
description: 'Opaque sync token returned after the last page; pass on next sync',
optional: true,
},
},
}
+20 -6
View File
@@ -1,5 +1,10 @@
import type { AshbyUserSummary } from '@/tools/ashby/types'
import { mapUserSummary, USER_SUMMARY_OUTPUT } from '@/tools/ashby/utils'
import {
ashbyAuthHeaders,
ashbyErrorMessage,
mapUserSummary,
USER_SUMMARY_OUTPUT,
} from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyListInterviewSchedulesParams {
@@ -8,6 +13,7 @@ interface AshbyListInterviewSchedulesParams {
interviewStageId?: string
cursor?: string
perPage?: number
createdAfter?: string
}
interface AshbyInterviewEvent {
@@ -126,21 +132,29 @@ export const listInterviewsTool: ToolConfig<
visibility: 'user-or-llm',
description: 'Number of results per page (default 100)',
},
createdAfter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Only return interview schedules created after this ISO 8601 timestamp (e.g. 2024-01-01T00:00:00Z)',
},
},
request: {
url: 'https://api.ashbyhq.com/interviewSchedule.list',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
}),
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.applicationId) body.applicationId = params.applicationId.trim()
if (params.interviewStageId) body.interviewStageId = params.interviewStageId.trim()
if (params.cursor) body.cursor = params.cursor
if (params.perPage) body.limit = params.perPage
if (params.createdAfter) {
const ms = new Date(params.createdAfter).getTime()
if (!Number.isNaN(ms)) body.createdAfter = ms
}
return body
},
},
@@ -149,7 +163,7 @@ export const listInterviewsTool: ToolConfig<
const data = await response.json()
if (!data.success) {
throw new Error(data.errorInfo?.message || 'Failed to list interview schedules')
throw new Error(ashbyErrorMessage(data, 'Failed to list interview schedules'))
}
return {
+40 -6
View File
@@ -1,7 +1,12 @@
import { ashbyAuthHeaders, ashbyErrorMessage } from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyListJobPostingsParams {
apiKey: string
location?: string
department?: string
listedOnly?: boolean
jobBoardId?: string
}
interface AshbyJobPostingSummary {
@@ -49,23 +54,52 @@ export const listJobPostingsTool: ToolConfig<
visibility: 'user-only',
description: 'Ashby API Key',
},
location: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by location name (case sensitive)',
},
department: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by department name (case sensitive)',
},
listedOnly: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'When true, only returns listed (publicly visible) job postings (default false)',
},
jobBoardId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'UUID of a specific job board to filter postings to. If omitted, returns postings on the primary external job board.',
},
},
request: {
url: 'https://api.ashbyhq.com/jobPosting.list',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
}),
body: () => ({}),
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.location) body.location = params.location
if (params.department) body.department = params.department
if (params.listedOnly !== undefined) body.listedOnly = params.listedOnly
if (params.jobBoardId) body.jobBoardId = params.jobBoardId.trim()
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(data.errorInfo?.message || 'Failed to list job postings')
throw new Error(ashbyErrorMessage(data, 'Failed to list job postings'))
}
return {
+59 -7
View File
@@ -1,5 +1,5 @@
import type { AshbyListJobsParams, AshbyListJobsResponse } from '@/tools/ashby/types'
import { JOB_OUTPUTS, mapJob } from '@/tools/ashby/utils'
import { ashbyAuthHeaders, ashbyErrorMessage, JOB_OUTPUTS, mapJob } from '@/tools/ashby/utils'
import type { ToolConfig } from '@/tools/types'
export const listJobsTool: ToolConfig<AshbyListJobsParams, AshbyListJobsResponse> = {
@@ -34,20 +34,72 @@ export const listJobsTool: ToolConfig<AshbyListJobsParams, AshbyListJobsResponse
visibility: 'user-or-llm',
description: 'Filter by job status: Open, Closed, Archived, or Draft',
},
createdAfter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Only return jobs created after this ISO 8601 timestamp (e.g. 2024-01-01T00:00:00Z)',
},
openedAfter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Only return jobs opened after this ISO 8601 timestamp',
},
openedBefore: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Only return jobs opened before this ISO 8601 timestamp',
},
closedAfter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Only return jobs closed after this ISO 8601 timestamp',
},
closedBefore: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Only return jobs closed before this ISO 8601 timestamp',
},
},
request: {
url: 'https://api.ashbyhq.com/job.list',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
}),
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {}
const body: Record<string, unknown> = { expand: ['openings', 'location'] }
if (params.cursor) body.cursor = params.cursor
if (params.perPage) body.limit = params.perPage
if (params.status) body.status = [params.status]
const isoToMs = (iso: string): number | null => {
const ms = new Date(iso).getTime()
return Number.isNaN(ms) ? null : ms
}
if (params.createdAfter) {
const ms = isoToMs(params.createdAfter)
if (ms !== null) body.createdAfter = ms
}
if (params.openedAfter) {
const ms = isoToMs(params.openedAfter)
if (ms !== null) body.openedAfter = ms
}
if (params.openedBefore) {
const ms = isoToMs(params.openedBefore)
if (ms !== null) body.openedBefore = ms
}
if (params.closedAfter) {
const ms = isoToMs(params.closedAfter)
if (ms !== null) body.closedAfter = ms
}
if (params.closedBefore) {
const ms = isoToMs(params.closedBefore)
if (ms !== null) body.closedBefore = ms
}
return body
},
},
@@ -56,7 +108,7 @@ export const listJobsTool: ToolConfig<AshbyListJobsParams, AshbyListJobsResponse
const data = await response.json()
if (!data.success) {
throw new Error(data.errorInfo?.message || 'Failed to list jobs')
throw new Error(ashbyErrorMessage(data, 'Failed to list jobs'))
}
return {
+75 -6
View File
@@ -1,7 +1,13 @@
import { ashbyAuthHeaders, ashbyErrorMessage } from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyListLocationsParams {
apiKey: string
cursor?: string
perPage?: number
syncToken?: string
includeArchived?: boolean
includeLocationHierarchy?: boolean
}
interface AshbyLocation {
@@ -20,11 +26,15 @@ interface AshbyLocation {
postalCode: string | null
streetAddress: string | null
} | null
extraData: Record<string, unknown> | null
}
interface AshbyListLocationsResponse extends ToolResponse {
output: {
locations: AshbyLocation[]
moreDataAvailable: boolean
nextCursor: string | null
syncToken: string | null
}
}
@@ -41,23 +51,59 @@ export const listLocationsTool: ToolConfig<AshbyListLocationsParams, AshbyListLo
visibility: 'user-only',
description: 'Ashby API Key',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opaque pagination cursor from a previous response nextCursor value',
},
perPage: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results per page (default and max 100)',
},
syncToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opaque token from a prior sync to fetch only items changed since then',
},
includeArchived: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'When true, includes archived locations in results (default false)',
},
includeLocationHierarchy: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'When true, includes location hierarchy components/regions (default false)',
},
},
request: {
url: 'https://api.ashbyhq.com/location.list',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
}),
body: () => ({}),
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.cursor) body.cursor = params.cursor
if (params.perPage) body.limit = params.perPage
if (params.syncToken) body.syncToken = params.syncToken
if (params.includeArchived !== undefined) body.includeArchived = params.includeArchived
if (params.includeLocationHierarchy !== undefined)
body.includeLocationHierarchy = params.includeLocationHierarchy
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(data.errorInfo?.message || 'Failed to list locations')
throw new Error(ashbyErrorMessage(data, 'Failed to list locations'))
}
return {
@@ -88,9 +134,13 @@ export const listLocationsTool: ToolConfig<AshbyListLocationsParams, AshbyListLo
streetAddress: (pa.streetAddress as string) ?? null,
}
: null,
extraData: (l.extraData as Record<string, unknown>) ?? null,
}
}
),
moreDataAvailable: data.moreDataAvailable ?? false,
nextCursor: data.nextCursor ?? null,
syncToken: data.syncToken ?? null,
},
}
},
@@ -149,8 +199,27 @@ export const listLocationsTool: ToolConfig<AshbyListLocationsParams, AshbyListLo
streetAddress: { type: 'string', description: 'Street address', optional: true },
},
},
extraData: {
type: 'json',
description: 'Free-form key-value metadata',
optional: true,
},
},
},
},
moreDataAvailable: {
type: 'boolean',
description: 'Whether more pages of results exist',
},
nextCursor: {
type: 'string',
description: 'Opaque cursor for fetching the next page',
optional: true,
},
syncToken: {
type: 'string',
description: 'Opaque sync token returned after the last page; pass on next sync',
optional: true,
},
},
}
+3 -5
View File
@@ -1,3 +1,4 @@
import { ashbyAuthHeaders, ashbyErrorMessage } from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyListNotesParams {
@@ -62,10 +63,7 @@ export const listNotesTool: ToolConfig<AshbyListNotesParams, AshbyListNotesRespo
request: {
url: 'https://api.ashbyhq.com/candidate.listNotes',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
}),
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {
candidateId: params.candidateId.trim(),
@@ -80,7 +78,7 @@ export const listNotesTool: ToolConfig<AshbyListNotesParams, AshbyListNotesRespo
const data = await response.json()
if (!data.success) {
throw new Error(data.errorInfo?.message || 'Failed to list notes')
throw new Error(ashbyErrorMessage(data, 'Failed to list notes'))
}
return {
+31 -6
View File
@@ -1,11 +1,14 @@
import type { AshbyOffer } from '@/tools/ashby/types'
import { mapOffer, OFFER_OUTPUTS } from '@/tools/ashby/utils'
import { ashbyAuthHeaders, ashbyErrorMessage, mapOffer, OFFER_OUTPUTS } from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyListOffersParams {
apiKey: string
cursor?: string
perPage?: number
syncToken?: string
createdAfter?: string
applicationId?: string
}
interface AshbyListOffersResponse extends ToolResponse {
@@ -41,19 +44,41 @@ export const listOffersTool: ToolConfig<AshbyListOffersParams, AshbyListOffersRe
visibility: 'user-or-llm',
description: 'Number of results per page',
},
createdAfter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Only return offers created after this ISO 8601 timestamp (e.g. 2024-01-01T00:00:00Z)',
},
syncToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Opaque token from a prior sync to fetch only items changed since then',
},
applicationId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Return only offers for the specified application UUID',
},
},
request: {
url: 'https://api.ashbyhq.com/offer.list',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
}),
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.cursor) body.cursor = params.cursor
if (params.perPage) body.limit = params.perPage
if (params.createdAfter) {
const ms = new Date(params.createdAfter).getTime()
if (!Number.isNaN(ms)) body.createdAfter = ms
}
if (params.syncToken) body.syncToken = params.syncToken
if (params.applicationId) body.applicationId = params.applicationId.trim()
return body
},
},
@@ -62,7 +87,7 @@ export const listOffersTool: ToolConfig<AshbyListOffersParams, AshbyListOffersRe
const data = await response.json()
if (!data.success) {
throw new Error(data.errorInfo?.message || 'Failed to list offers')
throw new Error(ashbyErrorMessage(data, 'Failed to list offers'))
}
return {
+20 -6
View File
@@ -1,11 +1,17 @@
import type { AshbyOpening } from '@/tools/ashby/types'
import { mapOpenings, OPENINGS_OUTPUT } from '@/tools/ashby/utils'
import {
ashbyAuthHeaders,
ashbyErrorMessage,
mapOpenings,
OPENINGS_OUTPUT,
} from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyListOpeningsParams {
apiKey: string
cursor?: string
perPage?: number
createdAfter?: string
}
interface AshbyListOpeningsResponse extends ToolResponse {
@@ -41,19 +47,27 @@ export const listOpeningsTool: ToolConfig<AshbyListOpeningsParams, AshbyListOpen
visibility: 'user-or-llm',
description: 'Number of results per page (default 100)',
},
createdAfter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Only return openings created after this ISO 8601 timestamp (e.g. 2024-01-01T00:00:00Z)',
},
},
request: {
url: 'https://api.ashbyhq.com/opening.list',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
}),
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.cursor) body.cursor = params.cursor
if (params.perPage) body.limit = params.perPage
if (params.createdAfter) {
const ms = new Date(params.createdAfter).getTime()
if (!Number.isNaN(ms)) body.createdAfter = ms
}
return body
},
},
@@ -62,7 +76,7 @@ export const listOpeningsTool: ToolConfig<AshbyListOpeningsParams, AshbyListOpen
const data = await response.json()
if (!data.success) {
throw new Error(data.errorInfo?.message || 'Failed to list openings')
throw new Error(ashbyErrorMessage(data, 'Failed to list openings'))
}
return {
+15 -6
View File
@@ -1,8 +1,10 @@
import type { AshbySourceSummary } from '@/tools/ashby/types'
import { ashbyAuthHeaders, ashbyErrorMessage } from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyListSourcesParams {
apiKey: string
includeArchived?: boolean
}
interface AshbyListSourcesResponse extends ToolResponse {
@@ -24,23 +26,30 @@ export const listSourcesTool: ToolConfig<AshbyListSourcesParams, AshbyListSource
visibility: 'user-only',
description: 'Ashby API Key',
},
includeArchived: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'When true, includes archived sources in results (default false)',
},
},
request: {
url: 'https://api.ashbyhq.com/source.list',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
}),
body: () => ({}),
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.includeArchived !== undefined) body.includeArchived = params.includeArchived
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(data.errorInfo?.message || 'Failed to list sources')
throw new Error(ashbyErrorMessage(data, 'Failed to list sources'))
}
return {
+17 -6
View File
@@ -1,11 +1,17 @@
import type { AshbyUserSummary } from '@/tools/ashby/types'
import { mapUserSummary, USER_SUMMARY_OUTPUT } from '@/tools/ashby/utils'
import {
ashbyAuthHeaders,
ashbyErrorMessage,
mapUserSummary,
USER_SUMMARY_OUTPUT,
} from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyListUsersParams {
apiKey: string
cursor?: string
perPage?: number
includeDeactivated?: boolean
}
interface AshbyListUsersResponse extends ToolResponse {
@@ -41,19 +47,24 @@ export const listUsersTool: ToolConfig<AshbyListUsersParams, AshbyListUsersRespo
visibility: 'user-or-llm',
description: 'Number of results per page (default 100)',
},
includeDeactivated: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'When true, includes deactivated users in results (default false)',
},
},
request: {
url: 'https://api.ashbyhq.com/user.list',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
}),
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.cursor) body.cursor = params.cursor
if (params.perPage) body.limit = params.perPage
if (params.includeDeactivated !== undefined)
body.includeDeactivated = params.includeDeactivated
return body
},
},
@@ -62,7 +73,7 @@ export const listUsersTool: ToolConfig<AshbyListUsersParams, AshbyListUsersRespo
const data = await response.json()
if (!data.success) {
throw new Error(data.errorInfo?.message || 'Failed to list users')
throw new Error(ashbyErrorMessage(data, 'Failed to list users'))
}
return {
+8 -6
View File
@@ -1,5 +1,10 @@
import type { AshbyCandidate } from '@/tools/ashby/types'
import { CANDIDATE_OUTPUTS, mapCandidate } from '@/tools/ashby/utils'
import {
ashbyAuthHeaders,
ashbyErrorMessage,
CANDIDATE_OUTPUTS,
mapCandidate,
} from '@/tools/ashby/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface AshbyRemoveCandidateTagParams {
@@ -45,10 +50,7 @@ export const removeCandidateTagTool: ToolConfig<
request: {
url: 'https://api.ashbyhq.com/candidate.removeTag',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
}),
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => ({
candidateId: params.candidateId.trim(),
tagId: params.tagId.trim(),
@@ -59,7 +61,7 @@ export const removeCandidateTagTool: ToolConfig<
const data = await response.json()
if (!data.success) {
throw new Error(data.errorInfo?.message || 'Failed to remove tag from candidate')
throw new Error(ashbyErrorMessage(data, 'Failed to remove tag from candidate'))
}
return {
+8 -6
View File
@@ -2,7 +2,12 @@ import type {
AshbySearchCandidatesParams,
AshbySearchCandidatesResponse,
} from '@/tools/ashby/types'
import { CANDIDATE_OUTPUTS, mapCandidate } from '@/tools/ashby/utils'
import {
ashbyAuthHeaders,
ashbyErrorMessage,
CANDIDATE_OUTPUTS,
mapCandidate,
} from '@/tools/ashby/utils'
import type { ToolConfig } from '@/tools/types'
export const searchCandidatesTool: ToolConfig<
@@ -39,10 +44,7 @@ export const searchCandidatesTool: ToolConfig<
request: {
url: 'https://api.ashbyhq.com/candidate.search',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
}),
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.name) body.name = params.name
@@ -55,7 +57,7 @@ export const searchCandidatesTool: ToolConfig<
const data = await response.json()
if (!data.success) {
throw new Error(data.errorInfo?.message || 'Failed to search candidates')
throw new Error(ashbyErrorMessage(data, 'Failed to search candidates'))
}
return {
+23
View File
@@ -94,6 +94,7 @@ export interface AshbyCandidate {
export interface AshbyListCandidatesParams extends AshbyBaseParams {
cursor?: string
perPage?: number
createdAfter?: string
}
export interface AshbyGetCandidateParams extends AshbyBaseParams {
@@ -106,7 +107,11 @@ export interface AshbyCreateCandidateParams extends AshbyBaseParams {
phoneNumber?: string
linkedInUrl?: string
githubUrl?: string
website?: string
sourceId?: string
creditedToUserId?: string
createdAt?: string
alternateEmailAddresses?: string[]
}
export interface AshbySearchCandidatesParams extends AshbyBaseParams {
@@ -118,6 +123,11 @@ export interface AshbyListJobsParams extends AshbyBaseParams {
cursor?: string
perPage?: number
status?: string
createdAfter?: string
openedAfter?: string
openedBefore?: string
closedAfter?: string
closedBefore?: string
}
export interface AshbyGetJobParams extends AshbyBaseParams {
@@ -129,6 +139,8 @@ export interface AshbyCreateNoteParams extends AshbyBaseParams {
note: string
noteType?: string
sendNotifications?: boolean
isPrivate?: boolean
createdAt?: string
}
export interface AshbyListApplicationsParams extends AshbyBaseParams {
@@ -136,6 +148,7 @@ export interface AshbyListApplicationsParams extends AshbyBaseParams {
perPage?: number
status?: string
jobId?: string
candidateId?: string
createdAfter?: string
}
@@ -309,6 +322,15 @@ export interface AshbyApplicationArchiveReason {
customFields: AshbyCustomField[]
}
export interface AshbyApplicationHistoryEntry {
id: string
stageId: string | null
stageNumber: number | null
title: string | null
enteredStageAt: string | null
actorId: string | null
}
export interface AshbyApplication {
id: string
createdAt: string | null
@@ -326,6 +348,7 @@ export interface AshbyApplication {
appliedViaJobPostingId: string | null
submitterClientIp: string | null
submitterUserAgent: string | null
applicationHistory: AshbyApplicationHistoryEntry[]
}
export interface AshbyListApplicationsResponse extends ToolResponse {
+51 -6
View File
@@ -1,5 +1,10 @@
import type { AshbyGetCandidateResponse } from '@/tools/ashby/types'
import { CANDIDATE_OUTPUTS, mapCandidate } from '@/tools/ashby/utils'
import {
ashbyAuthHeaders,
ashbyErrorMessage,
CANDIDATE_OUTPUTS,
mapCandidate,
} from '@/tools/ashby/utils'
import type { ToolConfig } from '@/tools/types'
interface AshbyUpdateCandidateParams {
@@ -11,7 +16,12 @@ interface AshbyUpdateCandidateParams {
linkedInUrl?: string
githubUrl?: string
websiteUrl?: string
alternateEmail?: string
sourceId?: string
creditedToUserId?: string
createdAt?: string
sendNotifications?: boolean
socialLinks?: Array<{ type: string; url: string }>
}
export const updateCandidateTool: ToolConfig<
@@ -72,21 +82,50 @@ export const updateCandidateTool: ToolConfig<
visibility: 'user-or-llm',
description: 'Personal website URL',
},
alternateEmail: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'An additional email address to add to the candidate',
},
sourceId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'UUID of the source to attribute the candidate to',
},
creditedToUserId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'UUID of the Ashby user to credit with sourcing this candidate',
},
createdAt: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Backdated creation timestamp in ISO 8601. Only updatable if originally backdated.',
},
sendNotifications: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether to send a notification when the source is updated (default true)',
},
socialLinks: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description:
'Array of social link objects to set on the candidate, e.g. [{"type":"LinkedIn","url":"https://..."}]. Replaces existing social links.',
},
},
request: {
url: 'https://api.ashbyhq.com/candidate.update',
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${params.apiKey}:`)}`,
}),
headers: (params) => ashbyAuthHeaders(params.apiKey),
body: (params) => {
const body: Record<string, unknown> = {
candidateId: params.candidateId.trim(),
@@ -97,7 +136,13 @@ export const updateCandidateTool: ToolConfig<
if (params.linkedInUrl) body.linkedInUrl = params.linkedInUrl
if (params.githubUrl) body.githubUrl = params.githubUrl
if (params.websiteUrl) body.websiteUrl = params.websiteUrl
if (params.alternateEmail) body.alternateEmail = params.alternateEmail
if (params.sourceId) body.sourceId = params.sourceId.trim()
if (params.creditedToUserId) body.creditedToUserId = params.creditedToUserId.trim()
if (params.createdAt) body.createdAt = params.createdAt
if (params.sendNotifications !== undefined) body.sendNotifications = params.sendNotifications
if (Array.isArray(params.socialLinks) && params.socialLinks.length > 0)
body.socialLinks = params.socialLinks
return body
},
},
@@ -106,7 +151,7 @@ export const updateCandidateTool: ToolConfig<
const data = await response.json()
if (!data.success) {
throw new Error(data.errorInfo?.message || 'Failed to update candidate')
throw new Error(ashbyErrorMessage(data, 'Failed to update candidate'))
}
return {
+71 -6
View File
@@ -17,6 +17,33 @@ import type { OutputProperty } from '@/tools/types'
type Unknown = Record<string, unknown>
/**
* Build the standard Ashby Authorization header. Ashby uses HTTP Basic auth
* with the API key as the username and an empty password.
*/
export function ashbyAuthHeaders(apiKey: string): Record<string, string> {
return {
'Content-Type': 'application/json',
Accept: 'application/json; version=1',
Authorization: `Basic ${btoa(`${apiKey}:`)}`,
}
}
/**
* Extract a human-readable error message from an Ashby error response. Ashby
* returns errors as either `errorInfo.message` or an `errors` string array.
*/
export function ashbyErrorMessage(data: unknown, fallback: string): string {
if (!data || typeof data !== 'object') return fallback
const d = data as Unknown
const info = d.errorInfo as Unknown | undefined
if (info && typeof info.message === 'string' && info.message) return info.message
if (Array.isArray(d.errors) && d.errors.length > 0) {
return d.errors.map((e) => String(e)).join('; ')
}
return fallback
}
function mapContact(raw: unknown): AshbyContactInfo | null {
if (!raw || typeof raw !== 'object') return null
const c = raw as Unknown
@@ -322,6 +349,16 @@ export function mapApplication(raw: unknown): AshbyApplication {
appliedViaJobPostingId: (a.appliedViaJobPostingId as string) ?? null,
submitterClientIp: (a.submitterClientIp as string) ?? null,
submitterUserAgent: (a.submitterUserAgent as string) ?? null,
applicationHistory: Array.isArray(a.applicationHistory)
? (a.applicationHistory as Unknown[]).map((h) => ({
id: (h.id as string) ?? '',
stageId: (h.stageId as string) ?? null,
stageNumber: (h.stageNumber as number) ?? null,
title: (h.title as string) ?? null,
enteredStageAt: (h.enteredStageAt as string) ?? null,
actorId: (h.actorId as string) ?? null,
}))
: [],
}
}
@@ -387,7 +424,7 @@ export const USER_SUMMARY_OUTPUT = {
globalRole: { type: 'string', description: 'Role', optional: true },
isEnabled: { type: 'boolean', description: 'Whether enabled' },
updatedAt: { type: 'string', description: 'Last update timestamp', optional: true },
managerId: { type: 'string', description: 'Manager user UUID', optional: true },
managerId: { type: 'string', description: "User ID of the user's manager", optional: true },
},
} as const satisfies OutputProperty
@@ -591,6 +628,29 @@ export const APPLICATION_OUTPUTS = {
},
createdAt: { type: 'string', description: 'ISO 8601 creation timestamp' },
updatedAt: { type: 'string', description: 'ISO 8601 last update timestamp' },
applicationHistory: {
type: 'array',
description: 'Stage history (only populated by application.info, empty for list endpoints)',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'History entry UUID' },
stageId: { type: 'string', description: 'Interview stage UUID', optional: true },
stageNumber: { type: 'number', description: 'Stage order number', optional: true },
title: { type: 'string', description: 'Stage title at the time', optional: true },
enteredStageAt: {
type: 'string',
description: 'ISO 8601 timestamp the stage was entered',
optional: true,
},
actorId: {
type: 'string',
description: 'User UUID who triggered the stage change',
optional: true,
},
},
},
},
} as const satisfies Record<string, OutputProperty>
export const OPENINGS_OUTPUT = {
@@ -823,23 +883,28 @@ export const JOB_OUTPUTS = {
openings: OPENINGS_OUTPUT,
compensation: {
type: 'object',
description: 'Job compensation structure',
description:
'Compensation tiers for the job. Only present when the request includes the `compensation` expand parameter.',
optional: true,
properties: {
compensationTiers: {
type: 'array',
description: 'Compensation tiers',
description: 'List of compensation tiers',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Tier UUID', optional: true },
id: { type: 'string', description: 'Tier ID', optional: true },
title: { type: 'string', description: 'Tier title', optional: true },
additionalInformation: {
type: 'string',
description: 'Additional info',
description: 'Additional information about the tier',
optional: true,
},
tierSummary: {
type: 'string',
description: 'Human-readable summary of the tier',
optional: true,
},
tierSummary: { type: 'string', description: 'Tier summary', optional: true },
},
},
},
+13
View File
@@ -14,6 +14,8 @@ export interface ConfluenceDeleteSpaceResponse {
ts: string
spaceId: string
deleted: boolean
longTaskId?: string
longTaskStatusLink?: string
}
}
@@ -83,6 +85,8 @@ export const confluenceDeleteSpaceTool: ToolConfig<
ts: new Date().toISOString(),
spaceId: data.spaceId ?? '',
deleted: true,
longTaskId: data.longTaskId,
longTaskStatusLink: data.longTaskStatusLink,
},
}
},
@@ -91,5 +95,14 @@ export const confluenceDeleteSpaceTool: ToolConfig<
ts: TIMESTAMP_OUTPUT,
spaceId: { type: 'string', description: 'Deleted space ID' },
deleted: { type: 'boolean', description: 'Deletion status' },
longTaskId: {
type: 'string',
description:
'ID of the long-running deletion task; poll Confluence long-task API to track completion',
},
longTaskStatusLink: {
type: 'string',
description: 'Relative link to the long-task status endpoint',
},
},
}
-1
View File
@@ -525,7 +525,6 @@ export interface ConfluenceUpdateParams {
pageId: string
title?: string
content?: string
version?: number
cloudId?: string
}
+2 -16
View File
@@ -44,12 +44,6 @@ export const confluenceUpdateTool: ToolConfig<ConfluenceUpdateParams, Confluence
visibility: 'user-or-llm',
description: 'New content for the page in Confluence storage format',
},
version: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Version number of the page (required for preventing conflicts)',
},
cloudId: {
type: 'string',
required: false,
@@ -78,16 +72,8 @@ export const confluenceUpdateTool: ToolConfig<ConfluenceUpdateParams, Confluence
pageId: params.pageId,
cloudId: params.cloudId,
title: params.title,
body: params.content
? {
representation: 'storage',
value: params.content,
}
: undefined,
version: {
number: params.version || 1,
message: params.version ? 'Updated via Sim' : 'Initial update via Sim',
},
body: params.content ? { value: params.content } : undefined,
version: { message: 'Updated via Sim' },
}
return body
},
+5 -2
View File
@@ -77,10 +77,13 @@ export const copyTool: ToolConfig<GoogleDriveCopyParams, GoogleDriveCopyResponse
},
transformResponse: async (response: Response) => {
const data = await response.json()
const data = await response.json().catch(() => ({}) as any)
if (!response.ok) {
throw new Error(data.error?.message || 'Failed to copy Google Drive file')
throw new Error(
data.error?.message ||
`Failed to copy Google Drive file (${response.status} ${response.statusText})`
)
}
return {
+2 -2
View File
@@ -61,9 +61,9 @@ export const createFolderTool: ToolConfig<GoogleDriveToolParams, GoogleDriveUplo
}
// Add parent folder if specified (prefer folderSelector over folderId)
const parentFolderId = params.folderSelector || params.folderId
const parentFolderId = (params.folderSelector || params.folderId)?.trim()
if (parentFolderId) {
metadata.parents = [parentFolderId]
metadata.parents = [parentFolderId.trim()]
}
return metadata
+5 -2
View File
@@ -52,8 +52,11 @@ export const deleteTool: ToolConfig<GoogleDriveDeleteParams, GoogleDriveDeleteRe
transformResponse: async (response: Response, params) => {
if (!response.ok) {
const data = await response.json()
throw new Error(data.error?.message || 'Failed to delete Google Drive file')
const data = await response.json().catch(() => ({}) as any)
throw new Error(
data.error?.message ||
`Failed to delete Google Drive file (${response.status} ${response.statusText})`
)
}
return {
+1 -1
View File
@@ -57,7 +57,7 @@ export const getContentTool: ToolConfig<GoogleDriveToolParams, GoogleDriveGetCon
request: {
url: (params) =>
`https://www.googleapis.com/drive/v3/files/${params.fileId}?fields=${ALL_FILE_FIELDS}&supportsAllDrives=true`,
`https://www.googleapis.com/drive/v3/files/${params.fileId?.trim()}?fields=${ALL_FILE_FIELDS}&supportsAllDrives=true`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
+5 -2
View File
@@ -52,10 +52,13 @@ export const getFileTool: ToolConfig<GoogleDriveGetFileParams, GoogleDriveGetFil
},
transformResponse: async (response: Response) => {
const data = await response.json()
const data = await response.json().catch(() => ({}) as any)
if (!response.ok) {
throw new Error(data.error?.message || 'Failed to get Google Drive file')
throw new Error(
data.error?.message ||
`Failed to get Google Drive file (${response.status} ${response.statusText})`
)
}
return {
+2 -2
View File
@@ -68,7 +68,7 @@ export const listTool: ToolConfig<GoogleDriveToolParams, GoogleDriveListResponse
// Build the query conditions
const conditions = ['trashed = false'] // Always exclude trashed files
const folderId = params.folderId || params.folderSelector
const folderId = (params.folderId || params.folderSelector)?.trim()
if (folderId) {
const escapedFolderId = escapeQueryValue(folderId)
conditions.push(`'${escapedFolderId}' in parents`)
@@ -108,7 +108,7 @@ export const listTool: ToolConfig<GoogleDriveToolParams, GoogleDriveListResponse
return {
success: true,
output: {
files: data.files,
files: data.files ?? [],
nextPageToken: data.nextPageToken,
},
}
@@ -3,6 +3,7 @@ import type { ToolConfig, ToolResponse } from '@/tools/types'
interface GoogleDriveListPermissionsParams extends GoogleDriveToolParams {
fileId: string
pageToken?: string
}
interface GoogleDriveListPermissionsResponse extends ToolResponse {
@@ -39,6 +40,12 @@ export const listPermissionsTool: ToolConfig<
visibility: 'user-or-llm',
description: 'The ID of the file to list permissions for',
},
pageToken: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'The page token to use for pagination',
},
},
request: {
@@ -51,6 +58,9 @@ export const listPermissionsTool: ToolConfig<
'fields',
'nextPageToken,permissions(id,type,role,emailAddress,displayName,photoLink,domain,expirationTime,deleted,allowFileDiscovery,pendingOwner,permissionDetails)'
)
if (params.pageToken) {
url.searchParams.append('pageToken', params.pageToken)
}
return url.toString()
},
method: 'GET',
+4 -2
View File
@@ -104,10 +104,12 @@ export const shareTool: ToolConfig<GoogleDriveShareParams, GoogleDriveShareRespo
if (params.transferOwnership) {
url.searchParams.append('transferOwnership', 'true')
}
if (params.moveToNewOwnersRoot) {
if (params.transferOwnership && params.moveToNewOwnersRoot) {
url.searchParams.append('moveToNewOwnersRoot', 'true')
}
if (params.sendNotification !== undefined) {
if (params.transferOwnership) {
url.searchParams.append('sendNotificationEmail', 'true')
} else if (params.sendNotification !== undefined) {
url.searchParams.append('sendNotificationEmail', String(params.sendNotification))
}
if (params.emailMessage) {
+5 -2
View File
@@ -107,10 +107,13 @@ export const updateTool: ToolConfig<GoogleDriveUpdateParams, GoogleDriveUpdateRe
},
transformResponse: async (response: Response) => {
const data = await response.json()
const data = await response.json().catch(() => ({}) as any)
if (!response.ok) {
throw new Error(data.error?.message || 'Failed to update Google Drive file')
throw new Error(
data.error?.message ||
`Failed to update Google Drive file (${response.status} ${response.statusText})`
)
}
return {
+5 -27
View File
@@ -1,6 +1,6 @@
import type { JiraAddCommentParams, JiraAddCommentResponse } from '@/tools/jira/types'
import { SUCCESS_OUTPUT, TIMESTAMP_OUTPUT, USER_OUTPUT_PROPERTIES } from '@/tools/jira/types'
import { extractAdfText, getJiraCloudId, transformUser } from '@/tools/jira/utils'
import { extractAdfText, getJiraCloudId, toAdf, transformUser } from '@/tools/jira/utils'
import type { ToolConfig } from '@/tools/types'
/**
@@ -74,7 +74,7 @@ export const jiraAddCommentTool: ToolConfig<JiraAddCommentParams, JiraAddComment
request: {
url: (params: JiraAddCommentParams) => {
if (params.cloudId) {
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/comment`
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}/comment`
}
return 'https://api.atlassian.com/oauth/token/accessible-resources'
},
@@ -88,40 +88,18 @@ export const jiraAddCommentTool: ToolConfig<JiraAddCommentParams, JiraAddComment
},
body: (params: JiraAddCommentParams) => {
if (!params.cloudId) return undefined as any
const payload: Record<string, any> = {
body: {
type: 'doc',
version: 1,
content: [
{
type: 'paragraph',
content: [{ type: 'text', text: params.body ?? '' }],
},
],
},
}
const payload: Record<string, any> = { body: toAdf(params.body ?? '') }
if (params.visibility) payload.visibility = params.visibility
return payload
},
},
transformResponse: async (response: Response, params?: JiraAddCommentParams) => {
const payload: Record<string, any> = {
body: {
type: 'doc',
version: 1,
content: [
{
type: 'paragraph',
content: [{ type: 'text', text: params?.body ?? '' }],
},
],
},
}
const payload: Record<string, any> = { body: toAdf(params?.body ?? '') }
if (params?.visibility) payload.visibility = params.visibility
const makeRequest = async (cloudId: string) => {
const commentUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey}/comment`
const commentUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey?.trim() ?? ''}/comment`
const commentResponse = await fetch(commentUrl, {
method: 'POST',
headers: {
+8 -2
View File
@@ -51,7 +51,7 @@ export const jiraAddWatcherTool: ToolConfig<JiraAddWatcherParams, JiraAddWatcher
request: {
url: (params: JiraAddWatcherParams) => {
if (params.cloudId) {
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/watchers`
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}/watchers`
}
return 'https://api.atlassian.com/oauth/token/accessible-resources'
},
@@ -65,14 +65,20 @@ export const jiraAddWatcherTool: ToolConfig<JiraAddWatcherParams, JiraAddWatcher
},
body: (params: JiraAddWatcherParams) => {
if (!params.cloudId) return undefined as any
if (!params.accountId) {
throw new Error('accountId is required to add a Jira watcher')
}
return params.accountId as any
},
},
transformResponse: async (response: Response, params?: JiraAddWatcherParams) => {
if (!params?.accountId) {
throw new Error('accountId is required to add a Jira watcher')
}
if (!params?.cloudId) {
const cloudId = await getJiraCloudId(params!.domain, params!.accessToken)
const watcherUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey}/watchers`
const watcherUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey?.trim() ?? ''}/watchers`
const watcherResponse = await fetch(watcherUrl, {
method: 'POST',
headers: {
+18 -21
View File
@@ -1,29 +1,25 @@
import type { JiraAddWorklogParams, JiraAddWorklogResponse } from '@/tools/jira/types'
import { SUCCESS_OUTPUT, TIMESTAMP_OUTPUT, USER_OUTPUT_PROPERTIES } from '@/tools/jira/types'
import { getJiraCloudId, transformUser } from '@/tools/jira/utils'
import {
getJiraCloudId,
normalizeJiraWorklogTimestamp,
toAdf,
transformUser,
} from '@/tools/jira/utils'
import type { ToolConfig } from '@/tools/types'
/**
* Builds the worklog request body per Jira API v3.
*/
function buildWorklogBody(params: JiraAddWorklogParams) {
const t = Number(params.timeSpentSeconds)
if (!Number.isFinite(t) || t <= 0) {
throw new Error('timeSpentSeconds must be a positive finite number')
}
const body: Record<string, any> = {
timeSpentSeconds: Number(params.timeSpentSeconds),
comment: params.comment
? {
type: 'doc',
version: 1,
content: [
{
type: 'paragraph',
content: [{ type: 'text', text: params.comment }],
},
],
}
: undefined,
started:
(params.started ? params.started.replace(/Z$/, '+0000') : undefined) ||
new Date().toISOString().replace(/Z$/, '+0000'),
timeSpentSeconds: t,
comment: params.comment ? toAdf(params.comment) : undefined,
started: normalizeJiraWorklogTimestamp(params.started || new Date().toISOString()),
}
if (params.visibility) body.visibility = params.visibility
return body
@@ -113,7 +109,7 @@ export const jiraAddWorklogTool: ToolConfig<JiraAddWorklogParams, JiraAddWorklog
request: {
url: (params: JiraAddWorklogParams) => {
if (params.cloudId) {
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/worklog`
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}/worklog`
}
return 'https://api.atlassian.com/oauth/token/accessible-resources'
},
@@ -132,12 +128,13 @@ export const jiraAddWorklogTool: ToolConfig<JiraAddWorklogParams, JiraAddWorklog
},
transformResponse: async (response: Response, params?: JiraAddWorklogParams) => {
if (!params?.timeSpentSeconds || params.timeSpentSeconds <= 0) {
throw new Error('timeSpentSeconds is required and must be greater than 0')
const t = Number(params?.timeSpentSeconds)
if (!params?.timeSpentSeconds || !Number.isFinite(t) || t <= 0) {
throw new Error('timeSpentSeconds is required and must be a positive finite number')
}
const makeRequest = async (cloudId: string) => {
const worklogUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey}/worklog`
const worklogUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey?.trim() ?? ''}/worklog`
const worklogResponse = await fetch(worklogUrl, {
method: 'POST',
headers: {
+19 -9
View File
@@ -3,6 +3,20 @@ import { SUCCESS_OUTPUT, TIMESTAMP_OUTPUT } from '@/tools/jira/types'
import { getJiraCloudId } from '@/tools/jira/utils'
import type { ToolConfig } from '@/tools/types'
/**
* Maps user-provided accountId to the Jira API value.
* Empty string, "null", "none", or "unassigned" → null (unassign).
* "-1" → "-1" (auto-assign). Otherwise the trimmed accountId.
*/
function resolveAssigneeAccountId(value: string | null | undefined): string | null {
if (value === null || value === undefined) return null
const trimmed = String(value).trim()
if (trimmed === '') return null
const lower = trimmed.toLowerCase()
if (lower === 'null' || lower === 'none' || lower === 'unassigned') return null
return trimmed
}
export const jiraAssignIssueTool: ToolConfig<JiraAssignIssueParams, JiraAssignIssueResponse> = {
id: 'jira_assign_issue',
name: 'Jira Assign Issue',
@@ -38,7 +52,7 @@ export const jiraAssignIssueTool: ToolConfig<JiraAssignIssueParams, JiraAssignIs
required: true,
visibility: 'user-or-llm',
description:
'Account ID of the user to assign the issue to. Use "-1" for automatic assignment or null to unassign.',
'Account ID of the user to assign the issue to. Use "-1" for automatic assignment, or leave empty / pass "null" to unassign.',
},
cloudId: {
type: 'string',
@@ -52,7 +66,7 @@ export const jiraAssignIssueTool: ToolConfig<JiraAssignIssueParams, JiraAssignIs
request: {
url: (params: JiraAssignIssueParams) => {
if (params.cloudId) {
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/assignee`
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}/assignee`
}
return 'https://api.atlassian.com/oauth/token/accessible-resources'
},
@@ -66,16 +80,14 @@ export const jiraAssignIssueTool: ToolConfig<JiraAssignIssueParams, JiraAssignIs
},
body: (params: JiraAssignIssueParams) => {
if (!params.cloudId) return undefined as any
return {
accountId: params.accountId === 'null' ? null : params.accountId,
}
return { accountId: resolveAssigneeAccountId(params.accountId) }
},
},
transformResponse: async (response: Response, params?: JiraAssignIssueParams) => {
if (!params?.cloudId) {
const cloudId = await getJiraCloudId(params!.domain, params!.accessToken)
const assignUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey}/assignee`
const assignUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey?.trim() ?? ''}/assignee`
const assignResponse = await fetch(assignUrl, {
method: 'PUT',
headers: {
@@ -83,9 +95,7 @@ export const jiraAssignIssueTool: ToolConfig<JiraAssignIssueParams, JiraAssignIs
'Content-Type': 'application/json',
Authorization: `Bearer ${params!.accessToken}`,
},
body: JSON.stringify({
accountId: params!.accountId === 'null' ? null : params!.accountId,
}),
body: JSON.stringify({ accountId: resolveAssigneeAccountId(params!.accountId) }),
})
if (!assignResponse.ok) {
+17 -7
View File
@@ -1,6 +1,6 @@
import type { JiraRetrieveBulkParams, JiraRetrieveResponseBulk } from '@/tools/jira/types'
import { TIMESTAMP_OUTPUT } from '@/tools/jira/types'
import { extractAdfText } from '@/tools/jira/utils'
import { extractAdfText, normalizeDomain } from '@/tools/jira/utils'
import type { ToolConfig } from '@/tools/types'
export const jiraBulkRetrieveTool: ToolConfig<JiraRetrieveBulkParams, JiraRetrieveResponseBulk> = {
@@ -71,19 +71,29 @@ export const jiraBulkRetrieveTool: ToolConfig<JiraRetrieveBulkParams, JiraRetrie
const resolveCloudId = async () => {
if (params?.cloudId) return params.cloudId
const accessibleResources = await response.json()
const normalizedInput = `https://${params?.domain}`.toLowerCase()
if (!Array.isArray(accessibleResources) || accessibleResources.length === 0) {
throw new Error('No Jira resources found')
}
const normalizedInput = normalizeDomain(params?.domain ?? '')
const matchedResource = accessibleResources.find(
(r: any) => r.url.toLowerCase() === normalizedInput
(r: { url: string }) => r.url.toLowerCase().replace(/\/+$/, '') === normalizedInput
)
if (matchedResource) return matchedResource.id
if (Array.isArray(accessibleResources) && accessibleResources.length > 0)
return accessibleResources[0].id
throw new Error('No Jira resources found')
if (accessibleResources.length === 1) return accessibleResources[0].id
throw new Error(
`Could not match Jira domain "${params?.domain}" to any accessible resource. ` +
`Available sites: ${accessibleResources.map((r: { url: string }) => r.url).join(', ')}`
)
}
const cloudId = await resolveCloudId()
const projectKey = await resolveProjectKey(cloudId, params!.accessToken, params!.projectId)
const jql = `project = ${projectKey} ORDER BY updated DESC`
if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(projectKey)) {
throw new Error(
`Invalid Jira project key "${projectKey}". Expected an alphanumeric project key (e.g., PROJ).`
)
}
const jql = `project = "${projectKey}" ORDER BY updated DESC`
let collected: any[] = []
let nextPageToken: string | undefined
+4 -22
View File
@@ -1,6 +1,6 @@
import type { JiraCreateIssueLinkParams, JiraCreateIssueLinkResponse } from '@/tools/jira/types'
import { SUCCESS_OUTPUT, TIMESTAMP_OUTPUT } from '@/tools/jira/types'
import { getJiraCloudId } from '@/tools/jira/utils'
import { getJiraCloudId, toAdf } from '@/tools/jira/utils'
import type { ToolConfig } from '@/tools/types'
export const jiraCreateIssueLinkTool: ToolConfig<
@@ -124,27 +124,9 @@ export const jiraCreateIssueLinkTool: ToolConfig<
},
body: JSON.stringify({
type: resolvedType,
inwardIssue: { key: params!.inwardIssueKey },
outwardIssue: { key: params!.outwardIssueKey },
comment: params?.comment
? {
body: {
type: 'doc',
version: 1,
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: params!.comment,
},
],
},
],
},
}
: undefined,
inwardIssue: { key: params!.inwardIssueKey?.trim() ?? '' },
outwardIssue: { key: params!.outwardIssueKey?.trim() ?? '' },
comment: params?.comment ? { body: toAdf(params.comment) } : undefined,
}),
})
if (!linkResponse.ok) {
+2 -2
View File
@@ -48,7 +48,7 @@ export const jiraDeleteAttachmentTool: ToolConfig<
request: {
url: (params: JiraDeleteAttachmentParams) => {
if (params.cloudId) {
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/attachment/${params.attachmentId}`
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/attachment/${params.attachmentId?.trim() ?? ''}`
}
return 'https://api.atlassian.com/oauth/token/accessible-resources'
},
@@ -65,7 +65,7 @@ export const jiraDeleteAttachmentTool: ToolConfig<
if (!params?.cloudId) {
const cloudId = await getJiraCloudId(params!.domain, params!.accessToken)
// Make the actual request with the resolved cloudId
const attachmentUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/attachment/${params?.attachmentId}`
const attachmentUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/attachment/${params?.attachmentId?.trim() ?? ''}`
const attachmentResponse = await fetch(attachmentUrl, {
method: 'DELETE',
headers: {
+2 -2
View File
@@ -52,7 +52,7 @@ export const jiraDeleteCommentTool: ToolConfig<JiraDeleteCommentParams, JiraDele
request: {
url: (params: JiraDeleteCommentParams) => {
if (params.cloudId) {
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/comment/${params.commentId}`
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}/comment/${params.commentId?.trim() ?? ''}`
}
return 'https://api.atlassian.com/oauth/token/accessible-resources'
},
@@ -69,7 +69,7 @@ export const jiraDeleteCommentTool: ToolConfig<JiraDeleteCommentParams, JiraDele
if (!params?.cloudId) {
const cloudId = await getJiraCloudId(params!.domain, params!.accessToken)
// Make the actual request with the resolved cloudId
const commentUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params?.issueKey}/comment/${params?.commentId}`
const commentUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params?.issueKey?.trim() ?? ''}/comment/${params?.commentId?.trim() ?? ''}`
const commentResponse = await fetch(commentUrl, {
method: 'DELETE',
headers: {
+2 -2
View File
@@ -53,7 +53,7 @@ export const jiraDeleteIssueTool: ToolConfig<JiraDeleteIssueParams, JiraDeleteIs
url: (params: JiraDeleteIssueParams) => {
if (params.cloudId) {
const deleteSubtasksParam = params.deleteSubtasks ? '?deleteSubtasks=true' : ''
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}${deleteSubtasksParam}`
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}${deleteSubtasksParam}`
}
return 'https://api.atlassian.com/oauth/token/accessible-resources'
},
@@ -70,7 +70,7 @@ export const jiraDeleteIssueTool: ToolConfig<JiraDeleteIssueParams, JiraDeleteIs
if (!params?.cloudId) {
const cloudId = await getJiraCloudId(params!.domain, params!.accessToken)
const deleteSubtasksParam = params!.deleteSubtasks ? '?deleteSubtasks=true' : ''
const deleteUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey}${deleteSubtasksParam}`
const deleteUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey?.trim() ?? ''}${deleteSubtasksParam}`
const deleteResponse = await fetch(deleteUrl, {
method: 'DELETE',
headers: {
+2 -2
View File
@@ -48,7 +48,7 @@ export const jiraDeleteIssueLinkTool: ToolConfig<
request: {
url: (params: JiraDeleteIssueLinkParams) => {
if (params.cloudId) {
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issueLink/${params.linkId}`
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issueLink/${params.linkId?.trim() ?? ''}`
}
return 'https://api.atlassian.com/oauth/token/accessible-resources'
},
@@ -64,7 +64,7 @@ export const jiraDeleteIssueLinkTool: ToolConfig<
transformResponse: async (response: Response, params?: JiraDeleteIssueLinkParams) => {
if (!params?.cloudId) {
const cloudId = await getJiraCloudId(params!.domain, params!.accessToken)
const issueLinkUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issueLink/${params!.linkId}`
const issueLinkUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issueLink/${params!.linkId?.trim() ?? ''}`
const issueLinkResponse = await fetch(issueLinkUrl, {
method: 'DELETE',
headers: {
+2 -2
View File
@@ -52,7 +52,7 @@ export const jiraDeleteWorklogTool: ToolConfig<JiraDeleteWorklogParams, JiraDele
request: {
url: (params: JiraDeleteWorklogParams) => {
if (params.cloudId) {
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/worklog/${params.worklogId}`
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}/worklog/${params.worklogId?.trim() ?? ''}`
}
return 'https://api.atlassian.com/oauth/token/accessible-resources'
},
@@ -68,7 +68,7 @@ export const jiraDeleteWorklogTool: ToolConfig<JiraDeleteWorklogParams, JiraDele
transformResponse: async (response: Response, params?: JiraDeleteWorklogParams) => {
if (!params?.cloudId) {
const cloudId = await getJiraCloudId(params!.domain, params!.accessToken)
const worklogUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey}/worklog/${params!.worklogId}`
const worklogUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey?.trim() ?? ''}/worklog/${params!.worklogId?.trim() ?? ''}`
const worklogResponse = await fetch(worklogUrl, {
method: 'DELETE',
headers: {
+2 -2
View File
@@ -72,7 +72,7 @@ export const jiraGetAttachmentsTool: ToolConfig<
request: {
url: (params: JiraGetAttachmentsParams) => {
if (params.cloudId) {
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}?fields=attachment`
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}?fields=attachment`
}
return 'https://api.atlassian.com/oauth/token/accessible-resources'
},
@@ -87,7 +87,7 @@ export const jiraGetAttachmentsTool: ToolConfig<
transformResponse: async (response: Response, params?: JiraGetAttachmentsParams) => {
const fetchAttachments = async (cloudId: string) => {
const attachmentsUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey}?fields=attachment`
const attachmentsUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey?.trim() ?? ''}?fields=attachment`
const attachmentsResponse = await fetch(attachmentsUrl, {
method: 'GET',
headers: {
+2 -2
View File
@@ -85,7 +85,7 @@ export const jiraGetCommentsTool: ToolConfig<JiraGetCommentsParams, JiraGetComme
const startAt = params.startAt ?? 0
const maxResults = params.maxResults ?? 50
const orderBy = params.orderBy ?? '-created'
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/comment?startAt=${startAt}&maxResults=${maxResults}&orderBy=${orderBy}`
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}/comment?startAt=${startAt}&maxResults=${maxResults}&orderBy=${orderBy}`
}
return 'https://api.atlassian.com/oauth/token/accessible-resources'
},
@@ -103,7 +103,7 @@ export const jiraGetCommentsTool: ToolConfig<JiraGetCommentsParams, JiraGetComme
const startAt = params?.startAt ?? 0
const maxResults = params?.maxResults ?? 50
const orderBy = params?.orderBy ?? '-created'
const commentsUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey}/comment?startAt=${startAt}&maxResults=${maxResults}&orderBy=${orderBy}`
const commentsUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey?.trim() ?? ''}/comment?startAt=${startAt}&maxResults=${maxResults}&orderBy=${orderBy}`
const commentsResponse = await fetch(commentsUrl, {
method: 'GET',
headers: {
+2 -2
View File
@@ -77,7 +77,7 @@ export const jiraGetWorklogsTool: ToolConfig<JiraGetWorklogsParams, JiraGetWorkl
if (params.cloudId) {
const startAt = params.startAt ?? 0
const maxResults = params.maxResults ?? 50
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/worklog?startAt=${startAt}&maxResults=${maxResults}`
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}/worklog?startAt=${startAt}&maxResults=${maxResults}`
}
return 'https://api.atlassian.com/oauth/token/accessible-resources'
},
@@ -94,7 +94,7 @@ export const jiraGetWorklogsTool: ToolConfig<JiraGetWorklogsParams, JiraGetWorkl
const fetchWorklogs = async (cloudId: string) => {
const startAt = params?.startAt ?? 0
const maxResults = params?.maxResults ?? 50
const worklogsUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey}/worklog?startAt=${startAt}&maxResults=${maxResults}`
const worklogsUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey?.trim() ?? ''}/worklog?startAt=${startAt}&maxResults=${maxResults}`
const worklogsResponse = await fetch(worklogsUrl, {
method: 'GET',
headers: {
+2 -2
View File
@@ -52,7 +52,7 @@ export const jiraRemoveWatcherTool: ToolConfig<JiraRemoveWatcherParams, JiraRemo
request: {
url: (params: JiraRemoveWatcherParams) => {
if (params.cloudId) {
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/watchers?accountId=${params.accountId}`
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}/watchers?accountId=${encodeURIComponent(params.accountId?.trim() ?? '')}`
}
return 'https://api.atlassian.com/oauth/token/accessible-resources'
},
@@ -68,7 +68,7 @@ export const jiraRemoveWatcherTool: ToolConfig<JiraRemoveWatcherParams, JiraRemo
transformResponse: async (response: Response, params?: JiraRemoveWatcherParams) => {
if (!params?.cloudId) {
const cloudId = await getJiraCloudId(params!.domain, params!.accessToken)
const watcherUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey}/watchers?accountId=${params!.accountId}`
const watcherUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey?.trim() ?? ''}/watchers?accountId=${encodeURIComponent(params!.accountId?.trim() ?? '')}`
const watcherResponse = await fetch(watcherUrl, {
method: 'DELETE',
headers: {
+3 -3
View File
@@ -232,7 +232,7 @@ export const jiraRetrieveTool: ToolConfig<JiraRetrieveParams, JiraRetrieveRespon
request: {
url: (params: JiraRetrieveParams) => {
if (params.cloudId) {
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}?expand=renderedFields,names,schema,transitions,operations,editmeta,changelog,versionedRepresentations`
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}?expand=renderedFields,names,schema,transitions,operations,editmeta,changelog,versionedRepresentations`
}
return 'https://api.atlassian.com/oauth/token/accessible-resources'
},
@@ -251,7 +251,7 @@ export const jiraRetrieveTool: ToolConfig<JiraRetrieveParams, JiraRetrieveRespon
}
const fetchIssue = async (cloudId: string) => {
const issueUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params.issueKey}?expand=renderedFields,names,schema,transitions,operations,editmeta,changelog,versionedRepresentations`
const issueUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}?expand=renderedFields,names,schema,transitions,operations,editmeta,changelog,versionedRepresentations`
const issueResponse = await fetch(issueUrl, {
method: 'GET',
headers: {
@@ -273,7 +273,7 @@ export const jiraRetrieveTool: ToolConfig<JiraRetrieveParams, JiraRetrieveRespon
}
const fetchSupplementary = async (cloudId: string, data: any) => {
const base = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params.issueKey}`
const base = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}`
const [commentsResp, worklogResp, watchersResp] = await Promise.all([
fetch(`${base}/comment?maxResults=100&orderBy=-created`, {
headers: { Accept: 'application/json', Authorization: `Bearer ${params.accessToken}` },
+5 -5
View File
@@ -117,8 +117,7 @@ export const jiraSearchIssuesTool: ToolConfig<JiraSearchIssuesParams, JiraSearch
type: 'array',
required: false,
visibility: 'user-or-llm',
description:
'Array of field names to return (default: all navigable). Use "*all" for every field.',
description: 'Array of field names to return (default: all fields).',
},
cloudId: {
type: 'string',
@@ -211,8 +210,8 @@ export const jiraSearchIssuesTool: ToolConfig<JiraSearchIssuesParams, JiraSearch
ts: new Date().toISOString(),
issues: (data?.issues ?? []).map(transformSearchIssue),
nextPageToken: data?.nextPageToken ?? null,
isLast: data?.isLast ?? true,
total: data?.total ?? null,
isLast: data?.isLast ?? !data?.nextPageToken,
total: null,
},
}
},
@@ -235,7 +234,8 @@ export const jiraSearchIssuesTool: ToolConfig<JiraSearchIssuesParams, JiraSearch
isLast: { type: 'boolean', description: 'Whether this is the last page of results' },
total: {
type: 'number',
description: 'Total number of matching issues (may not always be available)',
description:
'Always null. The Jira /search/jql endpoint does not return a total count; use isLast and nextPageToken for pagination.',
optional: true,
},
},
+5 -20
View File
@@ -1,6 +1,6 @@
import type { JiraTransitionIssueParams, JiraTransitionIssueResponse } from '@/tools/jira/types'
import { SUCCESS_OUTPUT, TIMESTAMP_OUTPUT } from '@/tools/jira/types'
import { getJiraCloudId } from '@/tools/jira/utils'
import { getJiraCloudId, toAdf } from '@/tools/jira/utils'
import type { ToolConfig } from '@/tools/types'
export const jiraTransitionIssueTool: ToolConfig<
@@ -67,7 +67,7 @@ export const jiraTransitionIssueTool: ToolConfig<
request: {
url: (params: JiraTransitionIssueParams) => {
if (params.cloudId) {
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/transitions`
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}/transitions`
}
return 'https://api.atlassian.com/oauth/token/accessible-resources'
},
@@ -88,7 +88,7 @@ export const jiraTransitionIssueTool: ToolConfig<
transformResponse: async (response: Response, params?: JiraTransitionIssueParams) => {
const performTransition = async (cloudId: string) => {
// First, fetch available transitions to get the name and target status
const transitionsUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey}/transitions`
const transitionsUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey?.trim() ?? ''}/transitions`
const transitionsResp = await fetch(transitionsUrl, {
method: 'GET',
headers: {
@@ -158,7 +158,7 @@ export const jiraTransitionIssueTool: ToolConfig<
// Fetch transition metadata for the response
try {
const transitionsUrl = `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/transitions`
const transitionsUrl = `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}/transitions`
const transitionsResp = await fetch(transitionsUrl, {
method: 'GET',
headers: {
@@ -229,22 +229,7 @@ function buildTransitionBody(params: JiraTransitionIssueParams) {
if (params.comment) {
body.update = {
comment: [
{
add: {
body: {
type: 'doc',
version: 1,
content: [
{
type: 'paragraph',
content: [{ type: 'text', text: params.comment }],
},
],
},
},
},
],
comment: [{ add: { body: toAdf(params.comment) } }],
}
}
+70 -70
View File
@@ -806,30 +806,30 @@ export interface JiraRetrieveResponse extends ToolResponse {
assignee: {
accountId: string
displayName: string
active?: boolean
emailAddress?: string
avatarUrl?: string
accountType?: string
timeZone?: string
active: boolean | null
emailAddress: string | null
avatarUrl: string | null
accountType: string | null
timeZone: string | null
} | null
assigneeName: string | null
reporter: {
accountId: string
displayName: string
active?: boolean
emailAddress?: string
avatarUrl?: string
accountType?: string
timeZone?: string
active: boolean | null
emailAddress: string | null
avatarUrl: string | null
accountType: string | null
timeZone: string | null
} | null
creator: {
accountId: string
displayName: string
active?: boolean
emailAddress?: string
avatarUrl?: string
accountType?: string
timeZone?: string
active: boolean | null
emailAddress: string | null
avatarUrl: string | null
accountType: string | null
timeZone: string | null
} | null
labels: string[]
components: Array<{ id: string; name: string; description?: string }>
@@ -869,21 +869,21 @@ export interface JiraRetrieveResponse extends ToolResponse {
author: {
accountId: string
displayName: string
active?: boolean
emailAddress?: string
avatarUrl?: string
accountType?: string
timeZone?: string
active: boolean | null
emailAddress: string | null
avatarUrl: string | null
accountType: string | null
timeZone: string | null
} | null
authorName: string
updateAuthor?: {
accountId: string
displayName: string
active?: boolean
emailAddress?: string
avatarUrl?: string
accountType?: string
timeZone?: string
active: boolean | null
emailAddress: string | null
avatarUrl: string | null
accountType: string | null
timeZone: string | null
} | null
created: string
updated: string
@@ -894,21 +894,21 @@ export interface JiraRetrieveResponse extends ToolResponse {
author: {
accountId: string
displayName: string
active?: boolean
emailAddress?: string
avatarUrl?: string
accountType?: string
timeZone?: string
active: boolean | null
emailAddress: string | null
avatarUrl: string | null
accountType: string | null
timeZone: string | null
} | null
authorName: string
updateAuthor?: {
accountId: string
displayName: string
active?: boolean
emailAddress?: string
avatarUrl?: string
accountType?: string
timeZone?: string
active: boolean | null
emailAddress: string | null
avatarUrl: string | null
accountType: string | null
timeZone: string | null
} | null
comment?: string | null
started: string
@@ -927,11 +927,11 @@ export interface JiraRetrieveResponse extends ToolResponse {
author: {
accountId: string
displayName: string
active?: boolean
emailAddress?: string
avatarUrl?: string
accountType?: string
timeZone?: string
active: boolean | null
emailAddress: string | null
avatarUrl: string | null
accountType: string | null
timeZone: string | null
} | null
authorName: string
created: string
@@ -1149,21 +1149,21 @@ export interface JiraSearchIssuesResponse extends ToolResponse {
assignee: {
accountId: string
displayName: string
active?: boolean
emailAddress?: string
avatarUrl?: string
accountType?: string
timeZone?: string
active: boolean | null
emailAddress: string | null
avatarUrl: string | null
accountType: string | null
timeZone: string | null
} | null
assigneeName: string | null
reporter: {
accountId: string
displayName: string
active?: boolean
emailAddress?: string
avatarUrl?: string
accountType?: string
timeZone?: string
active: boolean | null
emailAddress: string | null
avatarUrl: string | null
accountType: string | null
timeZone: string | null
} | null
labels: string[]
components: Array<{ id: string; name: string; description?: string }>
@@ -1223,21 +1223,21 @@ export interface JiraGetCommentsResponse extends ToolResponse {
author: {
accountId: string
displayName: string
active?: boolean
emailAddress?: string
avatarUrl?: string
accountType?: string
timeZone?: string
active: boolean | null
emailAddress: string | null
avatarUrl: string | null
accountType: string | null
timeZone: string | null
} | null
authorName: string
updateAuthor: {
accountId: string
displayName: string
active?: boolean
emailAddress?: string
avatarUrl?: string
accountType?: string
timeZone?: string
active: boolean | null
emailAddress: string | null
avatarUrl: string | null
accountType: string | null
timeZone: string | null
} | null
created: string
updated: string
@@ -1431,20 +1431,20 @@ export interface JiraUpdateWorklogResponse extends ToolResponse {
author: {
accountId: string
displayName: string
active?: boolean
emailAddress?: string
avatarUrl?: string
accountType?: string
timeZone?: string
active: boolean | null
emailAddress: string | null
avatarUrl: string | null
accountType: string | null
timeZone: string | null
} | null
updateAuthor: {
accountId: string
displayName: string
active?: boolean
emailAddress?: string
avatarUrl?: string
accountType?: string
timeZone?: string
active: boolean | null
emailAddress: string | null
avatarUrl: string | null
accountType: string | null
timeZone: string | null
} | null
started: string | null
created: string | null
+8 -1
View File
@@ -157,7 +157,14 @@ export const jiraUpdateTool: ToolConfig<JiraUpdateParams, JiraUpdateResponse> =
}
}
const data = JSON.parse(responseText)
let data: any
try {
data = JSON.parse(responseText)
} catch {
throw new Error(
`Jira update failed (${response.status} ${response.statusText}): non-JSON response from /api/tools/jira/update`
)
}
if (data.success && data.output) {
return data
+5 -27
View File
@@ -1,6 +1,6 @@
import type { JiraUpdateCommentParams, JiraUpdateCommentResponse } from '@/tools/jira/types'
import { SUCCESS_OUTPUT, TIMESTAMP_OUTPUT, USER_OUTPUT_PROPERTIES } from '@/tools/jira/types'
import { extractAdfText, getJiraCloudId, transformUser } from '@/tools/jira/utils'
import { extractAdfText, getJiraCloudId, toAdf, transformUser } from '@/tools/jira/utils'
import type { ToolConfig } from '@/tools/types'
/**
@@ -81,7 +81,7 @@ export const jiraUpdateCommentTool: ToolConfig<JiraUpdateCommentParams, JiraUpda
request: {
url: (params: JiraUpdateCommentParams) => {
if (params.cloudId) {
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/comment/${params.commentId}`
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}/comment/${params.commentId?.trim() ?? ''}`
}
return 'https://api.atlassian.com/oauth/token/accessible-resources'
},
@@ -95,40 +95,18 @@ export const jiraUpdateCommentTool: ToolConfig<JiraUpdateCommentParams, JiraUpda
},
body: (params: JiraUpdateCommentParams) => {
if (!params.cloudId) return undefined as any
const payload: Record<string, any> = {
body: {
type: 'doc',
version: 1,
content: [
{
type: 'paragraph',
content: [{ type: 'text', text: params.body }],
},
],
},
}
const payload: Record<string, any> = { body: toAdf(params.body ?? '') }
if (params.visibility) payload.visibility = params.visibility
return payload
},
},
transformResponse: async (response: Response, params?: JiraUpdateCommentParams) => {
const payload: Record<string, any> = {
body: {
type: 'doc',
version: 1,
content: [
{
type: 'paragraph',
content: [{ type: 'text', text: params?.body ?? '' }],
},
],
},
}
const payload: Record<string, any> = { body: toAdf(params?.body ?? '') }
if (params?.visibility) payload.visibility = params.visibility
const makeRequest = async (cloudId: string) => {
const commentUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey}/comment/${params!.commentId}`
const commentUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey?.trim() ?? ''}/comment/${params!.commentId?.trim() ?? ''}`
const commentResponse = await fetch(commentUrl, {
method: 'PUT',
headers: {
+24 -22
View File
@@ -1,29 +1,31 @@
import type { JiraUpdateWorklogParams, JiraUpdateWorklogResponse } from '@/tools/jira/types'
import { SUCCESS_OUTPUT, TIMESTAMP_OUTPUT, USER_OUTPUT_PROPERTIES } from '@/tools/jira/types'
import { extractAdfText, getJiraCloudId, transformUser } from '@/tools/jira/utils'
import {
extractAdfText,
getJiraCloudId,
normalizeJiraWorklogTimestamp,
toAdf,
transformUser,
} from '@/tools/jira/utils'
import type { ToolConfig } from '@/tools/types'
function buildWorklogBody(params: JiraUpdateWorklogParams) {
let timeSpentSeconds: number | undefined
if (
params.timeSpentSeconds !== undefined &&
params.timeSpentSeconds !== null &&
String(params.timeSpentSeconds).trim() !== ''
) {
const n = Number(params.timeSpentSeconds)
if (!Number.isFinite(n) || n <= 0) {
throw new Error('timeSpentSeconds must be a positive finite number')
}
timeSpentSeconds = n
}
const body: Record<string, any> = {
timeSpentSeconds: params.timeSpentSeconds ? Number(params.timeSpentSeconds) : undefined,
comment: params.comment
? {
type: 'doc',
version: 1,
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: params.comment,
},
],
},
],
}
: undefined,
started: params.started ? params.started.replace(/Z$/, '+0000') : undefined,
timeSpentSeconds,
comment: params.comment ? toAdf(params.comment) : undefined,
started: params.started ? normalizeJiraWorklogTimestamp(params.started) : undefined,
}
if (params.visibility) body.visibility = params.visibility
return body
@@ -120,7 +122,7 @@ export const jiraUpdateWorklogTool: ToolConfig<JiraUpdateWorklogParams, JiraUpda
request: {
url: (params: JiraUpdateWorklogParams) => {
if (params.cloudId) {
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/worklog/${params.worklogId}`
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey?.trim() ?? ''}/worklog/${params.worklogId?.trim() ?? ''}`
}
return 'https://api.atlassian.com/oauth/token/accessible-resources'
},
@@ -141,7 +143,7 @@ export const jiraUpdateWorklogTool: ToolConfig<JiraUpdateWorklogParams, JiraUpda
transformResponse: async (response: Response, params?: JiraUpdateWorklogParams) => {
if (!params?.cloudId) {
const cloudId = await getJiraCloudId(params!.domain, params!.accessToken)
const worklogUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey}/worklog/${params!.worklogId}`
const worklogUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey?.trim() ?? ''}/worklog/${params!.worklogId?.trim() ?? ''}`
const worklogResponse = await fetch(worklogUrl, {
method: 'PUT',
headers: {
+20 -6
View File
@@ -72,11 +72,11 @@ export function extractAdfText(content: any): string | null {
export function transformUser(user: any): {
accountId: string
displayName: string
active?: boolean
emailAddress?: string
avatarUrl?: string
accountType?: string
timeZone?: string
active: boolean | null
emailAddress: string | null
avatarUrl: string | null
accountType: string | null
timeZone: string | null
} | null {
if (!user) return null
return {
@@ -142,7 +142,21 @@ export async function downloadJiraAttachments(
return downloaded
}
function normalizeDomain(domain: string): string {
/**
* Normalizes an ISO timestamp into the format Jira's worklog API requires:
* `YYYY-MM-DDTHH:mm:ss.sss±HHMM` (offset without colon). Accepts trailing `Z`
* and `±HH:MM` offsets and rewrites them to `±HHMM`. If milliseconds are
* missing, `.000` is inserted before the offset.
*/
export function normalizeJiraWorklogTimestamp(value: string): string {
let s = value.trim()
s = s.replace(/Z$/i, '+0000')
s = s.replace(/([+-]\d{2}):(\d{2})$/, '$1$2')
s = s.replace(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})([+-]\d{4})$/, '$1.000$2')
return s
}
export function normalizeDomain(domain: string): string {
return `https://${domain
.trim()
.replace(/^https?:\/\//i, '')
+8 -1
View File
@@ -175,7 +175,14 @@ export const jiraWriteTool: ToolConfig<JiraWriteParams, JiraWriteResponse> = {
}
}
const data = JSON.parse(responseText)
let data: any
try {
data = JSON.parse(responseText)
} catch {
throw new Error(
`Jira write failed (${response.status} ${response.statusText}): non-JSON response from /api/tools/jira/write`
)
}
if (data.success && data.output) {
return {
+1 -8
View File
@@ -39,16 +39,10 @@ export const jsmAddCustomerTool: ToolConfig<JsmAddCustomerParams, JsmAddCustomer
},
accountIds: {
type: 'string',
required: false,
required: true,
visibility: 'user-or-llm',
description: 'Comma-separated Atlassian account IDs to add as customers',
},
emails: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated email addresses to add as customers',
},
},
request: {
@@ -63,7 +57,6 @@ export const jsmAddCustomerTool: ToolConfig<JsmAddCustomerParams, JsmAddCustomer
cloudId: params.cloudId,
serviceDeskId: params.serviceDeskId,
accountIds: params.accountIds,
emails: params.emails,
}),
},
+9 -3
View File
@@ -486,8 +486,7 @@ export interface JsmGetCustomersParams extends JsmBaseParams {
export interface JsmAddCustomerParams extends JsmBaseParams {
serviceDeskId: string
accountIds?: string
emails?: string
accountIds: string
}
export interface JsmGetOrganizationsParams extends JsmBaseParams {
@@ -1015,12 +1014,19 @@ export interface JsmGetFormAnswersParams extends JsmBaseParams {
formId: string
}
export interface JsmFormSimplifiedAnswer {
label?: string
answer?: string
fieldKey?: string
choice?: string
}
export interface JsmGetFormAnswersResponse extends ToolResponse {
output: {
ts: string
issueIdOrKey: string
formId: string
answers: Record<string, unknown> | null
answers: JsmFormSimplifiedAnswer[] | null
}
}
+3 -3
View File
@@ -60,9 +60,9 @@ export const slackAddReactionTool: ToolConfig<SlackAddReactionParams, SlackAddRe
}),
body: (params: SlackAddReactionParams) => ({
accessToken: params.accessToken || params.botToken,
channel: params.channel,
timestamp: params.timestamp,
name: params.name,
channel: params.channel?.trim(),
timestamp: params.timestamp?.trim(),
name: params.name?.trim(),
}),
},
+1 -5
View File
@@ -95,8 +95,6 @@ export const slackCanvasTool: ToolConfig<SlackCanvasParams, SlackCanvasResponse>
success: false,
output: {
canvas_id: '',
channel: '',
title: '',
},
error: data.error || 'Unknown error',
}
@@ -105,9 +103,7 @@ export const slackCanvasTool: ToolConfig<SlackCanvasParams, SlackCanvasResponse>
return {
success: true,
output: {
canvas_id: data.canvas_id || data.id,
channel: data.channel || '',
title: data.title || '',
canvas_id: data.canvas_id ?? data.id ?? '',
},
}
},
+2 -2
View File
@@ -57,8 +57,8 @@ export const slackDeleteMessageTool: ToolConfig<
}),
body: (params: SlackDeleteMessageParams) => ({
accessToken: params.accessToken || params.botToken,
channel: params.channel,
timestamp: params.timestamp,
channel: params.channel?.trim(),
timestamp: params.timestamp?.trim(),
}),
},
+1 -1
View File
@@ -6,7 +6,7 @@ export const slackDownloadTool: ToolConfig<SlackDownloadParams, SlackDownloadRes
id: 'slack_download',
name: 'Download File from Slack',
description: 'Download a file from Slack',
version: '1.0',
version: '1.0.0',
oauth: {
required: true,
+2 -2
View File
@@ -81,10 +81,10 @@ export const slackEphemeralMessageTool: ToolConfig<
}),
body: (params: SlackEphemeralMessageParams) => ({
accessToken: params.accessToken || params.botToken,
channel: params.channel,
channel: params.channel?.trim(),
user: params.user?.trim(),
text: params.text,
thread_ts: params.threadTs || undefined,
thread_ts: params.threadTs?.trim() || undefined,
blocks:
typeof params.blocks === 'string' ? JSON.parse(params.blocks) : params.blocks || undefined,
}),
+31 -27
View File
@@ -43,7 +43,7 @@ export const slackGetUserTool: ToolConfig<SlackGetUserParams, SlackGetUserRespon
request: {
url: (params: SlackGetUserParams) => {
const url = new URL('https://slack.com/api/users.info')
url.searchParams.append('user', params.userId)
url.searchParams.append('user', params.userId.trim())
return url.toString()
},
method: 'GET',
@@ -79,34 +79,38 @@ export const slackGetUserTool: ToolConfig<SlackGetUserParams, SlackGetUserRespon
output: {
user: {
id: user.id,
team_id: user.team_id ?? null,
name: user.name,
real_name: user.real_name || profile.real_name || '',
display_name: profile.display_name || '',
first_name: profile.first_name || '',
last_name: profile.last_name || '',
title: profile.title || '',
email: profile.email || '',
phone: profile.phone || '',
skype: profile.skype || '',
is_bot: user.is_bot || false,
is_admin: user.is_admin || false,
is_owner: user.is_owner || false,
is_primary_owner: user.is_primary_owner || false,
is_restricted: user.is_restricted || false,
is_ultra_restricted: user.is_ultra_restricted || false,
deleted: user.deleted || false,
timezone: user.tz,
timezone_label: user.tz_label,
timezone_offset: user.tz_offset,
avatar_24: profile.image_24,
avatar_48: profile.image_48,
avatar_72: profile.image_72,
avatar_192: profile.image_192,
avatar_512: profile.image_512,
status_text: profile.status_text || '',
status_emoji: profile.status_emoji || '',
status_expiration: profile.status_expiration,
updated: user.updated,
display_name: profile.display_name ?? '',
first_name: profile.first_name ?? '',
last_name: profile.last_name ?? '',
title: profile.title ?? '',
email: profile.email ?? '',
phone: profile.phone ?? '',
skype: profile.skype ?? '',
is_bot: user.is_bot ?? false,
is_admin: user.is_admin ?? false,
is_owner: user.is_owner ?? false,
is_primary_owner: user.is_primary_owner ?? false,
is_restricted: user.is_restricted ?? false,
is_ultra_restricted: user.is_ultra_restricted ?? false,
is_app_user: user.is_app_user ?? false,
deleted: user.deleted ?? false,
color: user.color ?? null,
timezone: user.tz ?? null,
timezone_label: user.tz_label ?? null,
timezone_offset: user.tz_offset ?? null,
avatar_24: profile.image_24 ?? null,
avatar_48: profile.image_48 ?? null,
avatar_72: profile.image_72 ?? null,
avatar_192: profile.image_192 ?? null,
avatar_512: profile.image_512 ?? null,
status_text: profile.status_text ?? '',
status_emoji: profile.status_emoji ?? '',
status_expiration: profile.status_expiration ?? null,
updated: user.updated ?? null,
has_2fa: user.has_2fa ?? false,
},
},
}
+17
View File
@@ -52,6 +52,12 @@ export const slackListChannelsTool: ToolConfig<SlackListChannelsParams, SlackLis
visibility: 'user-or-llm',
description: 'Maximum number of channels to return (default: 100, max: 200)',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination cursor from a previous response.next_cursor',
},
},
request: {
@@ -74,6 +80,11 @@ export const slackListChannelsTool: ToolConfig<SlackListChannelsParams, SlackLis
const limit = params.limit ? Math.min(Number(params.limit), 200) : 100
url.searchParams.append('limit', String(limit))
const cursor = params.cursor?.trim()
if (cursor) {
url.searchParams.append('cursor', cursor)
}
return url.toString()
},
method: 'GET',
@@ -121,6 +132,7 @@ export const slackListChannelsTool: ToolConfig<SlackListChannelsParams, SlackLis
ids,
names,
count: channels.length,
nextCursor: data.response_metadata?.next_cursor || null,
},
}
},
@@ -148,5 +160,10 @@ export const slackListChannelsTool: ToolConfig<SlackListChannelsParams, SlackLis
type: 'number',
description: 'Total number of channels returned',
},
nextCursor: {
type: 'string',
description: 'Cursor for the next page; null if no more pages',
optional: true,
},
},
}
+18 -1
View File
@@ -44,17 +44,28 @@ export const slackListMembersTool: ToolConfig<SlackListMembersParams, SlackListM
visibility: 'user-or-llm',
description: 'Maximum number of members to return (default: 100, max: 200)',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination cursor from a previous response.next_cursor',
},
},
request: {
url: (params: SlackListMembersParams) => {
const url = new URL('https://slack.com/api/conversations.members')
url.searchParams.append('channel', params.channel)
url.searchParams.append('channel', params.channel.trim())
// Set limit (default 100, max 200)
const limit = params.limit ? Math.min(Number(params.limit), 200) : 100
url.searchParams.append('limit', String(limit))
const cursor = params.cursor?.trim()
if (cursor) {
url.searchParams.append('cursor', cursor)
}
return url.toString()
},
method: 'GET',
@@ -89,6 +100,7 @@ export const slackListMembersTool: ToolConfig<SlackListMembersParams, SlackListM
output: {
members,
count: members.length,
nextCursor: data.response_metadata?.next_cursor || null,
},
}
},
@@ -105,5 +117,10 @@ export const slackListMembersTool: ToolConfig<SlackListMembersParams, SlackListM
type: 'number',
description: 'Total number of members returned',
},
nextCursor: {
type: 'string',
description: 'Cursor for the next page; null if no more pages',
optional: true,
},
},
}
+17
View File
@@ -44,6 +44,12 @@ export const slackListUsersTool: ToolConfig<SlackListUsersParams, SlackListUsers
visibility: 'user-or-llm',
description: 'Maximum number of users to return (default: 100, max: 200)',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination cursor from a previous response.next_cursor',
},
},
request: {
@@ -54,6 +60,11 @@ export const slackListUsersTool: ToolConfig<SlackListUsersParams, SlackListUsers
const limit = params.limit ? Math.min(Number(params.limit), 200) : 100
url.searchParams.append('limit', String(limit))
const cursor = params.cursor?.trim()
if (cursor) {
url.searchParams.append('cursor', cursor)
}
return url.toString()
},
method: 'GET',
@@ -114,6 +125,7 @@ export const slackListUsersTool: ToolConfig<SlackListUsersParams, SlackListUsers
ids,
names,
count: users.length,
nextCursor: data.response_metadata?.next_cursor || null,
},
}
},
@@ -141,5 +153,10 @@ export const slackListUsersTool: ToolConfig<SlackListUsersParams, SlackListUsers
type: 'number',
description: 'Total number of users returned',
},
nextCursor: {
type: 'string',
description: 'Cursor for the next page; null if no more pages',
optional: true,
},
},
}
+3 -3
View File
@@ -88,10 +88,10 @@ export const slackMessageTool: ToolConfig<SlackMessageParams, SlackMessageRespon
const isDM = params.destinationType === 'dm'
return {
accessToken: params.accessToken || params.botToken,
channel: isDM ? undefined : params.channel,
userId: isDM ? params.dmUserId : params.userId,
channel: isDM ? undefined : params.channel?.trim(),
userId: isDM ? params.dmUserId?.trim() : undefined,
text: params.text,
thread_ts: params.threadTs || undefined,
thread_ts: params.threadTs?.trim() || undefined,
blocks:
typeof params.blocks === 'string'
? JSON.parse(params.blocks)

Some files were not shown because too many files have changed in this diff Show More