daeda-mcp 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,355 @@
1
+ import AdmZip from "adm-zip";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, createWriteStream, unlinkSync, } from "node:fs";
3
+ import { join, dirname } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { pipeline } from "node:stream/promises";
6
+ import { Readable } from "node:stream";
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const HUBSPOT_API_BASE = "https://api.hubapi.com";
9
+ const CACHE_DIR = join(__dirname, "../../data/cache");
10
+ function ensureCacheDir() {
11
+ if (!existsSync(CACHE_DIR)) {
12
+ mkdirSync(CACHE_DIR, { recursive: true });
13
+ }
14
+ }
15
+ function getCachePath(name) {
16
+ return join(CACHE_DIR, `${name}.csv`);
17
+ }
18
+ export function getCachedCsv(name) {
19
+ const path = getCachePath(name);
20
+ if (existsSync(path)) {
21
+ return readFileSync(path, "utf-8");
22
+ }
23
+ return null;
24
+ }
25
+ export function saveCachedCsv(name, data) {
26
+ ensureCacheDir();
27
+ writeFileSync(getCachePath(name), data, "utf-8");
28
+ }
29
+ export function hasCachedExports() {
30
+ const required = ["contacts", "companies", "deals", "contact_company", "deal_contact", "deal_company"];
31
+ return required.every((name) => existsSync(getCachePath(name)));
32
+ }
33
+ const ASSOCIATION_CONFIGS = [
34
+ { fromObjectType: "contacts", toObjectType: "companies", associationType: "contact_company" },
35
+ { fromObjectType: "deals", toObjectType: "contacts", associationType: "deal_contact" },
36
+ { fromObjectType: "deals", toObjectType: "companies", associationType: "deal_company" },
37
+ ];
38
+ async function apiRequest(token, method, path, body) {
39
+ const response = await fetch(`${HUBSPOT_API_BASE}${path}`, {
40
+ method,
41
+ headers: {
42
+ Authorization: `Bearer ${token}`,
43
+ "Content-Type": "application/json",
44
+ },
45
+ body: body ? JSON.stringify(body) : undefined,
46
+ });
47
+ if (!response.ok) {
48
+ const errorText = await response.text();
49
+ throw new Error(`HubSpot API error (${response.status}): ${errorText}`);
50
+ }
51
+ return response.json();
52
+ }
53
+ export async function fetchObjectProperties(token, objectType) {
54
+ const result = (await apiRequest(token, "GET", `/crm/v3/properties/${objectType}`));
55
+ return result.results.map((prop) => prop.name);
56
+ }
57
+ export async function startObjectExport(token, objectType, properties) {
58
+ const body = {
59
+ exportType: "VIEW",
60
+ format: "CSV",
61
+ exportName: `daeda-mcp-${objectType}-export`,
62
+ objectType: objectType.toUpperCase(),
63
+ objectProperties: properties,
64
+ associatedObjectType: null,
65
+ };
66
+ const result = (await apiRequest(token, "POST", "/crm/v3/exports/export/async", body));
67
+ return result.id;
68
+ }
69
+ export async function startAssociationExport(token, fromObjectType, toObjectType) {
70
+ // For associations, we export the "from" object with its associated object IDs
71
+ const body = {
72
+ exportType: "VIEW",
73
+ format: "CSV",
74
+ exportName: `daeda-mcp-${fromObjectType}-to-${toObjectType}-associations`,
75
+ objectType: fromObjectType.toUpperCase(),
76
+ objectProperties: [], // We only need IDs
77
+ associatedObjectType: toObjectType.toUpperCase(),
78
+ };
79
+ const result = (await apiRequest(token, "POST", "/crm/v3/exports/export/async", body));
80
+ return result.id;
81
+ }
82
+ export async function getExportStatus(token, exportId) {
83
+ const result = (await apiRequest(token, "GET", `/crm/v3/exports/export/async/tasks/${exportId}/status`));
84
+ return result;
85
+ }
86
+ export async function listExportTasks(token) {
87
+ const allTasks = [];
88
+ let after;
89
+ do {
90
+ const path = after
91
+ ? `/crm/v3/exports/export/async/tasks?after=${after}`
92
+ : `/crm/v3/exports/export/async/tasks`;
93
+ const response = (await apiRequest(token, "GET", path));
94
+ allTasks.push(...response.results);
95
+ after = response.paging?.next?.after;
96
+ } while (after);
97
+ return allTasks;
98
+ }
99
+ const EXPORT_NAME_MAP = {
100
+ "daeda-mcp-contacts-export": "contacts",
101
+ "daeda-mcp-companies-export": "companies",
102
+ "daeda-mcp-deals-export": "deals",
103
+ "daeda-mcp-contacts-to-companies-associations": "contact_company",
104
+ "daeda-mcp-deals-to-contacts-associations": "deal_contact",
105
+ "daeda-mcp-deals-to-companies-associations": "deal_company",
106
+ };
107
+ const REUSABLE_STATUSES = new Set(["COMPLETE", "PENDING", "PROCESSING"]);
108
+ export async function findReusableExports(token, maxAgeDays = 7) {
109
+ const tasks = await listExportTasks(token);
110
+ const cutoffDate = new Date();
111
+ cutoffDate.setDate(cutoffDate.getDate() - maxAgeDays);
112
+ const reusable = new Map();
113
+ for (const task of tasks) {
114
+ if (!REUSABLE_STATUSES.has(task.status))
115
+ continue;
116
+ const internalName = EXPORT_NAME_MAP[task.exportName];
117
+ if (!internalName)
118
+ continue;
119
+ const createdAt = new Date(task.createdAt);
120
+ if (createdAt < cutoffDate)
121
+ continue;
122
+ const existing = reusable.get(internalName);
123
+ if (!existing || createdAt > existing.createdAt) {
124
+ reusable.set(internalName, {
125
+ exportName: internalName,
126
+ exportId: task.id,
127
+ createdAt,
128
+ });
129
+ }
130
+ }
131
+ return reusable;
132
+ }
133
+ export async function pollExportUntilComplete(token, exportId, onProgress, maxWaitMs = 300000, // 5 minutes
134
+ pollIntervalMs = 2000) {
135
+ const startTime = Date.now();
136
+ while (Date.now() - startTime < maxWaitMs) {
137
+ const status = await getExportStatus(token, exportId);
138
+ onProgress?.(status.status);
139
+ if (status.status === "COMPLETE") {
140
+ if (!status.result) {
141
+ throw new Error("Export completed but no result URL provided");
142
+ }
143
+ return status.result;
144
+ }
145
+ if (status.status === "FAILED") {
146
+ throw new Error(`Export failed: ${status.message || "Unknown error"}`);
147
+ }
148
+ // Wait before next poll
149
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
150
+ }
151
+ throw new Error(`Export timed out after ${maxWaitMs / 1000} seconds`);
152
+ }
153
+ export async function downloadExportCsv(token, downloadUrl) {
154
+ const response = await fetch(downloadUrl, {
155
+ headers: {
156
+ Authorization: `Bearer ${token}`,
157
+ },
158
+ });
159
+ if (!response.ok) {
160
+ throw new Error(`Failed to download export: ${response.status}`);
161
+ }
162
+ const buffer = await response.arrayBuffer();
163
+ const data = Buffer.from(buffer);
164
+ // Check if it's a ZIP file (starts with PK)
165
+ if (data[0] === 0x50 && data[1] === 0x4b) {
166
+ const zip = new AdmZip(data);
167
+ const entries = zip.getEntries();
168
+ const csvEntries = entries
169
+ .filter((e) => e.entryName.endsWith(".csv"))
170
+ .sort((a, b) => a.entryName.localeCompare(b.entryName));
171
+ if (csvEntries.length === 0) {
172
+ throw new Error("No CSV file found in ZIP archive");
173
+ }
174
+ if (csvEntries.length === 1) {
175
+ return csvEntries[0].getData().toString("utf-8");
176
+ }
177
+ // Multiple CSVs - concatenate them, keeping header only from first file
178
+ const csvParts = [];
179
+ for (let i = 0; i < csvEntries.length; i++) {
180
+ const csvContent = csvEntries[i].getData().toString("utf-8");
181
+ if (i === 0) {
182
+ csvParts.push(csvContent);
183
+ }
184
+ else {
185
+ // Skip header row for subsequent files
186
+ const firstNewline = csvContent.indexOf("\n");
187
+ if (firstNewline !== -1) {
188
+ csvParts.push(csvContent.slice(firstNewline + 1));
189
+ }
190
+ }
191
+ }
192
+ console.error(`[export-api] Concatenated ${csvEntries.length} CSV files from ZIP`);
193
+ return csvParts.join("\n");
194
+ }
195
+ // Not a ZIP, return as text
196
+ return data.toString("utf-8");
197
+ }
198
+ export async function downloadExportCsvToFile(token, downloadUrl, exportName) {
199
+ ensureCacheDir();
200
+ const outputPath = getCachePath(exportName);
201
+ const tempPath = `${outputPath}.tmp`;
202
+ const response = await fetch(downloadUrl, {
203
+ headers: {
204
+ Authorization: `Bearer ${token}`,
205
+ },
206
+ });
207
+ if (!response.ok) {
208
+ throw new Error(`Failed to download export: ${response.status}`);
209
+ }
210
+ const contentLength = response.headers.get("content-length");
211
+ console.error(`[export-api] Downloading ${exportName} (${contentLength ? `${Math.round(Number(contentLength) / 1024 / 1024)}MB` : "unknown size"})...`);
212
+ if (!response.body) {
213
+ throw new Error("Response has no body");
214
+ }
215
+ await pipeline(Readable.fromWeb(response.body), createWriteStream(tempPath));
216
+ const data = readFileSync(tempPath);
217
+ if (data[0] === 0x50 && data[1] === 0x4b) {
218
+ const zip = new AdmZip(tempPath);
219
+ const entries = zip.getEntries();
220
+ const csvEntries = entries
221
+ .filter((e) => e.entryName.endsWith(".csv"))
222
+ .sort((a, b) => a.entryName.localeCompare(b.entryName));
223
+ if (csvEntries.length === 0) {
224
+ unlinkSync(tempPath);
225
+ throw new Error("No CSV file found in ZIP archive");
226
+ }
227
+ const writeStream = createWriteStream(outputPath);
228
+ for (let i = 0; i < csvEntries.length; i++) {
229
+ const csvContent = csvEntries[i].getData().toString("utf-8");
230
+ if (i === 0) {
231
+ writeStream.write(csvContent);
232
+ }
233
+ else {
234
+ const firstNewline = csvContent.indexOf("\n");
235
+ if (firstNewline !== -1) {
236
+ writeStream.write("\n");
237
+ writeStream.write(csvContent.slice(firstNewline + 1));
238
+ }
239
+ }
240
+ }
241
+ await new Promise((resolve, reject) => {
242
+ writeStream.end((err) => {
243
+ if (err)
244
+ reject(err);
245
+ else
246
+ resolve();
247
+ });
248
+ });
249
+ unlinkSync(tempPath);
250
+ console.error(`[export-api] Extracted and concatenated ${csvEntries.length} CSV files from ZIP`);
251
+ }
252
+ else {
253
+ const { renameSync } = await import("node:fs");
254
+ renameSync(tempPath, outputPath);
255
+ }
256
+ return outputPath;
257
+ }
258
+ export async function runAllExports(token, onProgress, useCache = true) {
259
+ const objectTypes = ["contacts", "companies", "deals"];
260
+ const objects = {};
261
+ const associations = {};
262
+ // Check if we have all cached exports
263
+ if (useCache && hasCachedExports()) {
264
+ onProgress?.("Found cached exports, loading from disk...");
265
+ for (const objectType of objectTypes) {
266
+ objects[objectType] = getCachedCsv(objectType);
267
+ onProgress?.(`Loaded ${objectType} from cache`);
268
+ }
269
+ for (const config of ASSOCIATION_CONFIGS) {
270
+ associations[config.associationType] = getCachedCsv(config.associationType);
271
+ onProgress?.(`Loaded ${config.associationType} from cache`);
272
+ }
273
+ return {
274
+ objects: objects,
275
+ associations: associations,
276
+ };
277
+ }
278
+ onProgress?.("Starting parallel exports from HubSpot...");
279
+ const objectExportPromises = objectTypes.map(async (objectType) => {
280
+ const cached = useCache ? getCachedCsv(objectType) : null;
281
+ if (cached) {
282
+ onProgress?.(`Using cached ${objectType} export`);
283
+ return { objectType, csvData: cached, fromCache: true };
284
+ }
285
+ onProgress?.(`[${objectType}] Fetching properties...`);
286
+ const properties = await fetchObjectProperties(token, objectType);
287
+ onProgress?.(`[${objectType}] Found ${properties.length} properties, starting export...`);
288
+ const exportId = await startObjectExport(token, objectType, properties);
289
+ onProgress?.(`[${objectType}] Export started, polling for completion...`);
290
+ const downloadUrl = await pollExportUntilComplete(token, exportId, (status) => {
291
+ onProgress?.(`[${objectType}] ${status}`);
292
+ });
293
+ onProgress?.(`[${objectType}] Downloading CSV...`);
294
+ const csvData = await downloadExportCsv(token, downloadUrl);
295
+ saveCachedCsv(objectType, csvData);
296
+ onProgress?.(`[${objectType}] Export complete`);
297
+ return { objectType, csvData, fromCache: false };
298
+ });
299
+ const associationExportPromises = ASSOCIATION_CONFIGS.map(async (config) => {
300
+ const label = `${config.fromObjectType}→${config.toObjectType}`;
301
+ const cached = useCache ? getCachedCsv(config.associationType) : null;
302
+ if (cached) {
303
+ onProgress?.(`Using cached ${label} association export`);
304
+ return { associationType: config.associationType, csvData: cached, fromCache: true };
305
+ }
306
+ onProgress?.(`[${label}] Starting association export...`);
307
+ const exportId = await startAssociationExport(token, config.fromObjectType, config.toObjectType);
308
+ onProgress?.(`[${label}] Export started, polling for completion...`);
309
+ const downloadUrl = await pollExportUntilComplete(token, exportId, (status) => {
310
+ onProgress?.(`[${label}] ${status}`);
311
+ });
312
+ onProgress?.(`[${label}] Downloading CSV...`);
313
+ const csvData = await downloadExportCsv(token, downloadUrl);
314
+ saveCachedCsv(config.associationType, csvData);
315
+ onProgress?.(`[${label}] Export complete`);
316
+ return { associationType: config.associationType, csvData, fromCache: false };
317
+ });
318
+ const [objectResults, associationResults] = await Promise.all([
319
+ Promise.all(objectExportPromises),
320
+ Promise.all(associationExportPromises),
321
+ ]);
322
+ for (const result of objectResults) {
323
+ objects[result.objectType] = result.csvData;
324
+ }
325
+ for (const result of associationResults) {
326
+ associations[result.associationType] = result.csvData;
327
+ }
328
+ onProgress?.("All exports complete");
329
+ return {
330
+ objects: objects,
331
+ associations: associations,
332
+ };
333
+ }
334
+ export async function validateToken(token) {
335
+ try {
336
+ const response = await fetch(`${HUBSPOT_API_BASE}/crm/v3/objects/contacts?limit=1`, {
337
+ headers: {
338
+ Authorization: `Bearer ${token}`,
339
+ },
340
+ });
341
+ if (response.ok) {
342
+ return { valid: true };
343
+ }
344
+ const errorText = await response.text();
345
+ return {
346
+ valid: false,
347
+ error: `HTTP ${response.status}: ${errorText}`
348
+ };
349
+ }
350
+ catch (err) {
351
+ const errorMessage = err instanceof Error ? err.message : String(err);
352
+ return { valid: false, error: errorMessage };
353
+ }
354
+ }
355
+ export { ASSOCIATION_CONFIGS };
@@ -0,0 +1,5 @@
1
+ import { type InitState } from "./init-state.js";
2
+ export declare function getInitStatus(): InitState;
3
+ export declare function startInitialization(force?: boolean): Promise<void>;
4
+ export declare function resumeIfNeeded(): Promise<void>;
5
+ export declare function forceReinitialize(): void;
@@ -0,0 +1,274 @@
1
+ import { readInitState, writeInitState, createInitialState, isFullySynced, getSyncedCount, ALL_EXPORTS, } from "./init-state.js";
2
+ import { validateToken, startObjectExport, startAssociationExport, getExportStatus, downloadExportCsvToFile, fetchObjectProperties, findReusableExports, } from "./export-api.js";
3
+ import { loadContactsCsvFromFile, loadCompaniesCsvFromFile, loadDealsCsvFromFile, loadAssociationsCsvFromFile, } from "./csv-loader.js";
4
+ import { runSeeding } from "./seeder.js";
5
+ import { getHubSpotToken } from "../db/keychain.js";
6
+ import { getDb, setMetadata } from "../db/sqlite.js";
7
+ const POLL_INTERVAL_MS = 3000;
8
+ const OBJECT_EXPORTS = ["contacts", "companies", "deals"];
9
+ const ASSOCIATION_EXPORTS = [
10
+ { name: "contact_company", from: "contacts", to: "companies" },
11
+ { name: "deal_contact", from: "deals", to: "contacts" },
12
+ { name: "deal_company", from: "deals", to: "companies" },
13
+ ];
14
+ let pollLoopRunning = false;
15
+ let pollLoopTimeout = null;
16
+ export function getInitStatus() {
17
+ return readInitState();
18
+ }
19
+ export async function startInitialization(force = false) {
20
+ const state = readInitState();
21
+ if (state.status === "ready" && !force) {
22
+ console.error("[init-manager] Database already initialized");
23
+ return;
24
+ }
25
+ if (state.status === "polling_exports" && !force) {
26
+ console.error("[init-manager] Initialization already in progress, resuming poll loop");
27
+ startPollLoop();
28
+ return;
29
+ }
30
+ if (state.status === "sending_requests" && !force) {
31
+ console.error("[init-manager] Export requests in progress");
32
+ return;
33
+ }
34
+ const token = getHubSpotToken();
35
+ if (!token) {
36
+ const errorState = createInitialState();
37
+ errorState.status = "error";
38
+ errorState.error = "No HubSpot token found. Set HS_PRIVATE_TOKEN environment variable.";
39
+ writeInitState(errorState);
40
+ console.error("[init-manager] No HubSpot token");
41
+ return;
42
+ }
43
+ const validation = await validateToken(token);
44
+ if (!validation.valid) {
45
+ const errorState = createInitialState();
46
+ errorState.status = "error";
47
+ errorState.error = `Invalid HubSpot token: ${validation.error}`;
48
+ writeInitState(errorState);
49
+ console.error("[init-manager] Invalid token:", validation.error);
50
+ return;
51
+ }
52
+ await getDb();
53
+ const newState = createInitialState();
54
+ newState.status = "sending_requests";
55
+ newState.startedAt = new Date().toISOString();
56
+ writeInitState(newState);
57
+ console.error("[init-manager] Starting export requests...");
58
+ try {
59
+ await fireAllExportRequests(token, newState);
60
+ newState.status = "polling_exports";
61
+ newState.seedingStatus = "pending";
62
+ writeInitState(newState);
63
+ console.error("[init-manager] All export requests sent, starting seeding and poll loop");
64
+ runSeeding(token).catch((err) => {
65
+ console.error("[init-manager] Seeding failed (non-fatal):", err);
66
+ });
67
+ startPollLoop();
68
+ }
69
+ catch (err) {
70
+ newState.status = "error";
71
+ newState.error = err instanceof Error ? err.message : String(err);
72
+ writeInitState(newState);
73
+ console.error("[init-manager] Failed to send export requests:", newState.error);
74
+ }
75
+ }
76
+ async function fireAllExportRequests(token, state) {
77
+ console.error("[init-manager] Checking for reusable exports from the past week...");
78
+ let reusableExports;
79
+ try {
80
+ reusableExports = await findReusableExports(token, 7);
81
+ if (reusableExports.size > 0) {
82
+ console.error(`[init-manager] Found ${reusableExports.size} reusable export(s)`);
83
+ }
84
+ }
85
+ catch (err) {
86
+ console.error("[init-manager] Failed to check for reusable exports, creating new ones:", err);
87
+ reusableExports = new Map();
88
+ }
89
+ for (const objectType of OBJECT_EXPORTS) {
90
+ const existing = reusableExports.get(objectType);
91
+ if (existing) {
92
+ state.exports[objectType].exportId = existing.exportId;
93
+ state.exports[objectType].status = "polling";
94
+ writeInitState(state);
95
+ console.error(`[init-manager] Reusing ${objectType} export: ${existing.exportId} (created ${existing.createdAt.toISOString()})`);
96
+ continue;
97
+ }
98
+ const properties = await fetchObjectProperties(token, objectType);
99
+ const exportId = await startObjectExport(token, objectType, properties);
100
+ state.exports[objectType].exportId = exportId;
101
+ state.exports[objectType].status = "polling";
102
+ writeInitState(state);
103
+ console.error(`[init-manager] Started new ${objectType} export: ${exportId}`);
104
+ }
105
+ for (const assoc of ASSOCIATION_EXPORTS) {
106
+ const existing = reusableExports.get(assoc.name);
107
+ if (existing) {
108
+ state.exports[assoc.name].exportId = existing.exportId;
109
+ state.exports[assoc.name].status = "polling";
110
+ writeInitState(state);
111
+ console.error(`[init-manager] Reusing ${assoc.name} export: ${existing.exportId} (created ${existing.createdAt.toISOString()})`);
112
+ continue;
113
+ }
114
+ const exportId = await startAssociationExport(token, assoc.from, assoc.to);
115
+ state.exports[assoc.name].exportId = exportId;
116
+ state.exports[assoc.name].status = "polling";
117
+ writeInitState(state);
118
+ console.error(`[init-manager] Started new ${assoc.name} export: ${exportId}`);
119
+ }
120
+ }
121
+ function startPollLoop() {
122
+ if (pollLoopRunning) {
123
+ return;
124
+ }
125
+ pollLoopRunning = true;
126
+ schedulePoll();
127
+ }
128
+ function stopPollLoop() {
129
+ pollLoopRunning = false;
130
+ if (pollLoopTimeout) {
131
+ clearTimeout(pollLoopTimeout);
132
+ pollLoopTimeout = null;
133
+ }
134
+ }
135
+ function schedulePoll() {
136
+ if (!pollLoopRunning)
137
+ return;
138
+ pollLoopTimeout = setTimeout(() => {
139
+ pollOnce().catch((err) => {
140
+ console.error("[init-manager] Poll error:", err);
141
+ });
142
+ }, POLL_INTERVAL_MS);
143
+ }
144
+ async function pollOnce() {
145
+ const state = readInitState();
146
+ if (state.status !== "polling_exports") {
147
+ stopPollLoop();
148
+ return;
149
+ }
150
+ const token = getHubSpotToken();
151
+ if (!token) {
152
+ state.status = "error";
153
+ state.error = "Token no longer available";
154
+ writeInitState(state);
155
+ stopPollLoop();
156
+ return;
157
+ }
158
+ let anyPolling = false;
159
+ for (const exportName of ALL_EXPORTS) {
160
+ const exportState = state.exports[exportName];
161
+ if (exportState.status !== "polling" || !exportState.exportId) {
162
+ continue;
163
+ }
164
+ anyPolling = true;
165
+ try {
166
+ const status = await getExportStatus(token, exportState.exportId);
167
+ if (status.status === "COMPLETE" && status.result) {
168
+ console.error(`[init-manager] ${exportName} export complete, downloading...`);
169
+ const filePath = await downloadExportCsvToFile(token, status.result, exportName);
170
+ console.error(`[init-manager] ${exportName} syncing to database (streaming)...`);
171
+ await syncExportToDb(exportName, filePath);
172
+ exportState.status = "synced";
173
+ writeInitState(state);
174
+ console.error(`[init-manager] ${exportName} synced successfully`);
175
+ }
176
+ else if (status.status === "FAILED") {
177
+ exportState.status = "error";
178
+ exportState.error = status.message || "Export failed";
179
+ writeInitState(state);
180
+ console.error(`[init-manager] ${exportName} export failed:`, status.message);
181
+ }
182
+ }
183
+ catch (err) {
184
+ const errMsg = err instanceof Error ? err.message : String(err);
185
+ console.error(`[init-manager] Error polling ${exportName}:`, errMsg);
186
+ }
187
+ }
188
+ if (isFullySynced(state)) {
189
+ state.status = "ready";
190
+ await setMetadata("last_synced", new Date().toISOString());
191
+ await setMetadata("initialized_at", state.startedAt || new Date().toISOString());
192
+ writeInitState(state);
193
+ stopPollLoop();
194
+ console.error("[init-manager] All exports synced, database ready");
195
+ return;
196
+ }
197
+ const hasErrors = ALL_EXPORTS.some((name) => state.exports[name].status === "error");
198
+ if (hasErrors && !anyPolling) {
199
+ state.status = "error";
200
+ state.error = "One or more exports failed";
201
+ writeInitState(state);
202
+ stopPollLoop();
203
+ console.error("[init-manager] Initialization failed due to export errors");
204
+ return;
205
+ }
206
+ schedulePoll();
207
+ }
208
+ async function syncExportToDb(exportName, filePath) {
209
+ switch (exportName) {
210
+ case "contacts": {
211
+ const count = await loadContactsCsvFromFile(filePath);
212
+ await setMetadata("contacts_count", String(count));
213
+ break;
214
+ }
215
+ case "companies": {
216
+ const count = await loadCompaniesCsvFromFile(filePath);
217
+ await setMetadata("companies_count", String(count));
218
+ break;
219
+ }
220
+ case "deals": {
221
+ const count = await loadDealsCsvFromFile(filePath);
222
+ await setMetadata("deals_count", String(count));
223
+ break;
224
+ }
225
+ case "contact_company":
226
+ case "deal_contact":
227
+ case "deal_company": {
228
+ const count = await loadAssociationsCsvFromFile(filePath, exportName);
229
+ await setMetadata(`${exportName}_count`, String(count));
230
+ break;
231
+ }
232
+ }
233
+ }
234
+ export async function resumeIfNeeded() {
235
+ const state = readInitState();
236
+ if (state.status === "polling_exports") {
237
+ console.error("[init-manager] Resuming poll loop from previous state");
238
+ const syncedCount = getSyncedCount(state);
239
+ const seedingIncomplete = !state.seedingStatus ||
240
+ state.seedingStatus === "pending" ||
241
+ state.seedingStatus === "error";
242
+ if (syncedCount === 0 && seedingIncomplete) {
243
+ const token = getHubSpotToken();
244
+ if (token) {
245
+ console.error("[init-manager] No data yet, re-running quick seed");
246
+ runSeeding(token).catch((err) => {
247
+ console.error("[init-manager] Seeding failed (non-fatal):", err);
248
+ });
249
+ }
250
+ }
251
+ startPollLoop();
252
+ return;
253
+ }
254
+ if (state.status === "sending_requests") {
255
+ console.error("[init-manager] Previous init was interrupted during request phase, restarting");
256
+ await startInitialization(true);
257
+ return;
258
+ }
259
+ if (state.status === "idle" || state.status === "error") {
260
+ console.error("[init-manager] Starting fresh initialization");
261
+ await startInitialization(false);
262
+ return;
263
+ }
264
+ if (state.status === "ready") {
265
+ console.error("[init-manager] Database already initialized");
266
+ return;
267
+ }
268
+ }
269
+ export function forceReinitialize() {
270
+ stopPollLoop();
271
+ startInitialization(true).catch((err) => {
272
+ console.error("[init-manager] Force reinit failed:", err);
273
+ });
274
+ }
@@ -0,0 +1,31 @@
1
+ export type ExportName = "contacts" | "companies" | "deals" | "contact_company" | "deal_contact" | "deal_company";
2
+ export declare const ALL_EXPORTS: ExportName[];
3
+ export type ExportStatus = "pending" | "polling" | "synced" | "error";
4
+ export type SeedingStatus = "pending" | "in_progress" | "completed" | "skipped" | "error";
5
+ export interface SeedingProgress {
6
+ deals: number;
7
+ contacts: number;
8
+ companies: number;
9
+ }
10
+ export interface ExportState {
11
+ exportId: string | null;
12
+ status: ExportStatus;
13
+ error: string | null;
14
+ }
15
+ export type InitStatus = "idle" | "sending_requests" | "polling_exports" | "ready" | "error";
16
+ export interface InitState {
17
+ status: InitStatus;
18
+ exports: Record<ExportName, ExportState>;
19
+ startedAt: string | null;
20
+ error: string | null;
21
+ seedingStatus?: SeedingStatus;
22
+ seedingProgress?: SeedingProgress;
23
+ seedingError?: string;
24
+ }
25
+ export declare function createInitialState(): InitState;
26
+ export declare function readInitState(): InitState;
27
+ export declare function writeInitState(state: InitState): void;
28
+ export declare function resetInitState(): void;
29
+ export declare function getSyncedCount(state: InitState): number;
30
+ export declare function isFullySynced(state: InitState): boolean;
31
+ export declare function getStateFilePath(): string;