@striderlabs/mcp-thumbtack 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,180 @@
1
+ import { getBrowserManager } from "../browser.js";
2
+ import { loadSession, saveSession } from "../session.js";
3
+ export async function getProjects(params) {
4
+ const manager = getBrowserManager();
5
+ await manager.init();
6
+ const page = await manager.getPage();
7
+ const context = manager.getContext();
8
+ const sessionLoaded = await loadSession(page, context);
9
+ // Navigate to projects page
10
+ await page.goto("https://www.thumbtack.com/projects", {
11
+ waitUntil: "domcontentloaded",
12
+ timeout: 30000,
13
+ });
14
+ await page.waitForTimeout(3000);
15
+ // Check if redirected to login
16
+ const currentUrl = page.url();
17
+ const requiresLogin = currentUrl.includes("/login") ||
18
+ currentUrl.includes("/signup") ||
19
+ !sessionLoaded;
20
+ if (requiresLogin) {
21
+ // Also check page content for auth wall
22
+ const hasAuthWall = await page.evaluate(() => {
23
+ return (!!document.querySelector('[class*="AuthWall"], [class*="LoginWall"]') ||
24
+ window.location.href.includes("/login"));
25
+ });
26
+ if (hasAuthWall) {
27
+ return {
28
+ projects: [],
29
+ totalCount: 0,
30
+ requiresLogin: true,
31
+ message: "Authentication required. Please log in to Thumbtack first. Visit https://www.thumbtack.com/login to sign in.",
32
+ };
33
+ }
34
+ }
35
+ // Apply status filter if provided
36
+ if (params.status && params.status !== "all") {
37
+ try {
38
+ const filterMap = {
39
+ active: ["Active", "In Progress", "active"],
40
+ completed: ["Completed", "Done", "completed"],
41
+ };
42
+ const filterTexts = filterMap[params.status] || [];
43
+ for (const filterText of filterTexts) {
44
+ try {
45
+ const btn = await page.$(`button:has-text("${filterText}"), [role="tab"]:has-text("${filterText}")`);
46
+ if (btn) {
47
+ await btn.click();
48
+ await page.waitForTimeout(1500);
49
+ break;
50
+ }
51
+ }
52
+ catch {
53
+ continue;
54
+ }
55
+ }
56
+ }
57
+ catch {
58
+ // Filter may not be available
59
+ }
60
+ }
61
+ // Extract projects from the page
62
+ const projects = await page.evaluate(() => {
63
+ const results = [];
64
+ // Look for project cards
65
+ const cardSelectors = [
66
+ '[data-test="project-card"]',
67
+ '[class*="ProjectCard"]',
68
+ '[class*="ProjectItem"]',
69
+ '[class*="project-card"]',
70
+ "article[class*='Project']",
71
+ ];
72
+ let cards = [];
73
+ for (const selector of cardSelectors) {
74
+ const found = document.querySelectorAll(selector);
75
+ if (found.length > 0) {
76
+ cards = found;
77
+ break;
78
+ }
79
+ }
80
+ cards.forEach((card) => {
81
+ try {
82
+ // Extract project title
83
+ const titleEl = card.querySelector('[data-test="project-title"], [class*="ProjectTitle"], h3, h2');
84
+ const title = titleEl?.textContent?.trim() || "Untitled Project";
85
+ // Extract provider name
86
+ const providerNameEl = card.querySelector('[data-test="pro-name"], [class*="ProviderName"]');
87
+ const providerName = providerNameEl?.textContent?.trim() || "Unknown Provider";
88
+ // Extract provider URL
89
+ const linkEl = card.querySelector('a[href*="/ca/"], a[href*="/pro/"]');
90
+ const providerUrl = linkEl?.href || null;
91
+ // Extract project URL
92
+ const projectLinkEl = card.querySelector('a[href*="/projects/"]');
93
+ const projectUrl = projectLinkEl?.href || null;
94
+ // Extract project ID
95
+ const idFromData = card.getAttribute("data-project-id") || card.getAttribute("data-id");
96
+ const idFromUrl = projectUrl?.match(/\/projects\/(\w+)/)?.[1];
97
+ const id = idFromData || idFromUrl || Math.random().toString(36).substr(2, 9);
98
+ // Extract service type
99
+ const serviceTypeEl = card.querySelector('[data-test="service-type"], [class*="ServiceType"], [class*="Category"]');
100
+ const serviceType = serviceTypeEl?.textContent?.trim() || "Service";
101
+ // Extract status
102
+ const statusEl = card.querySelector('[data-test="status"], [class*="Status"], [class*="badge"]');
103
+ const status = statusEl?.textContent?.trim() || "active";
104
+ // Extract dates
105
+ const startDateEl = card.querySelector('[data-test="start-date"], [class*="StartDate"], time');
106
+ const startDate = startDateEl?.getAttribute("datetime") || startDateEl?.textContent?.trim() || null;
107
+ const completedDateEl = card.querySelector('[data-test="completed-date"], [class*="CompletedDate"]');
108
+ const completedDate = completedDateEl?.getAttribute("datetime") || completedDateEl?.textContent?.trim() || null;
109
+ // Extract price
110
+ const priceEl = card.querySelector('[data-test="price"], [class*="Price"], [class*="Amount"]');
111
+ const price = priceEl?.textContent?.trim() || null;
112
+ // Extract location
113
+ const locationEl = card.querySelector('[data-test="location"], [class*="Location"]');
114
+ const location = locationEl?.textContent?.trim() || null;
115
+ // Extract description
116
+ const descEl = card.querySelector('[data-test="description"], [class*="Description"], p');
117
+ const description = descEl?.textContent?.trim() || null;
118
+ // Check if has review
119
+ const reviewEl = card.querySelector('[data-test="review-badge"], [class*="ReviewBadge"], [class*="Reviewed"]');
120
+ const hasReview = !!reviewEl;
121
+ // Extract rating if reviewed
122
+ const ratingEl = card.querySelector('[aria-label*="star"], [class*="Rating"]');
123
+ const ratingText = ratingEl?.getAttribute("aria-label") || ratingEl?.textContent || "";
124
+ const ratingMatch = ratingText.match(/(\d+\.?\d*)/);
125
+ const rating = ratingMatch ? parseFloat(ratingMatch[1]) : null;
126
+ // Provider image
127
+ const imgEl = card.querySelector("img");
128
+ const providerImageUrl = imgEl?.src || null;
129
+ results.push({
130
+ id,
131
+ title,
132
+ providerName,
133
+ providerUrl,
134
+ providerImageUrl,
135
+ serviceType,
136
+ status,
137
+ startDate,
138
+ completedDate,
139
+ price,
140
+ location,
141
+ description,
142
+ hasReview,
143
+ rating,
144
+ projectUrl,
145
+ });
146
+ }
147
+ catch {
148
+ // Skip malformed cards
149
+ }
150
+ });
151
+ return results;
152
+ });
153
+ // Apply status filter in JS if server-side filter didn't work
154
+ let filteredProjects = projects;
155
+ if (params.status && params.status !== "all") {
156
+ filteredProjects = projects.filter((p) => {
157
+ const statusLower = p.status.toLowerCase();
158
+ if (params.status === "active") {
159
+ return (statusLower.includes("active") ||
160
+ statusLower.includes("progress") ||
161
+ statusLower.includes("open"));
162
+ }
163
+ else if (params.status === "completed") {
164
+ return (statusLower.includes("complete") ||
165
+ statusLower.includes("done") ||
166
+ statusLower.includes("finished"));
167
+ }
168
+ return true;
169
+ });
170
+ }
171
+ await saveSession(page, context).catch(() => { });
172
+ return {
173
+ projects: filteredProjects,
174
+ totalCount: filteredProjects.length,
175
+ requiresLogin: false,
176
+ message: filteredProjects.length > 0
177
+ ? `Found ${filteredProjects.length} project(s).`
178
+ : "No projects found for the specified filter.",
179
+ };
180
+ }
@@ -0,0 +1,40 @@
1
+ export interface GetProviderParams {
2
+ provider_id?: string;
3
+ provider_url?: string;
4
+ }
5
+ export interface Review {
6
+ author: string;
7
+ rating: number;
8
+ date: string;
9
+ text: string;
10
+ serviceType: string | null;
11
+ }
12
+ export interface ProviderDetails {
13
+ id: string;
14
+ name: string;
15
+ url: string;
16
+ profileImageUrl: string | null;
17
+ coverImageUrl: string | null;
18
+ rating: number | null;
19
+ reviewCount: number | null;
20
+ description: string;
21
+ location: string | null;
22
+ yearsInBusiness: number | null;
23
+ employees: string | null;
24
+ verified: boolean;
25
+ backgroundChecked: boolean;
26
+ licenseInfo: string | null;
27
+ insuranceInfo: string | null;
28
+ services: string[];
29
+ specialties: string[];
30
+ hireCount: number | null;
31
+ responseTime: string | null;
32
+ priceRange: string | null;
33
+ reviews: Review[];
34
+ contact: {
35
+ phone: string | null;
36
+ website: string | null;
37
+ };
38
+ socialLinks: Record<string, string>;
39
+ }
40
+ export declare function getProvider(params: GetProviderParams): Promise<ProviderDetails>;
@@ -0,0 +1,186 @@
1
+ import { getBrowserManager } from "../browser.js";
2
+ import { loadSession, saveSession } from "../session.js";
3
+ export async function getProvider(params) {
4
+ if (!params.provider_id && !params.provider_url) {
5
+ throw new Error("Either provider_id or provider_url must be provided");
6
+ }
7
+ const manager = getBrowserManager();
8
+ await manager.init();
9
+ const page = await manager.getPage();
10
+ const context = manager.getContext();
11
+ await loadSession(page, context);
12
+ // Build provider URL
13
+ let providerUrl = params.provider_url;
14
+ if (!providerUrl && params.provider_id) {
15
+ providerUrl = `https://www.thumbtack.com/pro/${params.provider_id}`;
16
+ }
17
+ await page.goto(providerUrl, {
18
+ waitUntil: "domcontentloaded",
19
+ timeout: 30000,
20
+ });
21
+ await page.waitForTimeout(3000);
22
+ // Extract provider details from the page
23
+ const details = await page.evaluate(() => {
24
+ // Helper to safely query text content
25
+ const getText = (selector) => {
26
+ const el = document.querySelector(selector);
27
+ return el?.textContent?.trim() || "";
28
+ };
29
+ const getAttr = (selector, attr) => {
30
+ const el = document.querySelector(selector);
31
+ return el?.getAttribute(attr) || null;
32
+ };
33
+ // Name
34
+ const name = getText("h1") ||
35
+ getText('[data-test="pro-name"]') ||
36
+ getText('[class*="BusinessName"]') ||
37
+ "";
38
+ // Profile image
39
+ const profileImageUrl = getAttr('img[data-test="profile-image"]', "src") ||
40
+ getAttr('[class*="ProfileImage"] img', "src") ||
41
+ getAttr(".profile-image img", "src") ||
42
+ null;
43
+ // Cover/header image
44
+ const coverImageUrl = getAttr('[class*="CoverImage"] img', "src") ||
45
+ getAttr('[class*="hero"] img', "src") ||
46
+ null;
47
+ // Rating
48
+ const ratingEl = document.querySelector('[aria-label*="star"], [data-test="rating-score"], [class*="RatingScore"]');
49
+ const ratingText = ratingEl?.getAttribute("aria-label") || ratingEl?.textContent || "";
50
+ const ratingMatch = ratingText.match(/(\d+\.?\d*)/);
51
+ const rating = ratingMatch ? parseFloat(ratingMatch[1]) : null;
52
+ // Review count
53
+ const reviewCountEl = document.querySelector('[data-test="review-count"], [class*="ReviewCount"]');
54
+ const reviewCountText = reviewCountEl?.textContent || "";
55
+ const reviewCountMatch = reviewCountText.match(/(\d+)/);
56
+ const reviewCount = reviewCountMatch ? parseInt(reviewCountMatch[1]) : null;
57
+ // Description/intro
58
+ const description = getText('[data-test="intro-text"]') ||
59
+ getText('[class*="Introduction"]') ||
60
+ getText('[class*="Bio"]') ||
61
+ getText(".introduction") ||
62
+ "";
63
+ // Location
64
+ const location = getText('[data-test="location"]') ||
65
+ getText('[class*="Location"]') ||
66
+ getText('[class*="ServiceArea"]') ||
67
+ null;
68
+ // Years in business
69
+ const yearsEl = document.querySelector('[data-test="years-in-business"], [class*="YearsInBusiness"]');
70
+ const yearsText = yearsEl?.textContent || "";
71
+ const yearsMatch = yearsText.match(/(\d+)/);
72
+ const yearsInBusiness = yearsMatch ? parseInt(yearsMatch[1]) : null;
73
+ // Employees
74
+ const employees = getText('[data-test="employees"]') ||
75
+ getText('[class*="Employees"]') ||
76
+ null;
77
+ // Verification badges
78
+ const verified = !!document.querySelector('[data-test="verified-badge"], [aria-label*="verified"], [class*="Verified"]');
79
+ const backgroundChecked = !!document.querySelector('[data-test="background-check"], [aria-label*="background"], [class*="BackgroundCheck"]');
80
+ // License info
81
+ const licenseInfo = getText('[data-test="license"]') ||
82
+ getText('[class*="License"]') ||
83
+ null;
84
+ // Insurance info
85
+ const insuranceInfo = getText('[data-test="insurance"]') ||
86
+ getText('[class*="Insurance"]') ||
87
+ null;
88
+ // Services offered
89
+ const serviceEls = document.querySelectorAll('[data-test="service-item"], [class*="ServiceItem"], [class*="service-tag"]');
90
+ const services = [];
91
+ serviceEls.forEach((el) => {
92
+ const text = el.textContent?.trim();
93
+ if (text)
94
+ services.push(text);
95
+ });
96
+ // Specialties
97
+ const specialtyEls = document.querySelectorAll('[data-test="specialty"], [class*="Specialty"]');
98
+ const specialties = [];
99
+ specialtyEls.forEach((el) => {
100
+ const text = el.textContent?.trim();
101
+ if (text)
102
+ specialties.push(text);
103
+ });
104
+ // Hire count
105
+ const hireEl = document.querySelector('[data-test="hire-count"], [class*="HireCount"]');
106
+ const hireText = hireEl?.textContent || "";
107
+ const hireMatch = hireText.match(/(\d+)/);
108
+ const hireCount = hireMatch ? parseInt(hireMatch[1]) : null;
109
+ // Response time
110
+ const responseTime = getText('[data-test="response-time"]') ||
111
+ getText('[class*="ResponseTime"]') ||
112
+ null;
113
+ // Price range
114
+ const priceRange = getText('[data-test="price-range"]') ||
115
+ getText('[class*="PriceRange"]') ||
116
+ null;
117
+ // Extract reviews
118
+ const reviews = [];
119
+ const reviewEls = document.querySelectorAll('[data-test="review"], [class*="ReviewItem"], [class*="review-card"]');
120
+ reviewEls.forEach((reviewEl) => {
121
+ const author = reviewEl.querySelector('[class*="author"], [data-test="reviewer-name"]')?.textContent?.trim() || "Anonymous";
122
+ const reviewRatingEl = reviewEl.querySelector('[aria-label*="star"], [class*="rating"]');
123
+ const reviewRatingText = reviewRatingEl?.getAttribute("aria-label") || reviewRatingEl?.textContent || "";
124
+ const reviewRatingMatch = reviewRatingText.match(/(\d+\.?\d*)/);
125
+ const reviewRating = reviewRatingMatch ? parseFloat(reviewRatingMatch[1]) : 5;
126
+ const date = reviewEl.querySelector('[class*="date"], time')?.textContent?.trim() || "";
127
+ const text = reviewEl.querySelector('[class*="body"], [data-test="review-text"], p')?.textContent?.trim() || "";
128
+ const serviceType = reviewEl.querySelector('[class*="service"], [data-test="service-type"]')?.textContent?.trim() || null;
129
+ if (author || text) {
130
+ reviews.push({ author, rating: reviewRating, date, text, serviceType });
131
+ }
132
+ });
133
+ // Contact info
134
+ const phoneEl = document.querySelector('a[href^="tel:"], [data-test="phone-number"]');
135
+ const phone = phoneEl?.href?.replace("tel:", "") || phoneEl?.textContent?.trim() || null;
136
+ const websiteEl = document.querySelector('[data-test="website-link"], a[data-test="external-link"]');
137
+ const website = websiteEl?.href || null;
138
+ // Social links
139
+ const socialLinks = {};
140
+ const socialSelectors = {
141
+ facebook: 'a[href*="facebook.com"]',
142
+ instagram: 'a[href*="instagram.com"]',
143
+ twitter: 'a[href*="twitter.com"]',
144
+ linkedin: 'a[href*="linkedin.com"]',
145
+ yelp: 'a[href*="yelp.com"]',
146
+ };
147
+ for (const [platform, selector] of Object.entries(socialSelectors)) {
148
+ const el = document.querySelector(selector);
149
+ if (el?.href) {
150
+ socialLinks[platform] = el.href;
151
+ }
152
+ }
153
+ return {
154
+ name,
155
+ profileImageUrl,
156
+ coverImageUrl,
157
+ rating,
158
+ reviewCount,
159
+ description,
160
+ location: location || null,
161
+ yearsInBusiness,
162
+ employees: employees || null,
163
+ verified,
164
+ backgroundChecked,
165
+ licenseInfo: licenseInfo || null,
166
+ insuranceInfo: insuranceInfo || null,
167
+ services,
168
+ specialties,
169
+ hireCount,
170
+ responseTime: responseTime || null,
171
+ priceRange: priceRange || null,
172
+ reviews: reviews.slice(0, 10),
173
+ contact: { phone, website },
174
+ socialLinks,
175
+ };
176
+ });
177
+ // Extract ID from URL
178
+ const urlMatch = page.url().match(/\/(\d+)\/?(?:\?.*)?$/);
179
+ const id = urlMatch ? urlMatch[1] : params.provider_id || "";
180
+ await saveSession(page, context).catch(() => { });
181
+ return {
182
+ id,
183
+ url: page.url(),
184
+ ...details,
185
+ };
186
+ }
@@ -0,0 +1,13 @@
1
+ export interface HireProviderParams {
2
+ quote_id: string;
3
+ provider_id: string;
4
+ }
5
+ export interface HireProviderResult {
6
+ success: boolean;
7
+ message: string;
8
+ projectId: string | null;
9
+ providerName: string | null;
10
+ requiresLogin: boolean;
11
+ nextSteps: string[];
12
+ }
13
+ export declare function hireProvider(params: HireProviderParams): Promise<HireProviderResult>;
@@ -0,0 +1,195 @@
1
+ import { getBrowserManager } from "../browser.js";
2
+ import { loadSession, saveSession } from "../session.js";
3
+ export async function hireProvider(params) {
4
+ const manager = getBrowserManager();
5
+ await manager.init();
6
+ const page = await manager.getPage();
7
+ const context = manager.getContext();
8
+ const sessionLoaded = await loadSession(page, context);
9
+ if (!sessionLoaded) {
10
+ return {
11
+ success: false,
12
+ message: "Authentication required. Please log in to Thumbtack first.",
13
+ projectId: null,
14
+ providerName: null,
15
+ requiresLogin: true,
16
+ nextSteps: [
17
+ "Visit https://www.thumbtack.com/login to sign in",
18
+ "Once logged in, navigate to your inbox to find the quote",
19
+ "Click 'Hire' on the provider's quote",
20
+ ],
21
+ };
22
+ }
23
+ // Navigate to the inbox/quote thread
24
+ await page.goto("https://www.thumbtack.com/inbox", {
25
+ waitUntil: "domcontentloaded",
26
+ timeout: 30000,
27
+ });
28
+ await page.waitForTimeout(2000);
29
+ // Check if redirected to login
30
+ const currentUrl = page.url();
31
+ if (currentUrl.includes("/login") || currentUrl.includes("/signup")) {
32
+ return {
33
+ success: false,
34
+ message: "Authentication required. Your session may have expired.",
35
+ projectId: null,
36
+ providerName: null,
37
+ requiresLogin: true,
38
+ nextSteps: [
39
+ "Visit https://www.thumbtack.com/login to sign in again",
40
+ "Then retry hiring the provider",
41
+ ],
42
+ };
43
+ }
44
+ // Try to find and click on the specific quote/conversation
45
+ let providerName = null;
46
+ let foundQuote = false;
47
+ // Look for the quote with matching ID
48
+ try {
49
+ const quoteSelectors = [
50
+ `[data-quote-id="${params.quote_id}"]`,
51
+ `[data-id="${params.quote_id}"]`,
52
+ `a[href*="${params.quote_id}"]`,
53
+ `a[href*="${params.provider_id}"]`,
54
+ ];
55
+ for (const selector of quoteSelectors) {
56
+ try {
57
+ const el = await page.$(selector);
58
+ if (el) {
59
+ await el.click();
60
+ foundQuote = true;
61
+ await page.waitForTimeout(2000);
62
+ break;
63
+ }
64
+ }
65
+ catch {
66
+ continue;
67
+ }
68
+ }
69
+ if (!foundQuote) {
70
+ // Try navigating directly to the conversation
71
+ await page.goto(`https://www.thumbtack.com/inbox/${params.quote_id}`, { waitUntil: "domcontentloaded", timeout: 15000 });
72
+ await page.waitForTimeout(2000);
73
+ }
74
+ }
75
+ catch {
76
+ // Navigation may have worked partially
77
+ }
78
+ // Get provider name from current page context
79
+ providerName = await page.evaluate(() => {
80
+ return (document.querySelector('[data-test="pro-name"], h1, h2')?.textContent?.trim() || null);
81
+ });
82
+ // Find and click the "Hire" button
83
+ try {
84
+ const hireButtonSelectors = [
85
+ 'button[data-test="hire-button"]',
86
+ 'button:has-text("Hire")',
87
+ 'button:has-text("Hire this pro")',
88
+ 'button:has-text("Mark as hired")',
89
+ '[class*="HireButton"]',
90
+ 'a[data-test="hire-cta"]',
91
+ ];
92
+ let hireClicked = false;
93
+ for (const selector of hireButtonSelectors) {
94
+ try {
95
+ const btn = await page.$(selector);
96
+ if (btn) {
97
+ await btn.click();
98
+ hireClicked = true;
99
+ await page.waitForTimeout(2000);
100
+ break;
101
+ }
102
+ }
103
+ catch {
104
+ continue;
105
+ }
106
+ }
107
+ if (!hireClicked) {
108
+ return {
109
+ success: false,
110
+ message: "Could not find the 'Hire' button. The quote may have already been acted upon or the page structure has changed.",
111
+ projectId: null,
112
+ providerName,
113
+ requiresLogin: false,
114
+ nextSteps: [
115
+ "Visit https://www.thumbtack.com/inbox to find your quotes manually",
116
+ "Look for the provider's quote and click 'Hire' directly",
117
+ ],
118
+ };
119
+ }
120
+ // Handle confirmation dialog if it appears
121
+ await page.waitForTimeout(1000);
122
+ try {
123
+ const confirmBtn = await page.$('button[data-test="confirm-hire"], button:has-text("Confirm"), button:has-text("Yes, hire")');
124
+ if (confirmBtn) {
125
+ await confirmBtn.click();
126
+ await page.waitForTimeout(2000);
127
+ }
128
+ }
129
+ catch {
130
+ // No confirmation dialog
131
+ }
132
+ // Check for success state
133
+ const result = await page.evaluate(() => {
134
+ const successEl = document.querySelector('[data-test="hire-success"], [class*="HireSuccess"], [class*="HiredBadge"]');
135
+ const bodyText = document.body.textContent || "";
136
+ const isSuccess = !!successEl ||
137
+ bodyText.toLowerCase().includes("you hired") ||
138
+ bodyText.toLowerCase().includes("hired!") ||
139
+ bodyText.toLowerCase().includes("project created");
140
+ const projectIdEl = document.querySelector("[data-project-id]");
141
+ const projectId = projectIdEl?.getAttribute("data-project-id") || null;
142
+ // Extract next steps / success message
143
+ const nextStepsEls = document.querySelectorAll('[class*="NextStep"], [data-test="next-step"]');
144
+ const nextSteps = [];
145
+ nextStepsEls.forEach((el) => {
146
+ const text = el.textContent?.trim();
147
+ if (text)
148
+ nextSteps.push(text);
149
+ });
150
+ return { isSuccess, projectId, nextSteps };
151
+ });
152
+ await saveSession(page, context).catch(() => { });
153
+ if (result.isSuccess) {
154
+ return {
155
+ success: true,
156
+ message: `Successfully hired ${providerName || "the provider"}! A project has been created.`,
157
+ projectId: result.projectId,
158
+ providerName,
159
+ requiresLogin: false,
160
+ nextSteps: result.nextSteps.length > 0
161
+ ? result.nextSteps
162
+ : [
163
+ "Communicate with your provider through the Thumbtack inbox",
164
+ "Agree on project details, timeline, and payment",
165
+ "Leave a review after the project is complete",
166
+ ],
167
+ };
168
+ }
169
+ else {
170
+ return {
171
+ success: false,
172
+ message: "Hire action was attempted but confirmation could not be verified. Please check your Thumbtack account.",
173
+ projectId: null,
174
+ providerName,
175
+ requiresLogin: false,
176
+ nextSteps: [
177
+ "Visit https://www.thumbtack.com/projects to check if the project was created",
178
+ ],
179
+ };
180
+ }
181
+ }
182
+ catch (error) {
183
+ const err = error;
184
+ return {
185
+ success: false,
186
+ message: `Error during hire process: ${err.message}`,
187
+ projectId: null,
188
+ providerName,
189
+ requiresLogin: false,
190
+ nextSteps: [
191
+ "Try again or visit https://www.thumbtack.com/inbox to hire manually",
192
+ ],
193
+ };
194
+ }
195
+ }
@@ -0,0 +1,20 @@
1
+ export interface RequestQuoteParams {
2
+ provider_id: string;
3
+ service_type: string;
4
+ description: string;
5
+ location: string;
6
+ contact_info: {
7
+ name?: string;
8
+ email?: string;
9
+ phone?: string;
10
+ };
11
+ }
12
+ export interface QuoteRequestResult {
13
+ success: boolean;
14
+ message: string;
15
+ quoteId: string | null;
16
+ providerName: string | null;
17
+ estimatedResponseTime: string | null;
18
+ requiresLogin: boolean;
19
+ }
20
+ export declare function requestQuote(params: RequestQuoteParams): Promise<QuoteRequestResult>;