mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
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:
co-authored by
Claude Opus 4.7
parent
36742740aa
commit
0c69302369
@@ -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: <b>, <i>, <u>, <a>, <ul>, <ol>, <li>, <code>, <pre> |
|
||||
| `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 > \'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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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 !== '') {
|
||||
|
||||
@@ -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
@@ -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' },
|
||||
|
||||
@@ -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)' },
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -525,7 +525,6 @@ export interface ConfluenceUpdateParams {
|
||||
pageId: string
|
||||
title?: string
|
||||
content?: string
|
||||
version?: number
|
||||
cloudId?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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}` },
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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) } }],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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, '')
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
},
|
||||
|
||||
|
||||
@@ -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 ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
},
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user