@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.
- package/dist/browser.d.ts +12 -0
- package/dist/browser.js +88 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +30 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +276 -0
- package/dist/session.d.ts +5 -0
- package/dist/session.js +99 -0
- package/dist/tools/get-projects.d.ts +27 -0
- package/dist/tools/get-projects.js +180 -0
- package/dist/tools/get-provider.d.ts +40 -0
- package/dist/tools/get-provider.js +186 -0
- package/dist/tools/hire-provider.d.ts +13 -0
- package/dist/tools/hire-provider.js +195 -0
- package/dist/tools/request-quote.d.ts +20 -0
- package/dist/tools/request-quote.js +234 -0
- package/dist/tools/search-services.d.ts +28 -0
- package/dist/tools/search-services.js +152 -0
- package/dist/tools/view-quotes.d.ts +25 -0
- package/dist/tools/view-quotes.js +163 -0
- package/package.json +30 -0
- package/server.json +14 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { BrowserContext, Page } from "playwright";
|
|
2
|
+
export declare class BrowserManager {
|
|
3
|
+
private browser;
|
|
4
|
+
private context;
|
|
5
|
+
private page;
|
|
6
|
+
init(): Promise<void>;
|
|
7
|
+
getPage(): Promise<Page>;
|
|
8
|
+
getContext(): BrowserContext | null;
|
|
9
|
+
close(): Promise<void>;
|
|
10
|
+
isLoggedIn(): Promise<boolean>;
|
|
11
|
+
}
|
|
12
|
+
export declare function getBrowserManager(): BrowserManager;
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { chromium } from "playwright";
|
|
2
|
+
const USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
3
|
+
export class BrowserManager {
|
|
4
|
+
browser = null;
|
|
5
|
+
context = null;
|
|
6
|
+
page = null;
|
|
7
|
+
async init() {
|
|
8
|
+
if (this.browser) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
this.browser = await chromium.launch({
|
|
12
|
+
headless: true,
|
|
13
|
+
args: [
|
|
14
|
+
"--no-sandbox",
|
|
15
|
+
"--disable-setuid-sandbox",
|
|
16
|
+
"--disable-blink-features=AutomationControlled",
|
|
17
|
+
"--disable-infobars",
|
|
18
|
+
"--window-size=1280,800",
|
|
19
|
+
],
|
|
20
|
+
});
|
|
21
|
+
this.context = await this.browser.newContext({
|
|
22
|
+
viewport: { width: 1280, height: 800 },
|
|
23
|
+
userAgent: USER_AGENT,
|
|
24
|
+
locale: "en-US",
|
|
25
|
+
timezoneId: "America/New_York",
|
|
26
|
+
extraHTTPHeaders: {
|
|
27
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
// Mask automation signals
|
|
31
|
+
await this.context.addInitScript(() => {
|
|
32
|
+
Object.defineProperty(navigator, "webdriver", { get: () => undefined });
|
|
33
|
+
});
|
|
34
|
+
this.page = await this.context.newPage();
|
|
35
|
+
}
|
|
36
|
+
async getPage() {
|
|
37
|
+
if (!this.page || !this.browser) {
|
|
38
|
+
await this.init();
|
|
39
|
+
}
|
|
40
|
+
return this.page;
|
|
41
|
+
}
|
|
42
|
+
getContext() {
|
|
43
|
+
return this.context;
|
|
44
|
+
}
|
|
45
|
+
async close() {
|
|
46
|
+
if (this.page) {
|
|
47
|
+
await this.page.close().catch(() => { });
|
|
48
|
+
this.page = null;
|
|
49
|
+
}
|
|
50
|
+
if (this.context) {
|
|
51
|
+
await this.context.close().catch(() => { });
|
|
52
|
+
this.context = null;
|
|
53
|
+
}
|
|
54
|
+
if (this.browser) {
|
|
55
|
+
await this.browser.close().catch(() => { });
|
|
56
|
+
this.browser = null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async isLoggedIn() {
|
|
60
|
+
const page = await this.getPage();
|
|
61
|
+
try {
|
|
62
|
+
// Navigate to thumbtack.com and check for authenticated nav elements
|
|
63
|
+
await page.goto("https://www.thumbtack.com", {
|
|
64
|
+
waitUntil: "domcontentloaded",
|
|
65
|
+
timeout: 30000,
|
|
66
|
+
});
|
|
67
|
+
// Check for user profile indicators in nav
|
|
68
|
+
const loggedIn = await page.evaluate(() => {
|
|
69
|
+
// Look for sign-in button absence or profile menu presence
|
|
70
|
+
const signInBtn = document.querySelector('[data-test="sign-in-button"], a[href*="login"], button[data-testid="login"]');
|
|
71
|
+
const profileMenu = document.querySelector('[data-test="profile-menu"], [data-testid="user-avatar"], .user-avatar');
|
|
72
|
+
return !signInBtn || !!profileMenu;
|
|
73
|
+
});
|
|
74
|
+
return loggedIn;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Singleton instance
|
|
82
|
+
let browserManagerInstance = null;
|
|
83
|
+
export function getBrowserManager() {
|
|
84
|
+
if (!browserManagerInstance) {
|
|
85
|
+
browserManagerInstance = new BrowserManager();
|
|
86
|
+
}
|
|
87
|
+
return browserManagerInstance;
|
|
88
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { startServer } from "./server.js";
|
|
3
|
+
import { getBrowserManager } from "./browser.js";
|
|
4
|
+
// Handle graceful shutdown
|
|
5
|
+
process.on("SIGINT", async () => {
|
|
6
|
+
process.stderr.write("Shutting down Thumbtack MCP server...\n");
|
|
7
|
+
const manager = getBrowserManager();
|
|
8
|
+
await manager.close();
|
|
9
|
+
process.exit(0);
|
|
10
|
+
});
|
|
11
|
+
process.on("SIGTERM", async () => {
|
|
12
|
+
process.stderr.write("Shutting down Thumbtack MCP server...\n");
|
|
13
|
+
const manager = getBrowserManager();
|
|
14
|
+
await manager.close();
|
|
15
|
+
process.exit(0);
|
|
16
|
+
});
|
|
17
|
+
process.on("uncaughtException", async (error) => {
|
|
18
|
+
process.stderr.write(`Uncaught exception: ${error.message}\n`);
|
|
19
|
+
const manager = getBrowserManager();
|
|
20
|
+
await manager.close().catch(() => { });
|
|
21
|
+
process.exit(1);
|
|
22
|
+
});
|
|
23
|
+
process.on("unhandledRejection", async (reason) => {
|
|
24
|
+
process.stderr.write(`Unhandled rejection: ${reason}\n`);
|
|
25
|
+
});
|
|
26
|
+
// Start the MCP server
|
|
27
|
+
startServer().catch((error) => {
|
|
28
|
+
process.stderr.write(`Failed to start server: ${error.message}\n`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
});
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { searchServices } from "./tools/search-services.js";
|
|
5
|
+
import { getProvider } from "./tools/get-provider.js";
|
|
6
|
+
import { requestQuote } from "./tools/request-quote.js";
|
|
7
|
+
import { viewQuotes } from "./tools/view-quotes.js";
|
|
8
|
+
import { hireProvider } from "./tools/hire-provider.js";
|
|
9
|
+
import { getProjects } from "./tools/get-projects.js";
|
|
10
|
+
export function createServer() {
|
|
11
|
+
const server = new Server({
|
|
12
|
+
name: "mcp-thumbtack",
|
|
13
|
+
version: "1.0.0",
|
|
14
|
+
}, {
|
|
15
|
+
capabilities: {
|
|
16
|
+
tools: {},
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
// List tools handler
|
|
20
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
21
|
+
return {
|
|
22
|
+
tools: [
|
|
23
|
+
{
|
|
24
|
+
name: "thumbtack_search_services",
|
|
25
|
+
description: "Search for service providers on Thumbtack home services marketplace. Returns a list of matching professionals with ratings, reviews, and contact information.",
|
|
26
|
+
inputSchema: {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
service_type: {
|
|
30
|
+
type: "string",
|
|
31
|
+
description: 'Type of service to search for (e.g., "plumber", "house cleaner", "electrician", "photographer")',
|
|
32
|
+
},
|
|
33
|
+
location: {
|
|
34
|
+
type: "string",
|
|
35
|
+
description: "City and state or full address (e.g., 'San Francisco, CA')",
|
|
36
|
+
},
|
|
37
|
+
zip_code: {
|
|
38
|
+
type: "string",
|
|
39
|
+
description: "ZIP code for location-based search (e.g., '94102')",
|
|
40
|
+
},
|
|
41
|
+
date: {
|
|
42
|
+
type: "string",
|
|
43
|
+
description: "Preferred service date in YYYY-MM-DD format",
|
|
44
|
+
},
|
|
45
|
+
num_people: {
|
|
46
|
+
type: "number",
|
|
47
|
+
description: "Number of people (relevant for event services)",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
required: ["service_type"],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "thumbtack_get_provider",
|
|
55
|
+
description: "Get detailed information about a specific Thumbtack service provider, including their profile, services offered, reviews, and contact information.",
|
|
56
|
+
inputSchema: {
|
|
57
|
+
type: "object",
|
|
58
|
+
properties: {
|
|
59
|
+
provider_id: {
|
|
60
|
+
type: "string",
|
|
61
|
+
description: "The Thumbtack provider ID (numeric ID from their profile URL)",
|
|
62
|
+
},
|
|
63
|
+
provider_url: {
|
|
64
|
+
type: "string",
|
|
65
|
+
description: "Full URL to the provider's Thumbtack profile page",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "thumbtack_request_quote",
|
|
72
|
+
description: "Request a quote from a specific Thumbtack service provider. Requires authentication - user must be logged in to Thumbtack.",
|
|
73
|
+
inputSchema: {
|
|
74
|
+
type: "object",
|
|
75
|
+
properties: {
|
|
76
|
+
provider_id: {
|
|
77
|
+
type: "string",
|
|
78
|
+
description: "The Thumbtack provider ID",
|
|
79
|
+
},
|
|
80
|
+
service_type: {
|
|
81
|
+
type: "string",
|
|
82
|
+
description: "Type of service you need",
|
|
83
|
+
},
|
|
84
|
+
description: {
|
|
85
|
+
type: "string",
|
|
86
|
+
description: "Detailed description of the project or service needed",
|
|
87
|
+
},
|
|
88
|
+
location: {
|
|
89
|
+
type: "string",
|
|
90
|
+
description: "Service location ZIP code or address",
|
|
91
|
+
},
|
|
92
|
+
contact_info: {
|
|
93
|
+
type: "object",
|
|
94
|
+
description: "Contact information for the request",
|
|
95
|
+
properties: {
|
|
96
|
+
name: {
|
|
97
|
+
type: "string",
|
|
98
|
+
description: "Your name",
|
|
99
|
+
},
|
|
100
|
+
email: {
|
|
101
|
+
type: "string",
|
|
102
|
+
description: "Your email address",
|
|
103
|
+
},
|
|
104
|
+
phone: {
|
|
105
|
+
type: "string",
|
|
106
|
+
description: "Your phone number",
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
required: ["provider_id", "service_type", "description", "location", "contact_info"],
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: "thumbtack_view_quotes",
|
|
116
|
+
description: "View quotes received from Thumbtack service providers in your inbox. Requires authentication.",
|
|
117
|
+
inputSchema: {
|
|
118
|
+
type: "object",
|
|
119
|
+
properties: {
|
|
120
|
+
status: {
|
|
121
|
+
type: "string",
|
|
122
|
+
enum: ["pending", "active", "completed", "archived", "all"],
|
|
123
|
+
description: "Filter quotes by status (default: all)",
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: "thumbtack_hire_provider",
|
|
130
|
+
description: "Hire a service provider from a received quote. This action creates a project and commits to working with the provider. Requires authentication.",
|
|
131
|
+
inputSchema: {
|
|
132
|
+
type: "object",
|
|
133
|
+
properties: {
|
|
134
|
+
quote_id: {
|
|
135
|
+
type: "string",
|
|
136
|
+
description: "The ID of the quote to accept",
|
|
137
|
+
},
|
|
138
|
+
provider_id: {
|
|
139
|
+
type: "string",
|
|
140
|
+
description: "The Thumbtack provider ID",
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
required: ["quote_id", "provider_id"],
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: "thumbtack_get_projects",
|
|
148
|
+
description: "Get current and past projects from your Thumbtack account, including project status, provider information, and completion details. Requires authentication.",
|
|
149
|
+
inputSchema: {
|
|
150
|
+
type: "object",
|
|
151
|
+
properties: {
|
|
152
|
+
status: {
|
|
153
|
+
type: "string",
|
|
154
|
+
enum: ["active", "completed", "all"],
|
|
155
|
+
description: "Filter projects by status (default: all)",
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
// Call tool handler
|
|
164
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
165
|
+
const { name, arguments: args } = request.params;
|
|
166
|
+
try {
|
|
167
|
+
switch (name) {
|
|
168
|
+
case "thumbtack_search_services": {
|
|
169
|
+
const params = args;
|
|
170
|
+
if (!params.service_type) {
|
|
171
|
+
return {
|
|
172
|
+
content: [{ type: "text", text: "Error: service_type parameter is required" }],
|
|
173
|
+
isError: true,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const result = await searchServices(params);
|
|
177
|
+
return {
|
|
178
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
case "thumbtack_get_provider": {
|
|
182
|
+
const params = args;
|
|
183
|
+
if (!params.provider_id && !params.provider_url) {
|
|
184
|
+
return {
|
|
185
|
+
content: [
|
|
186
|
+
{
|
|
187
|
+
type: "text",
|
|
188
|
+
text: "Error: Either provider_id or provider_url must be provided",
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
isError: true,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
const result = await getProvider(params);
|
|
195
|
+
return {
|
|
196
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
case "thumbtack_request_quote": {
|
|
200
|
+
const params = args;
|
|
201
|
+
if (!params.provider_id || !params.service_type || !params.description || !params.location) {
|
|
202
|
+
return {
|
|
203
|
+
content: [
|
|
204
|
+
{
|
|
205
|
+
type: "text",
|
|
206
|
+
text: "Error: provider_id, service_type, description, and location are required",
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
isError: true,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const result = await requestQuote(params);
|
|
213
|
+
return {
|
|
214
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
case "thumbtack_view_quotes": {
|
|
218
|
+
const params = (args || {});
|
|
219
|
+
const result = await viewQuotes(params);
|
|
220
|
+
return {
|
|
221
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
case "thumbtack_hire_provider": {
|
|
225
|
+
const params = args;
|
|
226
|
+
if (!params.quote_id || !params.provider_id) {
|
|
227
|
+
return {
|
|
228
|
+
content: [
|
|
229
|
+
{
|
|
230
|
+
type: "text",
|
|
231
|
+
text: "Error: quote_id and provider_id are required",
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
isError: true,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const result = await hireProvider(params);
|
|
238
|
+
return {
|
|
239
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
case "thumbtack_get_projects": {
|
|
243
|
+
const params = (args || {});
|
|
244
|
+
const result = await getProjects(params);
|
|
245
|
+
return {
|
|
246
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
default:
|
|
250
|
+
return {
|
|
251
|
+
content: [{ type: "text", text: `Error: Unknown tool: ${name}` }],
|
|
252
|
+
isError: true,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
const err = error;
|
|
258
|
+
return {
|
|
259
|
+
content: [
|
|
260
|
+
{
|
|
261
|
+
type: "text",
|
|
262
|
+
text: `Error: ${err.message || "An unexpected error occurred"}`,
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
isError: true,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
return server;
|
|
270
|
+
}
|
|
271
|
+
export async function startServer() {
|
|
272
|
+
const server = createServer();
|
|
273
|
+
const transport = new StdioServerTransport();
|
|
274
|
+
await server.connect(transport);
|
|
275
|
+
process.stderr.write("Thumbtack MCP server started\n");
|
|
276
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Page, BrowserContext } from "playwright";
|
|
2
|
+
export declare function saveSession(page: Page, context: BrowserContext): Promise<void>;
|
|
3
|
+
export declare function loadSession(page: Page, context: BrowserContext): Promise<boolean>;
|
|
4
|
+
export declare function clearSession(): Promise<void>;
|
|
5
|
+
export declare function sessionExists(): Promise<boolean>;
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { promises as fs } from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
const SESSION_FILE = path.join(os.homedir(), ".thumbtack-session.json");
|
|
5
|
+
export async function saveSession(page, context) {
|
|
6
|
+
try {
|
|
7
|
+
// Collect cookies from the browser context
|
|
8
|
+
const cookies = await context.cookies();
|
|
9
|
+
// Collect localStorage from thumbtack domain
|
|
10
|
+
const localStorage = await page.evaluate(() => {
|
|
11
|
+
const data = {};
|
|
12
|
+
for (let i = 0; i < window.localStorage.length; i++) {
|
|
13
|
+
const key = window.localStorage.key(i);
|
|
14
|
+
if (key) {
|
|
15
|
+
const value = window.localStorage.getItem(key);
|
|
16
|
+
if (value !== null) {
|
|
17
|
+
data[key] = value;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return data;
|
|
22
|
+
});
|
|
23
|
+
const sessionData = {
|
|
24
|
+
cookies: cookies.map((c) => ({
|
|
25
|
+
name: c.name,
|
|
26
|
+
value: c.value,
|
|
27
|
+
domain: c.domain,
|
|
28
|
+
path: c.path,
|
|
29
|
+
expires: c.expires,
|
|
30
|
+
httpOnly: c.httpOnly,
|
|
31
|
+
secure: c.secure,
|
|
32
|
+
sameSite: c.sameSite || "Lax",
|
|
33
|
+
})),
|
|
34
|
+
localStorage,
|
|
35
|
+
savedAt: new Date().toISOString(),
|
|
36
|
+
};
|
|
37
|
+
await fs.writeFile(SESSION_FILE, JSON.stringify(sessionData, null, 2), {
|
|
38
|
+
mode: 0o600,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
const err = error;
|
|
43
|
+
throw new Error(`Failed to save session: ${err.message}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function loadSession(page, context) {
|
|
47
|
+
try {
|
|
48
|
+
const raw = await fs.readFile(SESSION_FILE, "utf-8");
|
|
49
|
+
const sessionData = JSON.parse(raw);
|
|
50
|
+
// Check session age - invalidate after 7 days
|
|
51
|
+
const savedAt = new Date(sessionData.savedAt).getTime();
|
|
52
|
+
const ageMs = Date.now() - savedAt;
|
|
53
|
+
const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;
|
|
54
|
+
if (ageMs > sevenDaysMs) {
|
|
55
|
+
await clearSession();
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
// Restore cookies
|
|
59
|
+
if (sessionData.cookies && sessionData.cookies.length > 0) {
|
|
60
|
+
await context.addCookies(sessionData.cookies);
|
|
61
|
+
}
|
|
62
|
+
// Navigate to thumbtack first so we can set localStorage
|
|
63
|
+
await page.goto("https://www.thumbtack.com", {
|
|
64
|
+
waitUntil: "domcontentloaded",
|
|
65
|
+
timeout: 30000,
|
|
66
|
+
});
|
|
67
|
+
// Restore localStorage
|
|
68
|
+
if (sessionData.localStorage &&
|
|
69
|
+
Object.keys(sessionData.localStorage).length > 0) {
|
|
70
|
+
await page.evaluate((data) => {
|
|
71
|
+
for (const [key, value] of Object.entries(data)) {
|
|
72
|
+
window.localStorage.setItem(key, value);
|
|
73
|
+
}
|
|
74
|
+
}, sessionData.localStorage);
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
// Session file doesn't exist or is invalid
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export async function clearSession() {
|
|
84
|
+
try {
|
|
85
|
+
await fs.unlink(SESSION_FILE);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// File may not exist, that's fine
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export async function sessionExists() {
|
|
92
|
+
try {
|
|
93
|
+
await fs.access(SESSION_FILE);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface GetProjectsParams {
|
|
2
|
+
status?: "active" | "completed" | "all";
|
|
3
|
+
}
|
|
4
|
+
export interface Project {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
providerName: string;
|
|
8
|
+
providerUrl: string | null;
|
|
9
|
+
providerImageUrl: string | null;
|
|
10
|
+
serviceType: string;
|
|
11
|
+
status: string;
|
|
12
|
+
startDate: string | null;
|
|
13
|
+
completedDate: string | null;
|
|
14
|
+
price: string | null;
|
|
15
|
+
location: string | null;
|
|
16
|
+
description: string | null;
|
|
17
|
+
hasReview: boolean;
|
|
18
|
+
rating: number | null;
|
|
19
|
+
projectUrl: string | null;
|
|
20
|
+
}
|
|
21
|
+
export interface GetProjectsResult {
|
|
22
|
+
projects: Project[];
|
|
23
|
+
totalCount: number;
|
|
24
|
+
requiresLogin: boolean;
|
|
25
|
+
message: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function getProjects(params: GetProjectsParams): Promise<GetProjectsResult>;
|