@daeda/mcp-pro 0.1.11 → 0.1.12
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/index.js +1721 -890
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
-
import { Effect as
|
|
6
|
+
import { Effect as Effect93, Layer as Layer6, pipe as pipe78 } from "effect";
|
|
7
7
|
|
|
8
8
|
// src/plugins/crm-data.ts
|
|
9
9
|
import { Effect as Effect3, pipe as pipe3 } from "effect";
|
|
@@ -78,6 +78,7 @@ var SyncError = class extends Data.TaggedError("SyncError") {
|
|
|
78
78
|
|
|
79
79
|
// src/pure/csv-parser.ts
|
|
80
80
|
import { unzipSync, gunzipSync } from "fflate";
|
|
81
|
+
import Papa from "papaparse";
|
|
81
82
|
var ZIP_MAGIC = [80, 75, 3, 4];
|
|
82
83
|
var GZIP_MAGIC = [31, 139];
|
|
83
84
|
var hasMagicBytes = (bytes, magic) => bytes.length >= magic.length && magic.every((b, i) => bytes[i] === b);
|
|
@@ -106,74 +107,6 @@ var decompressBytes = (bytes, contentType = "", contentEncoding = "", url = "")
|
|
|
106
107
|
}
|
|
107
108
|
return new TextDecoder().decode(bytes);
|
|
108
109
|
};
|
|
109
|
-
var splitCsvRows = (csv) => {
|
|
110
|
-
const rows = [];
|
|
111
|
-
let current = "";
|
|
112
|
-
let inQuotes = false;
|
|
113
|
-
for (let i = 0; i < csv.length; i++) {
|
|
114
|
-
const char = csv[i];
|
|
115
|
-
if (inQuotes) {
|
|
116
|
-
if (char === '"') {
|
|
117
|
-
if (i + 1 < csv.length && csv[i + 1] === '"') {
|
|
118
|
-
current += '""';
|
|
119
|
-
i++;
|
|
120
|
-
} else {
|
|
121
|
-
inQuotes = false;
|
|
122
|
-
current += char;
|
|
123
|
-
}
|
|
124
|
-
} else {
|
|
125
|
-
current += char;
|
|
126
|
-
}
|
|
127
|
-
} else {
|
|
128
|
-
if (char === '"') {
|
|
129
|
-
inQuotes = true;
|
|
130
|
-
current += char;
|
|
131
|
-
} else if (char === "\r") {
|
|
132
|
-
if (i + 1 < csv.length && csv[i + 1] === "\n") i++;
|
|
133
|
-
if (current.trim().length > 0) rows.push(current);
|
|
134
|
-
current = "";
|
|
135
|
-
} else if (char === "\n") {
|
|
136
|
-
if (current.trim().length > 0) rows.push(current);
|
|
137
|
-
current = "";
|
|
138
|
-
} else {
|
|
139
|
-
current += char;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
if (current.trim().length > 0) rows.push(current);
|
|
144
|
-
return rows;
|
|
145
|
-
};
|
|
146
|
-
var parseCSVLine = (line) => {
|
|
147
|
-
const fields44 = [];
|
|
148
|
-
let current = "";
|
|
149
|
-
let inQuotes = false;
|
|
150
|
-
for (let i = 0; i < line.length; i++) {
|
|
151
|
-
const char = line[i];
|
|
152
|
-
if (inQuotes) {
|
|
153
|
-
if (char === '"') {
|
|
154
|
-
if (i + 1 < line.length && line[i + 1] === '"') {
|
|
155
|
-
current += '"';
|
|
156
|
-
i++;
|
|
157
|
-
} else {
|
|
158
|
-
inQuotes = false;
|
|
159
|
-
}
|
|
160
|
-
} else {
|
|
161
|
-
current += char;
|
|
162
|
-
}
|
|
163
|
-
} else {
|
|
164
|
-
if (char === '"') {
|
|
165
|
-
inQuotes = true;
|
|
166
|
-
} else if (char === ",") {
|
|
167
|
-
fields44.push(current);
|
|
168
|
-
current = "";
|
|
169
|
-
} else {
|
|
170
|
-
current += char;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
fields44.push(current);
|
|
175
|
-
return fields44;
|
|
176
|
-
};
|
|
177
110
|
var EXPORT_NAME_TO_OBJECT_TYPE = {
|
|
178
111
|
CONTACT: "contacts",
|
|
179
112
|
COMPANY: "companies",
|
|
@@ -195,58 +128,43 @@ var normalizeAssociationLabel = (value) => {
|
|
|
195
128
|
if (value === void 0) return null;
|
|
196
129
|
return value.trim().length > 0 ? value : null;
|
|
197
130
|
};
|
|
198
|
-
var
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
const
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
rows.push({
|
|
213
|
-
fromObjectType: from,
|
|
214
|
-
fromId,
|
|
215
|
-
toObjectType: to,
|
|
216
|
-
toId,
|
|
217
|
-
associationType: typeIndex !== -1 ? values[typeIndex] ?? "" : "",
|
|
218
|
-
label: labelIndex !== -1 ? normalizeAssociationLabel(values[labelIndex]) : null
|
|
219
|
-
});
|
|
131
|
+
var isAssociationHeader = (h) => h.startsWith("associations.ids.") || h.startsWith("associations.labels.") || h.startsWith("associations.");
|
|
132
|
+
var parseRow = (csvContent) => Papa.parse(csvContent, {
|
|
133
|
+
header: true,
|
|
134
|
+
skipEmptyLines: true,
|
|
135
|
+
dynamicTyping: false
|
|
136
|
+
});
|
|
137
|
+
var findAssociationMappings = (headers) => {
|
|
138
|
+
const mappings = [];
|
|
139
|
+
for (const h of headers) {
|
|
140
|
+
if (!h.startsWith("associations.ids.")) continue;
|
|
141
|
+
const exportName = h.replace("associations.ids.", "");
|
|
142
|
+
const toObjectType = resolveObjectTypeName(exportName);
|
|
143
|
+
const labelsHeader = headers.find((lh) => lh === `associations.labels.${exportName}`) ?? headers.find((lh) => lh === `associations.${exportName}`) ?? null;
|
|
144
|
+
mappings.push({ toObjectType, idsHeader: h, labelsHeader });
|
|
220
145
|
}
|
|
221
|
-
return
|
|
146
|
+
return mappings;
|
|
222
147
|
};
|
|
223
|
-
var
|
|
224
|
-
const
|
|
225
|
-
(
|
|
226
|
-
);
|
|
227
|
-
if (idIndex === -1) {
|
|
228
|
-
throw new Error(`No id column found in headers: ${headers.join(", ")}`);
|
|
229
|
-
}
|
|
230
|
-
const assocIdsIndex = headers.findIndex((h) => h.startsWith("associations.ids."));
|
|
231
|
-
if (assocIdsIndex === -1) {
|
|
232
|
-
throw new Error(`No associations.ids column found in headers: ${headers.join(", ")}`);
|
|
148
|
+
var findIdField = (headers) => {
|
|
149
|
+
for (const candidate of ["hs_object_id", "Record ID", "id", "ID"]) {
|
|
150
|
+
if (headers.includes(candidate)) return candidate;
|
|
233
151
|
}
|
|
234
|
-
const
|
|
235
|
-
|
|
152
|
+
const lowerMatch = headers.find((h) => h.toLowerCase() === "id");
|
|
153
|
+
if (lowerMatch) return lowerMatch;
|
|
154
|
+
throw new Error(`No id column found in headers: ${headers.join(", ")}`);
|
|
155
|
+
};
|
|
156
|
+
var extractAssociations = (row, fromId, objectType, mappings) => {
|
|
236
157
|
const rows = [];
|
|
237
|
-
for (
|
|
238
|
-
const
|
|
239
|
-
const fromId = values[idIndex] ?? "";
|
|
240
|
-
if (!fromId) continue;
|
|
241
|
-
const idsRaw = values[assocIdsIndex] ?? "";
|
|
158
|
+
for (const mapping of mappings) {
|
|
159
|
+
const idsRaw = row[mapping.idsHeader] ?? "";
|
|
242
160
|
if (!idsRaw) continue;
|
|
243
161
|
const toIds = idsRaw.split(";").map((s) => s.trim()).filter(Boolean);
|
|
244
|
-
const labels =
|
|
162
|
+
const labels = mapping.labelsHeader ? (row[mapping.labelsHeader] ?? "").split(";").map((s) => s.trim()) : [];
|
|
245
163
|
for (let j = 0; j < toIds.length; j++) {
|
|
246
164
|
rows.push({
|
|
247
|
-
fromObjectType:
|
|
165
|
+
fromObjectType: objectType,
|
|
248
166
|
fromId,
|
|
249
|
-
toObjectType:
|
|
167
|
+
toObjectType: mapping.toObjectType,
|
|
250
168
|
toId: toIds[j],
|
|
251
169
|
associationType: "",
|
|
252
170
|
label: normalizeAssociationLabel(labels[j])
|
|
@@ -255,78 +173,85 @@ var parseHubSpotAssociationCsv = (lines, headers, from, to) => {
|
|
|
255
173
|
}
|
|
256
174
|
return rows;
|
|
257
175
|
};
|
|
258
|
-
var findAssociationColumns = (headers) => {
|
|
259
|
-
const columns = [];
|
|
260
|
-
for (let i = 0; i < headers.length; i++) {
|
|
261
|
-
const h = headers[i];
|
|
262
|
-
if (!h.startsWith("associations.ids.")) continue;
|
|
263
|
-
const exportName = h.replace("associations.ids.", "");
|
|
264
|
-
const toObjectType = resolveObjectTypeName(exportName);
|
|
265
|
-
const labelHeader = `associations.labels.${exportName}`;
|
|
266
|
-
const labelsIndex = headers.findIndex((lh) => lh === labelHeader);
|
|
267
|
-
columns.push({ toObjectType, idsIndex: i, labelsIndex });
|
|
268
|
-
}
|
|
269
|
-
return columns;
|
|
270
|
-
};
|
|
271
|
-
var isAssociationHeader = (h) => h.startsWith("associations.ids.") || h.startsWith("associations.labels.");
|
|
272
176
|
var parseMergedExportCsv = (csvContent, objectType) => {
|
|
273
|
-
const
|
|
274
|
-
if (
|
|
275
|
-
const headers =
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
);
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const assocColumns = findAssociationColumns(headers);
|
|
283
|
-
const assocColumnIndices = /* @__PURE__ */ new Set();
|
|
284
|
-
for (const col of assocColumns) {
|
|
285
|
-
assocColumnIndices.add(col.idsIndex);
|
|
286
|
-
if (col.labelsIndex !== -1) assocColumnIndices.add(col.labelsIndex);
|
|
177
|
+
const result = parseRow(csvContent);
|
|
178
|
+
if (result.data.length === 0) return { objects: [], associations: [] };
|
|
179
|
+
const headers = result.meta.fields ?? [];
|
|
180
|
+
const idField = findIdField(headers);
|
|
181
|
+
const assocMappings = findAssociationMappings(headers);
|
|
182
|
+
const assocHeaders = /* @__PURE__ */ new Set();
|
|
183
|
+
for (const m of assocMappings) {
|
|
184
|
+
assocHeaders.add(m.idsHeader);
|
|
185
|
+
if (m.labelsHeader) assocHeaders.add(m.labelsHeader);
|
|
287
186
|
}
|
|
187
|
+
const propertyHeaders = headers.filter(
|
|
188
|
+
(h) => h !== idField && !assocHeaders.has(h) && !isAssociationHeader(h)
|
|
189
|
+
);
|
|
288
190
|
const objects = [];
|
|
289
191
|
const associations = [];
|
|
290
|
-
for (
|
|
291
|
-
const
|
|
292
|
-
const id = values[idIndex] ?? "";
|
|
192
|
+
for (const row of result.data) {
|
|
193
|
+
const id = row[idField] ?? "";
|
|
293
194
|
if (!id) continue;
|
|
294
195
|
const properties = {};
|
|
295
|
-
for (
|
|
296
|
-
|
|
297
|
-
if (assocColumnIndices.has(j)) continue;
|
|
298
|
-
if (isAssociationHeader(headers[j])) continue;
|
|
299
|
-
const key = headers[j];
|
|
300
|
-
const value = values[j];
|
|
196
|
+
for (const key of propertyHeaders) {
|
|
197
|
+
const value = row[key];
|
|
301
198
|
properties[key] = value !== void 0 && value !== "" ? value : null;
|
|
302
199
|
}
|
|
303
200
|
objects.push({ id, properties });
|
|
304
|
-
|
|
305
|
-
const idsRaw = values[col.idsIndex] ?? "";
|
|
306
|
-
if (!idsRaw) continue;
|
|
307
|
-
const toIds = idsRaw.split(";").map((s) => s.trim()).filter(Boolean);
|
|
308
|
-
const labels = col.labelsIndex !== -1 ? (values[col.labelsIndex] ?? "").split(";").map((s) => s.trim()) : [];
|
|
309
|
-
for (let j = 0; j < toIds.length; j++) {
|
|
310
|
-
associations.push({
|
|
311
|
-
fromObjectType: objectType,
|
|
312
|
-
fromId: id,
|
|
313
|
-
toObjectType: col.toObjectType,
|
|
314
|
-
toId: toIds[j],
|
|
315
|
-
associationType: "",
|
|
316
|
-
label: normalizeAssociationLabel(labels[j])
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
}
|
|
201
|
+
associations.push(...extractAssociations(row, id, objectType, assocMappings));
|
|
320
202
|
}
|
|
321
203
|
return { objects, associations };
|
|
322
204
|
};
|
|
323
205
|
var parseAssociationRows = (csvContent, objectType) => {
|
|
324
206
|
const { from, to } = parseAssociationType(objectType);
|
|
325
|
-
const
|
|
326
|
-
if (
|
|
327
|
-
const headers =
|
|
328
|
-
const isSupabaseFormat = headers.
|
|
329
|
-
|
|
207
|
+
const result = parseRow(csvContent);
|
|
208
|
+
if (result.data.length === 0) return [];
|
|
209
|
+
const headers = result.meta.fields ?? [];
|
|
210
|
+
const isSupabaseFormat = headers.includes("fromObjectId");
|
|
211
|
+
if (isSupabaseFormat) {
|
|
212
|
+
const rows2 = [];
|
|
213
|
+
for (const row of result.data) {
|
|
214
|
+
const fromId = row["fromObjectId"] ?? "";
|
|
215
|
+
const toId = row["toObjectId"] ?? "";
|
|
216
|
+
if (!fromId || !toId) continue;
|
|
217
|
+
rows2.push({
|
|
218
|
+
fromObjectType: from,
|
|
219
|
+
fromId,
|
|
220
|
+
toObjectType: to,
|
|
221
|
+
toId,
|
|
222
|
+
associationType: row["associationType"] ?? "",
|
|
223
|
+
label: normalizeAssociationLabel(row["label"])
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return rows2;
|
|
227
|
+
}
|
|
228
|
+
const idField = findIdField(headers);
|
|
229
|
+
const assocIdsHeader = headers.find((h) => h.startsWith("associations.ids."));
|
|
230
|
+
if (!assocIdsHeader) {
|
|
231
|
+
throw new Error(`No associations.ids column found in headers: ${headers.join(", ")}`);
|
|
232
|
+
}
|
|
233
|
+
const exportName = assocIdsHeader.replace("associations.ids.", "");
|
|
234
|
+
const labelHeader = headers.find((h) => h === `associations.labels.${exportName}`) ?? headers.find((h) => h === `associations.${exportName}`) ?? null;
|
|
235
|
+
const rows = [];
|
|
236
|
+
for (const row of result.data) {
|
|
237
|
+
const fromId = row[idField] ?? "";
|
|
238
|
+
if (!fromId) continue;
|
|
239
|
+
const idsRaw = row[assocIdsHeader] ?? "";
|
|
240
|
+
if (!idsRaw) continue;
|
|
241
|
+
const toIds = idsRaw.split(";").map((s) => s.trim()).filter(Boolean);
|
|
242
|
+
const labels = labelHeader ? (row[labelHeader] ?? "").split(";").map((s) => s.trim()) : [];
|
|
243
|
+
for (let j = 0; j < toIds.length; j++) {
|
|
244
|
+
rows.push({
|
|
245
|
+
fromObjectType: from,
|
|
246
|
+
fromId,
|
|
247
|
+
toObjectType: to,
|
|
248
|
+
toId: toIds[j],
|
|
249
|
+
associationType: "",
|
|
250
|
+
label: normalizeAssociationLabel(labels[j])
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return rows;
|
|
330
255
|
};
|
|
331
256
|
|
|
332
257
|
// src/effects/csv-parser.ts
|
|
@@ -386,15 +311,16 @@ import path3 from "path";
|
|
|
386
311
|
// src/paths.ts
|
|
387
312
|
import os2 from "os";
|
|
388
313
|
import path2 from "path";
|
|
389
|
-
var
|
|
390
|
-
var
|
|
391
|
-
var
|
|
314
|
+
var _dataRoot = path2.join(os2.homedir(), ".daeda-mcp");
|
|
315
|
+
var DATA_ROOT = () => _dataRoot;
|
|
316
|
+
var PORTALS_DIR = () => path2.join(_dataRoot, "portals");
|
|
317
|
+
var portalDir = (portalId) => path2.join(PORTALS_DIR(), String(portalId));
|
|
392
318
|
var dbPath = (portalId) => path2.join(portalDir(portalId), "hubspot.duckdb");
|
|
393
|
-
var stateFilePath = () => path2.join(
|
|
319
|
+
var stateFilePath = () => path2.join(_dataRoot, "client_state.json");
|
|
394
320
|
var portalStatePath = (portalId) => path2.join(portalDir(portalId), "portal_state.json");
|
|
395
321
|
var errorsDirPath = (portalId) => path2.join(portalDir(portalId), "errors");
|
|
396
|
-
var CHARTS_DIR = path2.join(
|
|
397
|
-
var chartFilePath = (filename) => path2.join(CHARTS_DIR, filename);
|
|
322
|
+
var CHARTS_DIR = () => path2.join(_dataRoot, "charts");
|
|
323
|
+
var chartFilePath = (filename) => path2.join(CHARTS_DIR(), filename);
|
|
398
324
|
|
|
399
325
|
// src/effects/save-error-csv.ts
|
|
400
326
|
var logStderr = (message) => Effect2.sync(() => console.error(message));
|
|
@@ -1647,7 +1573,7 @@ var ConfigLive = Layer.succeed(
|
|
|
1647
1573
|
),
|
|
1648
1574
|
save: (state) => Effect19.try({
|
|
1649
1575
|
try: () => {
|
|
1650
|
-
fs3.mkdirSync(DATA_ROOT, { recursive: true });
|
|
1576
|
+
fs3.mkdirSync(DATA_ROOT(), { recursive: true });
|
|
1651
1577
|
fs3.writeFileSync(stateFilePath(), JSON.stringify(state, null, 2), "utf-8");
|
|
1652
1578
|
},
|
|
1653
1579
|
catch: (error) => new ConfigWriteError({ path: stateFilePath(), cause: error })
|
|
@@ -2332,103 +2258,113 @@ var PortalStateLive = Layer3.effect(
|
|
|
2332
2258
|
);
|
|
2333
2259
|
|
|
2334
2260
|
// src/layers/SyncLayerLive.ts
|
|
2335
|
-
import { Effect as Effect24, Layer as Layer4,
|
|
2261
|
+
import { Effect as Effect24, Layer as Layer4, pipe as pipe18 } from "effect";
|
|
2336
2262
|
|
|
2337
2263
|
// src/effects/run-full-sync.ts
|
|
2338
2264
|
import { Effect as Effect22, Ref as Ref2, pipe as pipe16 } from "effect";
|
|
2339
2265
|
var logStderr5 = (message) => Effect22.sync(() => console.error(message));
|
|
2340
|
-
var runFullSync = (ws, db, portalState, portalId, artifacts,
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
(
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
)
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2266
|
+
var runFullSync = (ws, db, portalState, portalId, artifacts, objectTypesToSync) => pipe16(
|
|
2267
|
+
Ref2.make({
|
|
2268
|
+
totalArtifacts: 0,
|
|
2269
|
+
completedArtifacts: 0,
|
|
2270
|
+
currentArtifact: null,
|
|
2271
|
+
status: "idle"
|
|
2272
|
+
}),
|
|
2273
|
+
Effect22.flatMap(
|
|
2274
|
+
(progressRef) => pipe16(
|
|
2275
|
+
Effect22.Do,
|
|
2276
|
+
Effect22.let(
|
|
2277
|
+
"objectTypesToSyncSet",
|
|
2278
|
+
() => objectTypesToSync ? new Set(objectTypesToSync) : void 0
|
|
2279
|
+
),
|
|
2280
|
+
Effect22.let(
|
|
2281
|
+
"toSync",
|
|
2282
|
+
({ objectTypesToSyncSet }) => filterUnsyncedArtifacts(artifacts, objectTypesToSyncSet)
|
|
2283
|
+
),
|
|
2284
|
+
Effect22.tap(
|
|
2285
|
+
({ toSync }) => Ref2.set(progressRef, {
|
|
2286
|
+
totalArtifacts: toSync.length,
|
|
2287
|
+
completedArtifacts: 0,
|
|
2288
|
+
currentArtifact: null,
|
|
2289
|
+
status: "syncing"
|
|
2290
|
+
})
|
|
2291
|
+
),
|
|
2292
|
+
Effect22.tap(() => db.initializeDb(portalId)),
|
|
2293
|
+
Effect22.let("artifactCtx", () => ({
|
|
2294
|
+
db,
|
|
2295
|
+
ws,
|
|
2296
|
+
portalState,
|
|
2297
|
+
portalId
|
|
2298
|
+
})),
|
|
2299
|
+
Effect22.tap(
|
|
2300
|
+
({ toSync, artifactCtx }) => Effect22.forEach(
|
|
2301
|
+
toSync,
|
|
2302
|
+
(artifact, idx) => pipe16(
|
|
2303
|
+
Ref2.update(progressRef, (p) => ({
|
|
2304
|
+
...p,
|
|
2305
|
+
currentArtifact: artifact.object_type
|
|
2306
|
+
})),
|
|
2307
|
+
Effect22.tap(
|
|
2308
|
+
() => logStderr5(
|
|
2309
|
+
`[sync] (${idx + 1}/${toSync.length}) Processing: ${artifact.object_type}`
|
|
2310
|
+
)
|
|
2311
|
+
),
|
|
2312
|
+
Effect22.flatMap(() => {
|
|
2313
|
+
const plugin = findArtifactPlugin(artifact.object_type);
|
|
2314
|
+
if (!plugin) {
|
|
2315
|
+
return logStderr5(`[sync] No artifact plugin found for ${artifact.object_type}, skipping`);
|
|
2316
|
+
}
|
|
2317
|
+
return plugin.processArtifact(artifactCtx, artifact);
|
|
2318
|
+
}),
|
|
2319
|
+
Effect22.tap(
|
|
2320
|
+
() => Ref2.update(progressRef, (p) => ({
|
|
2321
|
+
...p,
|
|
2322
|
+
completedArtifacts: p.completedArtifacts + 1
|
|
2323
|
+
}))
|
|
2324
|
+
)
|
|
2325
|
+
),
|
|
2326
|
+
{ concurrency: 1 }
|
|
2390
2327
|
)
|
|
2391
2328
|
),
|
|
2392
|
-
|
|
2329
|
+
Effect22.tap(
|
|
2330
|
+
() => db.setMetadata("last_synced", (/* @__PURE__ */ new Date()).toISOString())
|
|
2331
|
+
),
|
|
2332
|
+
Effect22.tap(({ toSync }) => {
|
|
2333
|
+
const objectTypes = filterObjectTypes(toSync);
|
|
2334
|
+
return Effect22.forEach(
|
|
2335
|
+
objectTypes,
|
|
2336
|
+
(ot) => db.setMetadata(`last_synced:${ot}`, (/* @__PURE__ */ new Date()).toISOString()),
|
|
2337
|
+
{ concurrency: "unbounded" }
|
|
2338
|
+
);
|
|
2339
|
+
}),
|
|
2340
|
+
Effect22.tap(
|
|
2341
|
+
() => db.setMetadata("portal_id", String(portalId))
|
|
2342
|
+
),
|
|
2343
|
+
Effect22.tap(
|
|
2344
|
+
() => portalState.touchSyncedAt(portalId).pipe(Effect22.catchAll(() => Effect22.void))
|
|
2345
|
+
),
|
|
2346
|
+
Effect22.tap(
|
|
2347
|
+
() => Ref2.update(progressRef, (p) => ({
|
|
2348
|
+
...p,
|
|
2349
|
+
currentArtifact: null,
|
|
2350
|
+
status: "complete"
|
|
2351
|
+
}))
|
|
2352
|
+
),
|
|
2353
|
+
Effect22.flatMap(() => Ref2.get(progressRef)),
|
|
2354
|
+
Effect22.tapError(
|
|
2355
|
+
(err) => Ref2.update(progressRef, (p) => ({
|
|
2356
|
+
...p,
|
|
2357
|
+
status: "error",
|
|
2358
|
+
error: String(err)
|
|
2359
|
+
}))
|
|
2360
|
+
),
|
|
2361
|
+
Effect22.mapError(
|
|
2362
|
+
(err) => new SyncError({
|
|
2363
|
+
message: `Full sync failed for portal ${portalId}`,
|
|
2364
|
+
cause: err
|
|
2365
|
+
})
|
|
2366
|
+
)
|
|
2393
2367
|
)
|
|
2394
|
-
),
|
|
2395
|
-
Effect22.tap(
|
|
2396
|
-
() => db.setMetadata("last_synced", (/* @__PURE__ */ new Date()).toISOString())
|
|
2397
|
-
),
|
|
2398
|
-
Effect22.tap(({ toSync }) => {
|
|
2399
|
-
const objectTypes = filterObjectTypes(toSync);
|
|
2400
|
-
return Effect22.forEach(
|
|
2401
|
-
objectTypes,
|
|
2402
|
-
(ot) => db.setMetadata(`last_synced:${ot}`, (/* @__PURE__ */ new Date()).toISOString()),
|
|
2403
|
-
{ concurrency: "unbounded" }
|
|
2404
|
-
);
|
|
2405
|
-
}),
|
|
2406
|
-
Effect22.tap(
|
|
2407
|
-
() => db.setMetadata("portal_id", String(portalId))
|
|
2408
|
-
),
|
|
2409
|
-
Effect22.tap(
|
|
2410
|
-
() => portalState.touchSyncedAt(portalId).pipe(Effect22.catchAll(() => Effect22.void))
|
|
2411
|
-
),
|
|
2412
|
-
Effect22.tap(
|
|
2413
|
-
() => Ref2.update(progressRef, (p) => ({
|
|
2414
|
-
...p,
|
|
2415
|
-
currentArtifact: null,
|
|
2416
|
-
status: "complete"
|
|
2417
|
-
}))
|
|
2418
|
-
),
|
|
2419
|
-
Effect22.flatMap(() => Ref2.get(progressRef)),
|
|
2420
|
-
Effect22.tapError(
|
|
2421
|
-
(err) => Ref2.update(progressRef, (p) => ({
|
|
2422
|
-
...p,
|
|
2423
|
-
status: "error",
|
|
2424
|
-
error: String(err)
|
|
2425
|
-
}))
|
|
2426
|
-
),
|
|
2427
|
-
Effect22.mapError(
|
|
2428
|
-
(err) => new SyncError({
|
|
2429
|
-
message: `Full sync failed for portal ${portalId}`,
|
|
2430
|
-
cause: err
|
|
2431
|
-
})
|
|
2432
2368
|
)
|
|
2433
2369
|
);
|
|
2434
2370
|
|
|
@@ -2591,14 +2527,8 @@ var SyncLive = Layer4.effect(
|
|
|
2591
2527
|
const ws = yield* WebSocketService;
|
|
2592
2528
|
const db = yield* DatabaseService;
|
|
2593
2529
|
const portalState = yield* PortalStateService;
|
|
2594
|
-
const progressRef = yield* Ref3.make({
|
|
2595
|
-
totalArtifacts: 0,
|
|
2596
|
-
completedArtifacts: 0,
|
|
2597
|
-
currentArtifact: null,
|
|
2598
|
-
status: "idle"
|
|
2599
|
-
});
|
|
2600
2530
|
return {
|
|
2601
|
-
syncArtifacts: (portalId, artifacts, objectTypesToSync) => runFullSync(ws, db, portalState, portalId, artifacts,
|
|
2531
|
+
syncArtifacts: (portalId, artifacts, objectTypesToSync) => runFullSync(ws, db, portalState, portalId, artifacts, objectTypesToSync),
|
|
2602
2532
|
syncMessagePlugins: (portalId) => pipe18(
|
|
2603
2533
|
syncAllMessagePlugins(ws, db, portalState, portalId),
|
|
2604
2534
|
Effect24.mapError(
|
|
@@ -2607,66 +2537,52 @@ var SyncLive = Layer4.effect(
|
|
|
2607
2537
|
cause: err
|
|
2608
2538
|
})
|
|
2609
2539
|
)
|
|
2610
|
-
)
|
|
2611
|
-
getProgress: () => Ref3.get(progressRef)
|
|
2540
|
+
)
|
|
2612
2541
|
};
|
|
2613
2542
|
})
|
|
2614
2543
|
);
|
|
2615
2544
|
|
|
2616
2545
|
// src/layers/WebSocketLayerLive.ts
|
|
2617
|
-
import { Effect as Effect36, Layer as Layer5, Match, Ref as
|
|
2546
|
+
import { Effect as Effect36, Layer as Layer5, Match, Ref as Ref14, Schedule, Duration, pipe as pipe30 } from "effect";
|
|
2618
2547
|
|
|
2619
2548
|
// src/handlers/handle-register-response.ts
|
|
2620
|
-
import { Effect as Effect25, Ref as
|
|
2549
|
+
import { Effect as Effect25, Ref as Ref3, pipe as pipe19 } from "effect";
|
|
2621
2550
|
var handleRegisterResponse = (payload, ctx) => !payload.success || !payload.target_portals ? Effect25.sync(
|
|
2622
2551
|
() => console.error(`Registration failed: ${payload.error ?? "unknown"}`)
|
|
2623
2552
|
) : pipe19(
|
|
2624
|
-
|
|
2553
|
+
Ref3.set(ctx.portalsRef, payload.target_portals),
|
|
2625
2554
|
Effect25.flatMap(
|
|
2626
|
-
() => payload.encryption_key ?
|
|
2555
|
+
() => payload.encryption_key ? Ref3.set(ctx.encryptionKeyRef, payload.encryption_key) : Effect25.void
|
|
2627
2556
|
),
|
|
2628
|
-
Effect25.flatMap(() =>
|
|
2557
|
+
Effect25.flatMap(() => Ref3.get(ctx.handlersRef)),
|
|
2629
2558
|
Effect25.flatMap(
|
|
2630
2559
|
(handlers) => Effect25.sync(() => {
|
|
2631
2560
|
const portals = payload.target_portals;
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
Effect25.runFork(
|
|
2635
|
-
pipe19(
|
|
2636
|
-
Ref4.set(ctx.currentSyncPortalRef, selectedPortalId),
|
|
2637
|
-
Effect25.flatMap(() => ctx.requireWs),
|
|
2638
|
-
Effect25.flatMap((ws) => ctx.sendJson(ws, "sync:full", { target_portal: selectedPortalId })),
|
|
2639
|
-
Effect25.catchAll(() => Effect25.void)
|
|
2640
|
-
)
|
|
2641
|
-
);
|
|
2642
|
-
} else {
|
|
2643
|
-
handlers?.onRegisterSuccess(portals);
|
|
2644
|
-
}
|
|
2561
|
+
handlers?.autoSelectSinglePortal(portals);
|
|
2562
|
+
handlers?.onRegisterSuccess(portals);
|
|
2645
2563
|
})
|
|
2646
2564
|
)
|
|
2647
2565
|
);
|
|
2648
2566
|
|
|
2649
2567
|
// src/handlers/handle-sync-full-response.ts
|
|
2650
|
-
import { Effect as Effect26, Ref as
|
|
2568
|
+
import { Effect as Effect26, Ref as Ref4, pipe as pipe20 } from "effect";
|
|
2651
2569
|
var handleSyncFullResponse = (payload, ctx) => !payload.success ? Effect26.sync(() => console.error(`Sync failed: ${payload.error ?? "unknown"}`)) : pipe20(
|
|
2652
2570
|
Effect26.sync(() => console.error(`[sync:full] Received ${payload.artifacts.length} artifacts`)),
|
|
2653
2571
|
Effect26.flatMap(() => {
|
|
2654
2572
|
const artifacts = payload.artifacts.map((a) => ({ ...a, status: "NOT_STARTED" }));
|
|
2655
2573
|
return pipe20(
|
|
2656
|
-
|
|
2657
|
-
Effect26.flatMap(
|
|
2658
|
-
Effect26.
|
|
2659
|
-
|
|
2660
|
-
)
|
|
2661
|
-
))
|
|
2574
|
+
Ref4.get(ctx.handlersRef),
|
|
2575
|
+
Effect26.flatMap(
|
|
2576
|
+
(handlers) => handlers?.onSyncArtifacts ? Effect26.sync(() => handlers.onSyncArtifacts(payload.target_portal, artifacts)) : Effect26.void
|
|
2577
|
+
)
|
|
2662
2578
|
);
|
|
2663
2579
|
})
|
|
2664
2580
|
);
|
|
2665
2581
|
|
|
2666
2582
|
// src/handlers/handle-sync-artifact-response.ts
|
|
2667
|
-
import { Effect as Effect27, Ref as
|
|
2583
|
+
import { Effect as Effect27, Ref as Ref5, pipe as pipe21 } from "effect";
|
|
2668
2584
|
var handleSyncArtifactResponse = (payload, ctx) => pipe21(
|
|
2669
|
-
|
|
2585
|
+
Ref5.getAndSet(ctx.pendingArtifactRef, null),
|
|
2670
2586
|
Effect27.flatMap(
|
|
2671
2587
|
(pending) => pending !== null ? Effect27.sync(() => pending.resolve(payload)) : Effect27.void
|
|
2672
2588
|
),
|
|
@@ -2678,21 +2594,21 @@ var handleSyncArtifactResponse = (payload, ctx) => pipe21(
|
|
|
2678
2594
|
);
|
|
2679
2595
|
|
|
2680
2596
|
// src/handlers/handle-new-artifacts.ts
|
|
2681
|
-
import { Effect as Effect28, Ref as
|
|
2597
|
+
import { Effect as Effect28, Ref as Ref6, pipe as pipe22 } from "effect";
|
|
2682
2598
|
var handleNewArtifacts = (payload, ctx) => pipe22(
|
|
2683
2599
|
Effect28.sync(
|
|
2684
2600
|
() => console.error(
|
|
2685
2601
|
`[sync:new-artifacts] Received ${payload.artifacts.length} new artifact(s) for portal ${payload.target_portal}: ${payload.artifacts.map((a) => a.object_type).join(", ")}`
|
|
2686
2602
|
)
|
|
2687
2603
|
),
|
|
2688
|
-
Effect28.flatMap(() =>
|
|
2604
|
+
Effect28.flatMap(() => Ref6.get(ctx.handlersRef)),
|
|
2689
2605
|
Effect28.flatMap(
|
|
2690
2606
|
(handlers) => handlers?.onNewArtifacts ? Effect28.sync(() => handlers.onNewArtifacts(payload.target_portal, payload.artifacts)) : Effect28.void
|
|
2691
2607
|
)
|
|
2692
2608
|
);
|
|
2693
2609
|
|
|
2694
2610
|
// src/handlers/handle-diff-batch.ts
|
|
2695
|
-
import { Effect as Effect29, Ref as
|
|
2611
|
+
import { Effect as Effect29, Ref as Ref7, pipe as pipe23 } from "effect";
|
|
2696
2612
|
var handleDiffBatch = (payload, ctx) => !payload.success ? Effect29.sync(
|
|
2697
2613
|
() => console.error(`Diff batch failed: ${payload.error ?? "unknown"}`)
|
|
2698
2614
|
) : pipe23(
|
|
@@ -2701,46 +2617,42 @@ var handleDiffBatch = (payload, ctx) => !payload.success ? Effect29.sync(
|
|
|
2701
2617
|
`[sync:diff:batch] ${payload.object_type}: ${payload.records.length} records, ${payload.associations.length} associations (server_time: ${payload.server_time})${payload.truncated ? " (TRUNCATED)" : ""}`
|
|
2702
2618
|
)
|
|
2703
2619
|
),
|
|
2704
|
-
Effect29.flatMap(() =>
|
|
2705
|
-
Effect29.flatMap(
|
|
2706
|
-
Effect29.
|
|
2707
|
-
|
|
2708
|
-
)
|
|
2709
|
-
))
|
|
2620
|
+
Effect29.flatMap(() => Ref7.get(ctx.handlersRef)),
|
|
2621
|
+
Effect29.flatMap(
|
|
2622
|
+
(handlers) => handlers?.onDiffBatch ? Effect29.sync(() => handlers.onDiffBatch(payload.target_portal, payload)) : Effect29.void
|
|
2623
|
+
)
|
|
2710
2624
|
);
|
|
2711
2625
|
|
|
2712
2626
|
// src/handlers/handle-diff-complete.ts
|
|
2713
|
-
import { Effect as Effect30, Ref as
|
|
2627
|
+
import { Effect as Effect30, Ref as Ref8, pipe as pipe24 } from "effect";
|
|
2714
2628
|
var handleDiffComplete = (payload, ctx) => pipe24(
|
|
2715
2629
|
Effect30.sync(
|
|
2716
2630
|
() => console.error(
|
|
2717
2631
|
`[sync:diff:complete] ${payload.object_types_processed.length} types, ${payload.total_records} records, ${payload.total_associations} associations`
|
|
2718
2632
|
)
|
|
2719
2633
|
),
|
|
2720
|
-
Effect30.flatMap(() =>
|
|
2721
|
-
Effect30.flatMap(
|
|
2722
|
-
Effect30.
|
|
2723
|
-
|
|
2724
|
-
)
|
|
2725
|
-
))
|
|
2634
|
+
Effect30.flatMap(() => Ref8.get(ctx.handlersRef)),
|
|
2635
|
+
Effect30.flatMap(
|
|
2636
|
+
(handlers) => handlers?.onDiffComplete ? Effect30.sync(() => handlers.onDiffComplete(payload.target_portal, payload)) : Effect30.void
|
|
2637
|
+
)
|
|
2726
2638
|
);
|
|
2727
2639
|
|
|
2728
2640
|
// src/handlers/handle-subscribe-ack.ts
|
|
2729
|
-
import { Effect as Effect31, Ref as
|
|
2641
|
+
import { Effect as Effect31, Ref as Ref9, pipe as pipe25 } from "effect";
|
|
2730
2642
|
var handleSubscribeAck = (payload, ctx) => pipe25(
|
|
2731
2643
|
Effect31.sync(
|
|
2732
2644
|
() => console.error(
|
|
2733
2645
|
`[sync:subscribe:ack] Portal ${payload.target_portal}: ${payload.success ? "subscribed" : payload.error}`
|
|
2734
2646
|
)
|
|
2735
2647
|
),
|
|
2736
|
-
Effect31.flatMap(() =>
|
|
2648
|
+
Effect31.flatMap(() => Ref9.get(ctx.handlersRef)),
|
|
2737
2649
|
Effect31.flatMap(
|
|
2738
2650
|
(handlers) => handlers?.onSubscribeAck ? Effect31.sync(() => handlers.onSubscribeAck(payload)) : Effect31.void
|
|
2739
2651
|
)
|
|
2740
2652
|
);
|
|
2741
2653
|
|
|
2742
2654
|
// src/handlers/handle-sync-update.ts
|
|
2743
|
-
import { Effect as Effect32, Ref as
|
|
2655
|
+
import { Effect as Effect32, Ref as Ref10, pipe as pipe26 } from "effect";
|
|
2744
2656
|
var handleSyncUpdate = (payload, ctx) => !payload.success ? Effect32.sync(
|
|
2745
2657
|
() => console.error(`Sync update failed: ${payload.error ?? "unknown"}`)
|
|
2746
2658
|
) : pipe26(
|
|
@@ -2749,36 +2661,34 @@ var handleSyncUpdate = (payload, ctx) => !payload.success ? Effect32.sync(
|
|
|
2749
2661
|
`[sync:update] ${payload.object_type}: ${payload.records.length} records, ${payload.associations.length} associations (server_time: ${payload.server_time})`
|
|
2750
2662
|
)
|
|
2751
2663
|
),
|
|
2752
|
-
Effect32.flatMap(() =>
|
|
2753
|
-
Effect32.flatMap(
|
|
2754
|
-
Effect32.
|
|
2755
|
-
|
|
2756
|
-
)
|
|
2757
|
-
))
|
|
2664
|
+
Effect32.flatMap(() => Ref10.get(ctx.handlersRef)),
|
|
2665
|
+
Effect32.flatMap(
|
|
2666
|
+
(handlers) => handlers?.onDiffBatch ? Effect32.sync(() => handlers.onDiffBatch(payload.target_portal, payload)) : Effect32.void
|
|
2667
|
+
)
|
|
2758
2668
|
);
|
|
2759
2669
|
|
|
2760
2670
|
// src/handlers/handle-plan-create-response.ts
|
|
2761
|
-
import { Effect as Effect33, Ref as
|
|
2671
|
+
import { Effect as Effect33, Ref as Ref11, pipe as pipe27 } from "effect";
|
|
2762
2672
|
var handlePlanCreateResponse = (payload, ctx) => pipe27(
|
|
2763
|
-
|
|
2673
|
+
Ref11.getAndSet(ctx.pendingPlanCreateRef, null),
|
|
2764
2674
|
Effect33.flatMap(
|
|
2765
2675
|
(pending) => pending ? Effect33.sync(() => pending.resolve(payload)) : Effect33.sync(() => console.error("[plan:create] No pending request for response"))
|
|
2766
2676
|
)
|
|
2767
2677
|
);
|
|
2768
2678
|
|
|
2769
2679
|
// src/handlers/handle-plan-list-response.ts
|
|
2770
|
-
import { Effect as Effect34, Ref as
|
|
2680
|
+
import { Effect as Effect34, Ref as Ref12, pipe as pipe28 } from "effect";
|
|
2771
2681
|
var handlePlanListResponse = (payload, ctx) => pipe28(
|
|
2772
|
-
|
|
2682
|
+
Ref12.getAndSet(ctx.pendingPlanListRef, null),
|
|
2773
2683
|
Effect34.flatMap(
|
|
2774
2684
|
(pending) => pending ? Effect34.sync(() => pending.resolve(payload)) : Effect34.sync(() => console.error("[plan:list] No pending request for response"))
|
|
2775
2685
|
)
|
|
2776
2686
|
);
|
|
2777
2687
|
|
|
2778
2688
|
// src/handlers/handle-plugin-data-response.ts
|
|
2779
|
-
import { Effect as Effect35, Ref as
|
|
2689
|
+
import { Effect as Effect35, Ref as Ref13, pipe as pipe29 } from "effect";
|
|
2780
2690
|
var handlePluginDataResponse = (payload, ctx) => pipe29(
|
|
2781
|
-
|
|
2691
|
+
Ref13.modify(ctx.pendingPluginRequestsRef, (pendingMap) => {
|
|
2782
2692
|
const next = new Map(pendingMap);
|
|
2783
2693
|
const pending = next.get(payload.request_id);
|
|
2784
2694
|
next.delete(payload.request_id);
|
|
@@ -2812,20 +2722,18 @@ var WS_URL = process.env.websocket_url ?? "wss://mcp-ws.daeda.tech";
|
|
|
2812
2722
|
var WebSocketLive = Layer5.effect(
|
|
2813
2723
|
WebSocketService,
|
|
2814
2724
|
Effect36.gen(function* () {
|
|
2815
|
-
const stateRef = yield*
|
|
2816
|
-
const portalsRef = yield*
|
|
2817
|
-
const wsRef = yield*
|
|
2818
|
-
const
|
|
2819
|
-
const
|
|
2820
|
-
const
|
|
2821
|
-
const
|
|
2822
|
-
const
|
|
2823
|
-
const
|
|
2824
|
-
const
|
|
2825
|
-
const handlersRef = yield* Ref15.make(null);
|
|
2826
|
-
const encryptionKeyRef = yield* Ref15.make(null);
|
|
2725
|
+
const stateRef = yield* Ref14.make("disconnected");
|
|
2726
|
+
const portalsRef = yield* Ref14.make([]);
|
|
2727
|
+
const wsRef = yield* Ref14.make(null);
|
|
2728
|
+
const isReconnectingRef = yield* Ref14.make(false);
|
|
2729
|
+
const pendingArtifactRef = yield* Ref14.make(null);
|
|
2730
|
+
const pendingPluginRequestsRef = yield* Ref14.make(/* @__PURE__ */ new Map());
|
|
2731
|
+
const pendingPlanCreateRef = yield* Ref14.make(null);
|
|
2732
|
+
const pendingPlanListRef = yield* Ref14.make(null);
|
|
2733
|
+
const handlersRef = yield* Ref14.make(null);
|
|
2734
|
+
const encryptionKeyRef = yield* Ref14.make(null);
|
|
2827
2735
|
const requireWs = pipe30(
|
|
2828
|
-
|
|
2736
|
+
Ref14.get(wsRef),
|
|
2829
2737
|
Effect36.flatMap(
|
|
2830
2738
|
(ws) => ws === null ? Effect36.fail(new WebSocketError({ url: WS_URL, cause: new Error("WebSocket not connected") })) : Effect36.succeed(ws)
|
|
2831
2739
|
)
|
|
@@ -2833,8 +2741,6 @@ var WebSocketLive = Layer5.effect(
|
|
|
2833
2741
|
const sendJson = (ws, type, payload) => Effect36.sync(() => ws.send(JSON.stringify({ type, payload })));
|
|
2834
2742
|
const ctx = {
|
|
2835
2743
|
portalsRef,
|
|
2836
|
-
syncedArtifactsRef,
|
|
2837
|
-
currentSyncPortalRef,
|
|
2838
2744
|
pendingArtifactRef,
|
|
2839
2745
|
pendingPluginRequestsRef,
|
|
2840
2746
|
pendingPlanCreateRef,
|
|
@@ -2875,13 +2781,13 @@ var WebSocketLive = Layer5.effect(
|
|
|
2875
2781
|
});
|
|
2876
2782
|
const wireCloseHandler = (ws) => Effect36.sync(() => {
|
|
2877
2783
|
ws.onclose = () => {
|
|
2878
|
-
const pending = Effect36.runSync(
|
|
2784
|
+
const pending = Effect36.runSync(Ref14.getAndSet(pendingArtifactRef, null));
|
|
2879
2785
|
if (pending) {
|
|
2880
2786
|
pending.reject(new WebSocketError({ url: WS_URL, cause: new Error("WebSocket closed") }));
|
|
2881
2787
|
}
|
|
2882
2788
|
pipe30(
|
|
2883
|
-
|
|
2884
|
-
Effect36.flatMap(() =>
|
|
2789
|
+
Ref14.set(stateRef, "disconnected"),
|
|
2790
|
+
Effect36.flatMap(() => Ref14.set(wsRef, null)),
|
|
2885
2791
|
Effect36.flatMap(() => scheduleReconnect()),
|
|
2886
2792
|
Effect36.runFork
|
|
2887
2793
|
);
|
|
@@ -2889,7 +2795,7 @@ var WebSocketLive = Layer5.effect(
|
|
|
2889
2795
|
});
|
|
2890
2796
|
const connectInternal = () => pipe30(
|
|
2891
2797
|
Effect36.sync(() => new WebSocket(WS_URL)),
|
|
2892
|
-
Effect36.tap((ws) =>
|
|
2798
|
+
Effect36.tap((ws) => Ref14.set(wsRef, ws)),
|
|
2893
2799
|
Effect36.tap((ws) => wireMessageHandler(ws)),
|
|
2894
2800
|
Effect36.tap((ws) => wireCloseHandler(ws)),
|
|
2895
2801
|
Effect36.flatMap(
|
|
@@ -2897,8 +2803,8 @@ var WebSocketLive = Layer5.effect(
|
|
|
2897
2803
|
ws.onopen = () => {
|
|
2898
2804
|
console.error(`[ws] Connected to ${WS_URL}, sending register`);
|
|
2899
2805
|
pipe30(
|
|
2900
|
-
|
|
2901
|
-
Effect36.flatMap(() =>
|
|
2806
|
+
Ref14.set(stateRef, "connected"),
|
|
2807
|
+
Effect36.flatMap(() => Ref14.set(isReconnectingRef, false)),
|
|
2902
2808
|
Effect36.flatMap(() => sendJson(ws, "register", { license_key: LICENSE_KEY })),
|
|
2903
2809
|
Effect36.runFork
|
|
2904
2810
|
);
|
|
@@ -2911,7 +2817,7 @@ var WebSocketLive = Layer5.effect(
|
|
|
2911
2817
|
)
|
|
2912
2818
|
);
|
|
2913
2819
|
const scheduleReconnect = () => pipe30(
|
|
2914
|
-
|
|
2820
|
+
Ref14.getAndSet(isReconnectingRef, true),
|
|
2915
2821
|
Effect36.flatMap(
|
|
2916
2822
|
(wasAlready) => wasAlready ? Effect36.void : pipe30(
|
|
2917
2823
|
Effect36.sync(() => console.error(`[ws] Scheduling reconnect to ${WS_URL}`)),
|
|
@@ -2926,21 +2832,19 @@ var WebSocketLive = Layer5.effect(
|
|
|
2926
2832
|
)
|
|
2927
2833
|
);
|
|
2928
2834
|
const connect = () => pipe30(
|
|
2929
|
-
|
|
2835
|
+
Ref14.getAndSet(isReconnectingRef, true),
|
|
2930
2836
|
Effect36.flatMap(
|
|
2931
2837
|
(wasAlready) => wasAlready ? Effect36.void : pipe30(connectInternal(), Effect36.retry(Schedule.fixed(Duration.seconds(3))))
|
|
2932
2838
|
)
|
|
2933
2839
|
);
|
|
2934
2840
|
const send = (data) => pipe30(requireWs, Effect36.flatMap((ws) => Effect36.sync(() => ws.send(JSON.stringify(data)))));
|
|
2935
2841
|
const sendSyncFull = (portalId) => pipe30(
|
|
2936
|
-
|
|
2937
|
-
Effect36.flatMap(() => requireWs),
|
|
2842
|
+
requireWs,
|
|
2938
2843
|
Effect36.tap(() => Effect36.sync(() => console.error(`[sync:full] Sending request for portal ${portalId}`))),
|
|
2939
2844
|
Effect36.flatMap((ws) => sendJson(ws, "sync:full", { target_portal: portalId }))
|
|
2940
2845
|
);
|
|
2941
2846
|
const sendSyncDiff = (portalId, lastSyncTimes) => pipe30(
|
|
2942
|
-
|
|
2943
|
-
Effect36.flatMap(() => requireWs),
|
|
2847
|
+
requireWs,
|
|
2944
2848
|
Effect36.tap(() => Effect36.sync(() => console.error(`[sync:diff] Sending request for portal ${portalId}`))),
|
|
2945
2849
|
Effect36.flatMap((ws) => sendJson(ws, "sync:diff", { target_portal: portalId, last_sync_times: lastSyncTimes }))
|
|
2946
2850
|
);
|
|
@@ -2949,17 +2853,21 @@ var WebSocketLive = Layer5.effect(
|
|
|
2949
2853
|
Effect36.tap(() => Effect36.sync(() => console.error(`[sync:subscribe] Sending request for portal ${portalId}`))),
|
|
2950
2854
|
Effect36.flatMap((ws) => sendJson(ws, "sync:subscribe", { target_portal: portalId }))
|
|
2951
2855
|
);
|
|
2856
|
+
const sendSyncUnsubscribe = (portalId) => pipe30(
|
|
2857
|
+
requireWs,
|
|
2858
|
+
Effect36.tap(() => Effect36.sync(() => console.error(`[sync:unsubscribe] Sending request for portal ${portalId}`))),
|
|
2859
|
+
Effect36.flatMap((ws) => sendJson(ws, "sync:unsubscribe", { target_portal: portalId }))
|
|
2860
|
+
);
|
|
2952
2861
|
return {
|
|
2953
2862
|
connect,
|
|
2954
2863
|
send,
|
|
2955
2864
|
sendSyncFull,
|
|
2956
2865
|
sendSyncDiff,
|
|
2957
2866
|
sendSyncSubscribe,
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
setSyncedArtifacts: (syncedArtifacts) => Ref15.set(syncedArtifactsRef, syncedArtifacts),
|
|
2867
|
+
sendSyncUnsubscribe,
|
|
2868
|
+
getState: () => Ref14.get(stateRef),
|
|
2869
|
+
getPortals: () => Ref14.get(portalsRef),
|
|
2870
|
+
setPortals: (portals) => Ref14.set(portalsRef, portals),
|
|
2963
2871
|
requestArtifactUrl: (artifactId) => pipe30(
|
|
2964
2872
|
requireWs,
|
|
2965
2873
|
Effect36.flatMap(
|
|
@@ -2968,7 +2876,7 @@ var WebSocketLive = Layer5.effect(
|
|
|
2968
2876
|
resolve: (payload) => resume(Effect36.succeed(payload)),
|
|
2969
2877
|
reject: (error) => resume(Effect36.fail(error))
|
|
2970
2878
|
};
|
|
2971
|
-
Effect36.runSync(
|
|
2879
|
+
Effect36.runSync(Ref14.set(pendingArtifactRef, pending));
|
|
2972
2880
|
ws.send(JSON.stringify({ type: "sync:artifact", payload: { artifact_id: artifactId } }));
|
|
2973
2881
|
})
|
|
2974
2882
|
),
|
|
@@ -2987,10 +2895,10 @@ var WebSocketLive = Layer5.effect(
|
|
|
2987
2895
|
resolve: (payload) => resume(Effect36.succeed(payload)),
|
|
2988
2896
|
reject: (error) => resume(Effect36.fail(error))
|
|
2989
2897
|
};
|
|
2990
|
-
const pendingMap = Effect36.runSync(
|
|
2898
|
+
const pendingMap = Effect36.runSync(Ref14.get(pendingPluginRequestsRef));
|
|
2991
2899
|
const next = new Map(pendingMap);
|
|
2992
2900
|
next.set(requestId, pending);
|
|
2993
|
-
Effect36.runSync(
|
|
2901
|
+
Effect36.runSync(Ref14.set(pendingPluginRequestsRef, next));
|
|
2994
2902
|
ws.send(JSON.stringify({
|
|
2995
2903
|
type: "sync:plugin-data",
|
|
2996
2904
|
payload: {
|
|
@@ -3008,7 +2916,7 @@ var WebSocketLive = Layer5.effect(
|
|
|
3008
2916
|
})
|
|
3009
2917
|
}),
|
|
3010
2918
|
Effect36.tapError(
|
|
3011
|
-
() =>
|
|
2919
|
+
() => Ref14.update(pendingPluginRequestsRef, (pendingMap) => {
|
|
3012
2920
|
const next = new Map(pendingMap);
|
|
3013
2921
|
next.delete(requestId);
|
|
3014
2922
|
return next;
|
|
@@ -3025,7 +2933,7 @@ var WebSocketLive = Layer5.effect(
|
|
|
3025
2933
|
resolve: (payload) => resume(Effect36.succeed(payload)),
|
|
3026
2934
|
reject: (error) => resume(Effect36.fail(error))
|
|
3027
2935
|
};
|
|
3028
|
-
Effect36.runSync(
|
|
2936
|
+
Effect36.runSync(Ref14.set(pendingPlanCreateRef, pending));
|
|
3029
2937
|
ws.send(JSON.stringify({ type: "plan:create", payload: { target_portal: portalId, ...plan.dry_run ? { dry_run: true } : {}, plan } }));
|
|
3030
2938
|
})
|
|
3031
2939
|
),
|
|
@@ -3042,7 +2950,7 @@ var WebSocketLive = Layer5.effect(
|
|
|
3042
2950
|
resolve: (payload) => resume(Effect36.succeed(payload)),
|
|
3043
2951
|
reject: (error) => resume(Effect36.fail(error))
|
|
3044
2952
|
};
|
|
3045
|
-
Effect36.runSync(
|
|
2953
|
+
Effect36.runSync(Ref14.set(pendingPlanListRef, pending));
|
|
3046
2954
|
ws.send(JSON.stringify({ type: "plan:list", payload: { target_portal: portalId, ...status && { status } } }));
|
|
3047
2955
|
})
|
|
3048
2956
|
),
|
|
@@ -3051,89 +2959,33 @@ var WebSocketLive = Layer5.effect(
|
|
|
3051
2959
|
onTimeout: () => new WebSocketError({ url: WS_URL, cause: new Error("Plan list request timed out") })
|
|
3052
2960
|
})
|
|
3053
2961
|
),
|
|
3054
|
-
registerHandlers: (handlers) =>
|
|
3055
|
-
getEncryptionKey: () =>
|
|
2962
|
+
registerHandlers: (handlers) => Ref14.set(handlersRef, handlers),
|
|
2963
|
+
getEncryptionKey: () => Ref14.get(encryptionKeyRef)
|
|
3056
2964
|
};
|
|
3057
2965
|
})
|
|
3058
2966
|
);
|
|
3059
2967
|
|
|
3060
2968
|
// src/tools/portal_selection.ts
|
|
3061
2969
|
import { z as z12 } from "zod";
|
|
3062
|
-
import { Effect as Effect38, pipe as pipe32 } from "effect";
|
|
3063
|
-
|
|
3064
|
-
// src/effects/read-connection.ts
|
|
3065
2970
|
import { Effect as Effect37, pipe as pipe31 } from "effect";
|
|
3066
|
-
import { DuckDBInstance as DuckDBInstance2 } from "@duckdb/node-api";
|
|
3067
|
-
import fs6 from "fs";
|
|
3068
|
-
var handles = /* @__PURE__ */ new Map();
|
|
3069
|
-
var getReadConnection = (portalId, encryptionKey) => pipe31(
|
|
3070
|
-
Effect37.sync(() => handles.get(portalId)),
|
|
3071
|
-
Effect37.flatMap(
|
|
3072
|
-
(existing) => existing ? Effect37.succeed(existing.conn) : pipe31(
|
|
3073
|
-
Effect37.sync(() => dbPath(portalId)),
|
|
3074
|
-
Effect37.filterOrFail(
|
|
3075
|
-
(dbFile) => fs6.existsSync(dbFile),
|
|
3076
|
-
() => new DatabaseError({
|
|
3077
|
-
message: `Database not found for portal ${portalId}. Has it been synced?`
|
|
3078
|
-
})
|
|
3079
|
-
),
|
|
3080
|
-
Effect37.flatMap(
|
|
3081
|
-
(dbFile) => Effect37.tryPromise({
|
|
3082
|
-
try: async () => {
|
|
3083
|
-
const instance = await DuckDBInstance2.create(":memory:");
|
|
3084
|
-
const conn = await instance.connect();
|
|
3085
|
-
const escapedPath = dbFile.replace(/'/g, "''");
|
|
3086
|
-
if (encryptionKey) {
|
|
3087
|
-
const escapedKey = encryptionKey.replace(/'/g, "''");
|
|
3088
|
-
await conn.run(`ATTACH '${escapedPath}' AS portal_db (ENCRYPTION_KEY '${escapedKey}', READ_ONLY)`);
|
|
3089
|
-
} else {
|
|
3090
|
-
await conn.run(`ATTACH '${escapedPath}' AS portal_db (READ_ONLY)`);
|
|
3091
|
-
}
|
|
3092
|
-
await conn.run(`USE portal_db`);
|
|
3093
|
-
handles.set(portalId, { conn, instance });
|
|
3094
|
-
return conn;
|
|
3095
|
-
},
|
|
3096
|
-
catch: (e) => new DatabaseError({
|
|
3097
|
-
message: `Failed to open read connection for portal ${portalId}`,
|
|
3098
|
-
cause: e
|
|
3099
|
-
})
|
|
3100
|
-
})
|
|
3101
|
-
)
|
|
3102
|
-
)
|
|
3103
|
-
)
|
|
3104
|
-
);
|
|
3105
|
-
var closeAllReadConnections = () => Effect37.sync(() => {
|
|
3106
|
-
for (const [portalId] of handles) {
|
|
3107
|
-
const handle = handles.get(portalId);
|
|
3108
|
-
if (!handle) continue;
|
|
3109
|
-
try {
|
|
3110
|
-
handle.conn.closeSync();
|
|
3111
|
-
handle.instance.closeSync();
|
|
3112
|
-
} catch {
|
|
3113
|
-
}
|
|
3114
|
-
handles.delete(portalId);
|
|
3115
|
-
}
|
|
3116
|
-
});
|
|
3117
|
-
|
|
3118
|
-
// src/tools/portal_selection.ts
|
|
3119
2971
|
function getSelectedPortalId(config) {
|
|
3120
|
-
return
|
|
3121
|
-
config.load().pipe(
|
|
2972
|
+
return Effect37.runSync(
|
|
2973
|
+
config.load().pipe(Effect37.map((state) => state.selectedPortalId))
|
|
3122
2974
|
);
|
|
3123
2975
|
}
|
|
3124
2976
|
function setSelectedPortalId(config, portalId) {
|
|
3125
|
-
|
|
2977
|
+
Effect37.runSync(config.save({ selectedPortalId: portalId }));
|
|
3126
2978
|
}
|
|
3127
2979
|
function registerPortalSelection(server2, deps) {
|
|
3128
|
-
const { ws, config } = deps;
|
|
2980
|
+
const { ws, config, handoffSelectedPortal } = deps;
|
|
3129
2981
|
server2.registerPrompt(
|
|
3130
2982
|
"select-portal",
|
|
3131
2983
|
{
|
|
3132
2984
|
description: "Show available HubSpot portals and select one to work with"
|
|
3133
2985
|
},
|
|
3134
2986
|
async () => {
|
|
3135
|
-
const targetPortals =
|
|
3136
|
-
const
|
|
2987
|
+
const targetPortals = Effect37.runSync(ws.getPortals());
|
|
2988
|
+
const selectedPortalId2 = getSelectedPortalId(config);
|
|
3137
2989
|
if (targetPortals.length === 0) {
|
|
3138
2990
|
return {
|
|
3139
2991
|
messages: [{
|
|
@@ -3142,7 +2994,7 @@ function registerPortalSelection(server2, deps) {
|
|
|
3142
2994
|
}]
|
|
3143
2995
|
};
|
|
3144
2996
|
}
|
|
3145
|
-
const currentSelection =
|
|
2997
|
+
const currentSelection = selectedPortalId2 ? targetPortals.find((p) => p.target_portal === selectedPortalId2) : null;
|
|
3146
2998
|
const portalList = targetPortals.map((p) => `- **${p.target_name ?? "Unnamed"}** (ID: ${p.target_portal})`).join("\n");
|
|
3147
2999
|
const statusText = currentSelection ? `Currently selected: **${currentSelection.target_name ?? "Unnamed"}** (${currentSelection.target_portal})` : "No portal currently selected.";
|
|
3148
3000
|
return {
|
|
@@ -3165,13 +3017,15 @@ Which portal would you like to use? Use the \`set_portal\` tool to select one.`
|
|
|
3165
3017
|
server2.registerTool(
|
|
3166
3018
|
"set_portal",
|
|
3167
3019
|
{
|
|
3168
|
-
description:
|
|
3020
|
+
description: `Select which HubSpot portal (account) to work with. Sets the default portal used by tools when portalIds is not explicitly provided. This also triggers a full artifact sync and real-time subscription for the selected portal.
|
|
3021
|
+
|
|
3022
|
+
Other tools accept an optional portalIds parameter to target specific portals regardless of this selection. All registered portals are kept warm with background data syncing.`,
|
|
3169
3023
|
inputSchema: {
|
|
3170
3024
|
portalId: z12.number().describe("The portal ID to select")
|
|
3171
3025
|
}
|
|
3172
3026
|
},
|
|
3173
3027
|
async ({ portalId }) => {
|
|
3174
|
-
const targetPortals =
|
|
3028
|
+
const targetPortals = Effect37.runSync(ws.getPortals());
|
|
3175
3029
|
const portal = targetPortals.find((p) => p.target_portal === portalId);
|
|
3176
3030
|
if (!portal) {
|
|
3177
3031
|
const available = targetPortals.map((p) => p.target_portal).join(", ");
|
|
@@ -3183,9 +3037,12 @@ Which portal would you like to use? Use the \`set_portal\` tool to select one.`
|
|
|
3183
3037
|
isError: true
|
|
3184
3038
|
};
|
|
3185
3039
|
}
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3040
|
+
if (handoffSelectedPortal) {
|
|
3041
|
+
handoffSelectedPortal(portalId);
|
|
3042
|
+
} else {
|
|
3043
|
+
setSelectedPortalId(config, portalId);
|
|
3044
|
+
Effect37.runFork(pipe31(ws.sendSyncFull(portalId), Effect37.catchAll(() => Effect37.void)));
|
|
3045
|
+
}
|
|
3189
3046
|
return {
|
|
3190
3047
|
content: [{
|
|
3191
3048
|
type: "text",
|
|
@@ -3208,8 +3065,139 @@ function autoSelectSinglePortal(config, portals) {
|
|
|
3208
3065
|
// src/tools/query.ts
|
|
3209
3066
|
import { z as z13 } from "zod";
|
|
3210
3067
|
import { Effect as Effect39 } from "effect";
|
|
3068
|
+
|
|
3069
|
+
// src/effects/read-connection.ts
|
|
3070
|
+
import { Effect as Effect38, pipe as pipe32 } from "effect";
|
|
3071
|
+
import { DuckDBInstance as DuckDBInstance2 } from "@duckdb/node-api";
|
|
3072
|
+
import fs6 from "fs";
|
|
3073
|
+
var handles = /* @__PURE__ */ new Map();
|
|
3074
|
+
var closeHandle = (portalId) => {
|
|
3075
|
+
const handle = handles.get(portalId);
|
|
3076
|
+
if (!handle) return;
|
|
3077
|
+
try {
|
|
3078
|
+
handle.conn.closeSync();
|
|
3079
|
+
handle.instance.closeSync();
|
|
3080
|
+
} catch {
|
|
3081
|
+
}
|
|
3082
|
+
handles.delete(portalId);
|
|
3083
|
+
};
|
|
3084
|
+
var getFreshnessToken = (portalId, dbFile) => Effect38.try({
|
|
3085
|
+
try: () => {
|
|
3086
|
+
const stat = fs6.statSync(dbFile);
|
|
3087
|
+
return `${stat.mtimeMs}:${stat.size}`;
|
|
3088
|
+
},
|
|
3089
|
+
catch: (e) => new DatabaseError({
|
|
3090
|
+
message: `Failed to read database freshness for portal ${portalId}`,
|
|
3091
|
+
cause: e
|
|
3092
|
+
})
|
|
3093
|
+
});
|
|
3094
|
+
var openReadHandle = (portalId, dbFile, encryptionKey, freshnessToken) => Effect38.tryPromise({
|
|
3095
|
+
try: async () => {
|
|
3096
|
+
const instance = await DuckDBInstance2.create(":memory:");
|
|
3097
|
+
const conn = await instance.connect();
|
|
3098
|
+
const escapedPath = dbFile.replace(/'/g, "''");
|
|
3099
|
+
if (encryptionKey) {
|
|
3100
|
+
const escapedKey = encryptionKey.replace(/'/g, "''");
|
|
3101
|
+
await conn.run(`ATTACH '${escapedPath}' AS portal_db (ENCRYPTION_KEY '${escapedKey}', READ_ONLY)`);
|
|
3102
|
+
} else {
|
|
3103
|
+
await conn.run(`ATTACH '${escapedPath}' AS portal_db (READ_ONLY)`);
|
|
3104
|
+
}
|
|
3105
|
+
await conn.run("USE portal_db");
|
|
3106
|
+
handles.set(portalId, { conn, instance, freshnessToken });
|
|
3107
|
+
return conn;
|
|
3108
|
+
},
|
|
3109
|
+
catch: (e) => new DatabaseError({
|
|
3110
|
+
message: `Failed to open read connection for portal ${portalId}`,
|
|
3111
|
+
cause: e
|
|
3112
|
+
})
|
|
3113
|
+
});
|
|
3114
|
+
var getReadConnection = (portalId, encryptionKey) => pipe32(
|
|
3115
|
+
Effect38.Do,
|
|
3116
|
+
Effect38.let("dbFile", () => dbPath(portalId)),
|
|
3117
|
+
Effect38.filterOrFail(
|
|
3118
|
+
({ dbFile }) => fs6.existsSync(dbFile),
|
|
3119
|
+
() => new DatabaseError({
|
|
3120
|
+
message: `Database not found for portal ${portalId}. Has it been synced?`
|
|
3121
|
+
})
|
|
3122
|
+
),
|
|
3123
|
+
Effect38.bind("freshnessToken", ({ dbFile }) => getFreshnessToken(portalId, dbFile)),
|
|
3124
|
+
Effect38.tap(
|
|
3125
|
+
({ freshnessToken }) => Effect38.sync(() => {
|
|
3126
|
+
const existing = handles.get(portalId);
|
|
3127
|
+
if (!existing || existing.freshnessToken === freshnessToken) return;
|
|
3128
|
+
closeHandle(portalId);
|
|
3129
|
+
})
|
|
3130
|
+
),
|
|
3131
|
+
Effect38.flatMap(
|
|
3132
|
+
({ dbFile, freshnessToken }) => pipe32(
|
|
3133
|
+
Effect38.sync(() => handles.get(portalId)),
|
|
3134
|
+
Effect38.flatMap(
|
|
3135
|
+
(existing) => existing ? Effect38.succeed(existing.conn) : openReadHandle(portalId, dbFile, encryptionKey, freshnessToken)
|
|
3136
|
+
)
|
|
3137
|
+
)
|
|
3138
|
+
)
|
|
3139
|
+
);
|
|
3140
|
+
|
|
3141
|
+
// src/pure/resolve-portal-ids.ts
|
|
3142
|
+
var dedupePortalIds = (portalIds) => {
|
|
3143
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3144
|
+
const deduped = [];
|
|
3145
|
+
for (const portalId of portalIds) {
|
|
3146
|
+
if (seen.has(portalId)) continue;
|
|
3147
|
+
seen.add(portalId);
|
|
3148
|
+
deduped.push(portalId);
|
|
3149
|
+
}
|
|
3150
|
+
return deduped;
|
|
3151
|
+
};
|
|
3152
|
+
var resolvePortalIds = (explicitPortalIds, selectedPortalId2, portals) => {
|
|
3153
|
+
if (explicitPortalIds !== void 0) {
|
|
3154
|
+
if (explicitPortalIds.length === 0) {
|
|
3155
|
+
return { error: "portalIds must include at least one portal ID." };
|
|
3156
|
+
}
|
|
3157
|
+
const deduped = dedupePortalIds(explicitPortalIds);
|
|
3158
|
+
const availableIds = new Set(portals.map((portal) => portal.target_portal));
|
|
3159
|
+
const invalidPortalIds = deduped.filter((portalId) => !availableIds.has(portalId));
|
|
3160
|
+
if (invalidPortalIds.length > 0) {
|
|
3161
|
+
return {
|
|
3162
|
+
error: `Portal ID(s) not available: ${invalidPortalIds.join(", ")}`
|
|
3163
|
+
};
|
|
3164
|
+
}
|
|
3165
|
+
return {
|
|
3166
|
+
portalIds: deduped,
|
|
3167
|
+
explicit: true
|
|
3168
|
+
};
|
|
3169
|
+
}
|
|
3170
|
+
if (selectedPortalId2 === null) {
|
|
3171
|
+
return {
|
|
3172
|
+
error: "No portal selected. Use the set_portal tool first or pass portalIds."
|
|
3173
|
+
};
|
|
3174
|
+
}
|
|
3175
|
+
return {
|
|
3176
|
+
portalIds: [selectedPortalId2],
|
|
3177
|
+
explicit: false
|
|
3178
|
+
};
|
|
3179
|
+
};
|
|
3180
|
+
|
|
3181
|
+
// src/tools/query.ts
|
|
3211
3182
|
var MAX_ROWS = 200;
|
|
3212
3183
|
var hasLimitClause = (sql) => /\bLIMIT\s+\d+/i.test(sql);
|
|
3184
|
+
var runQueryForPortal = async (portalId, trimmedSql, finalSql, getEncryptionKey) => {
|
|
3185
|
+
const conn = await Effect39.runPromise(getReadConnection(portalId, getEncryptionKey()));
|
|
3186
|
+
const start = performance.now();
|
|
3187
|
+
const reader = await conn.runAndReadAll(finalSql);
|
|
3188
|
+
const rows = reader.getRowObjects();
|
|
3189
|
+
const elapsed = (performance.now() - start).toFixed(1);
|
|
3190
|
+
return {
|
|
3191
|
+
rows,
|
|
3192
|
+
rowCount: rows.length,
|
|
3193
|
+
executionTimeMs: elapsed,
|
|
3194
|
+
...rows.length >= MAX_ROWS && !hasLimitClause(trimmedSql) ? {
|
|
3195
|
+
truncated: true,
|
|
3196
|
+
note: `Results capped at ${MAX_ROWS} rows. Add your own LIMIT clause for different limits.`
|
|
3197
|
+
} : {}
|
|
3198
|
+
};
|
|
3199
|
+
};
|
|
3200
|
+
var stringifyResult = (value) => JSON.stringify(value, (_, inner) => typeof inner === "bigint" ? Number(inner) : inner, 2);
|
|
3213
3201
|
function registerQueryTool(server2, deps) {
|
|
3214
3202
|
const schemaDocs = [
|
|
3215
3203
|
'- The "sync_metadata" table has: key TEXT, value TEXT',
|
|
@@ -3230,6 +3218,10 @@ Use this to answer questions about HubSpot data such as:
|
|
|
3230
3218
|
|
|
3231
3219
|
Only SELECT statements are allowed. Results are capped at ${MAX_ROWS} rows.
|
|
3232
3220
|
|
|
3221
|
+
Optional: pass portalIds to run the same query across one or more specific portals. When portalIds is provided, the result is always a JSON object keyed by portal ID, where each value is the query result or an error.
|
|
3222
|
+
|
|
3223
|
+
Data freshness: each portal's data is automatically checked for freshness before querying. If new artifacts have been synced since the last query, they are ingested first. Results reflect the latest synced state.
|
|
3224
|
+
|
|
3233
3225
|
IMPORTANT: Always use the 'status' tool with section "schema" first to discover available object types and property names before writing queries. Do not guess property names.
|
|
3234
3226
|
|
|
3235
3227
|
The database schema:
|
|
@@ -3240,14 +3232,15 @@ Example queries:
|
|
|
3240
3232
|
- SELECT from_object_type, to_object_type, COUNT(*) as cnt FROM associations GROUP BY 1, 2 ORDER BY cnt DESC
|
|
3241
3233
|
- SELECT id, json_extract_string(properties, '$.dealname') as name, json_extract_string(properties, '$.amount') as amount FROM deals WHERE json_extract_string(properties, '$.dealstage') = 'closedwon' LIMIT 20`,
|
|
3242
3234
|
inputSchema: {
|
|
3243
|
-
sql: z13.string().describe("A SELECT SQL query to run against the DuckDB database")
|
|
3235
|
+
sql: z13.string().describe("A SELECT SQL query to run against the DuckDB database"),
|
|
3236
|
+
portalIds: z13.array(z13.number()).optional().describe("Optional explicit portal IDs to query. If provided, results are keyed by portal ID.")
|
|
3244
3237
|
}
|
|
3245
3238
|
},
|
|
3246
|
-
async ({ sql }) => {
|
|
3247
|
-
const
|
|
3248
|
-
if (
|
|
3239
|
+
async ({ sql, portalIds }) => {
|
|
3240
|
+
const portalResolution = resolvePortalIds(portalIds, deps.getSelectedPortalId(), deps.getPortals());
|
|
3241
|
+
if ("error" in portalResolution) {
|
|
3249
3242
|
return {
|
|
3250
|
-
content: [{ type: "text", text:
|
|
3243
|
+
content: [{ type: "text", text: portalResolution.error }],
|
|
3251
3244
|
isError: true
|
|
3252
3245
|
};
|
|
3253
3246
|
}
|
|
@@ -3259,28 +3252,48 @@ Example queries:
|
|
|
3259
3252
|
};
|
|
3260
3253
|
}
|
|
3261
3254
|
const finalSql = hasLimitClause(trimmed) ? trimmed : `${trimmed} LIMIT ${MAX_ROWS}`;
|
|
3262
|
-
|
|
3263
|
-
const
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3255
|
+
if (!portalResolution.explicit) {
|
|
3256
|
+
const [portalId] = portalResolution.portalIds;
|
|
3257
|
+
try {
|
|
3258
|
+
await deps.ensureFresh(portalId);
|
|
3259
|
+
const result = await runQueryForPortal(
|
|
3260
|
+
portalId,
|
|
3261
|
+
trimmed,
|
|
3262
|
+
finalSql,
|
|
3263
|
+
deps.getEncryptionKey
|
|
3264
|
+
);
|
|
3265
|
+
return {
|
|
3266
|
+
content: [{ type: "text", text: stringifyResult(result) }]
|
|
3267
|
+
};
|
|
3268
|
+
} catch (e) {
|
|
3269
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
3270
|
+
return {
|
|
3271
|
+
content: [{ type: "text", text: `Query error: ${message}` }],
|
|
3272
|
+
isError: true
|
|
3273
|
+
};
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
const keyedResults = {};
|
|
3277
|
+
let hadAnyErrors = false;
|
|
3278
|
+
for (const portalId of portalResolution.portalIds) {
|
|
3279
|
+
try {
|
|
3280
|
+
await deps.ensureFresh(portalId);
|
|
3281
|
+
keyedResults[String(portalId)] = await runQueryForPortal(
|
|
3282
|
+
portalId,
|
|
3283
|
+
trimmed,
|
|
3284
|
+
finalSql,
|
|
3285
|
+
deps.getEncryptionKey
|
|
3286
|
+
);
|
|
3287
|
+
} catch (e) {
|
|
3288
|
+
hadAnyErrors = true;
|
|
3289
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
3290
|
+
keyedResults[String(portalId)] = { error: message };
|
|
3291
|
+
}
|
|
3283
3292
|
}
|
|
3293
|
+
return {
|
|
3294
|
+
content: [{ type: "text", text: stringifyResult(keyedResults) }],
|
|
3295
|
+
...hadAnyErrors ? { isError: true } : {}
|
|
3296
|
+
};
|
|
3284
3297
|
}
|
|
3285
3298
|
);
|
|
3286
3299
|
}
|
|
@@ -3327,8 +3340,8 @@ function mapQueryResultsToChartData(rows, columns) {
|
|
|
3327
3340
|
}));
|
|
3328
3341
|
return { labels, datasets };
|
|
3329
3342
|
}
|
|
3330
|
-
function generateChartHtml(
|
|
3331
|
-
const { chartType, data, title, sql } =
|
|
3343
|
+
function generateChartHtml(options2) {
|
|
3344
|
+
const { chartType, data, title, sql } = options2;
|
|
3332
3345
|
const datasets = data.datasets.map((ds, i) => ({
|
|
3333
3346
|
label: ds.label,
|
|
3334
3347
|
data: ds.data,
|
|
@@ -3490,7 +3503,64 @@ function openInBrowser(target) {
|
|
|
3490
3503
|
// src/tools/chart.ts
|
|
3491
3504
|
var MAX_ROWS2 = 200;
|
|
3492
3505
|
var hasLimitClause2 = (sql) => /\bLIMIT\s+\d+/i.test(sql);
|
|
3506
|
+
var runQueryForPortal2 = async (portalId, sql, getEncryptionKey) => {
|
|
3507
|
+
const conn = await Effect40.runPromise(getReadConnection(portalId, getEncryptionKey()));
|
|
3508
|
+
const start = performance.now();
|
|
3509
|
+
const reader = await conn.runAndReadAll(sql);
|
|
3510
|
+
const rows = reader.getRowObjects();
|
|
3511
|
+
const elapsed = (performance.now() - start).toFixed(1);
|
|
3512
|
+
const columns = rows.length > 0 ? Object.keys(rows[0]) : [];
|
|
3513
|
+
return {
|
|
3514
|
+
portalId,
|
|
3515
|
+
rows,
|
|
3516
|
+
columns,
|
|
3517
|
+
executionTimeMs: elapsed
|
|
3518
|
+
};
|
|
3519
|
+
};
|
|
3520
|
+
var buildMergedPortalChartData = (portalRows) => {
|
|
3521
|
+
const allLabels = [];
|
|
3522
|
+
const seenLabels = /* @__PURE__ */ new Set();
|
|
3523
|
+
for (const entry of portalRows) {
|
|
3524
|
+
const labelColumn = entry.columns[0];
|
|
3525
|
+
for (const row of entry.rows) {
|
|
3526
|
+
const label = String(row[labelColumn] ?? "");
|
|
3527
|
+
if (seenLabels.has(label)) continue;
|
|
3528
|
+
seenLabels.add(label);
|
|
3529
|
+
allLabels.push(label);
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
3532
|
+
const datasets = [];
|
|
3533
|
+
for (const entry of portalRows) {
|
|
3534
|
+
const labelColumn = entry.columns[0];
|
|
3535
|
+
const dataColumns = entry.columns.slice(1);
|
|
3536
|
+
for (const dataColumn of dataColumns) {
|
|
3537
|
+
const valuesByLabel = /* @__PURE__ */ new Map();
|
|
3538
|
+
for (const row of entry.rows) {
|
|
3539
|
+
const label = String(row[labelColumn] ?? "");
|
|
3540
|
+
const value = Number(row[dataColumn]);
|
|
3541
|
+
valuesByLabel.set(label, Number.isFinite(value) ? value : 0);
|
|
3542
|
+
}
|
|
3543
|
+
datasets.push({
|
|
3544
|
+
label: `Portal ${entry.portalId} - ${dataColumn}`,
|
|
3545
|
+
data: allLabels.map((label) => valuesByLabel.get(label) ?? 0)
|
|
3546
|
+
});
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
return {
|
|
3550
|
+
labels: allLabels,
|
|
3551
|
+
datasets
|
|
3552
|
+
};
|
|
3553
|
+
};
|
|
3554
|
+
var writeChartFile = (chartType, html) => {
|
|
3555
|
+
mkdirSync4(CHARTS_DIR(), { recursive: true });
|
|
3556
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3557
|
+
const filename = `${timestamp}-${chartType}.html`;
|
|
3558
|
+
const filePath = chartFilePath(filename);
|
|
3559
|
+
writeFileSync4(filePath, html, "utf-8");
|
|
3560
|
+
return filePath;
|
|
3561
|
+
};
|
|
3493
3562
|
function registerChartTool(server2, deps) {
|
|
3563
|
+
const openChartFile = deps.openChartFile ?? openInBrowser;
|
|
3494
3564
|
server2.registerTool(
|
|
3495
3565
|
"chart",
|
|
3496
3566
|
{
|
|
@@ -3504,6 +3574,10 @@ Use this when the user wants to visualize HubSpot data, such as:
|
|
|
3504
3574
|
|
|
3505
3575
|
Only SELECT statements are allowed. Results are capped at ${MAX_ROWS2} rows.
|
|
3506
3576
|
|
|
3577
|
+
Optional: pass portalIds to run the same chart query across one or more specific portals. When portalIds is provided, results include a JSON object keyed by portal ID with per-portal summaries, and chart datasets are labeled by portal.
|
|
3578
|
+
|
|
3579
|
+
Data freshness: each portal's data is automatically checked for freshness before charting. Results reflect the latest synced state.
|
|
3580
|
+
|
|
3507
3581
|
The query results map to the chart as follows:
|
|
3508
3582
|
- First column \u2192 X-axis labels (categories)
|
|
3509
3583
|
- Remaining columns \u2192 one dataset per column (Y-axis values, must be numeric)
|
|
@@ -3519,14 +3593,15 @@ SELECT json_extract_string(properties, '$.dealstage') as stage, COUNT(*) as coun
|
|
|
3519
3593
|
inputSchema: {
|
|
3520
3594
|
sql: z14.string().describe("A SELECT SQL query. First column = labels, remaining columns = numeric datasets."),
|
|
3521
3595
|
chart_type: z14.enum(["bar", "line"]).describe("The chart type to render."),
|
|
3522
|
-
title: z14.string().optional().describe("Chart title. Auto-generated from chart type if omitted.")
|
|
3596
|
+
title: z14.string().optional().describe("Chart title. Auto-generated from chart type if omitted."),
|
|
3597
|
+
portalIds: z14.array(z14.number()).optional().describe("Optional explicit portal IDs to chart. If provided, output includes keyed portal results.")
|
|
3523
3598
|
}
|
|
3524
3599
|
},
|
|
3525
|
-
async ({ sql, chart_type, title }) => {
|
|
3526
|
-
const
|
|
3527
|
-
if (
|
|
3600
|
+
async ({ sql, chart_type, title, portalIds }) => {
|
|
3601
|
+
const portalResolution = resolvePortalIds(portalIds, deps.getSelectedPortalId(), deps.getPortals());
|
|
3602
|
+
if ("error" in portalResolution) {
|
|
3528
3603
|
return {
|
|
3529
|
-
content: [{ type: "text", text:
|
|
3604
|
+
content: [{ type: "text", text: portalResolution.error }],
|
|
3530
3605
|
isError: true
|
|
3531
3606
|
};
|
|
3532
3607
|
}
|
|
@@ -3538,55 +3613,120 @@ SELECT json_extract_string(properties, '$.dealstage') as stage, COUNT(*) as coun
|
|
|
3538
3613
|
};
|
|
3539
3614
|
}
|
|
3540
3615
|
const finalSql = hasLimitClause2(trimmed) ? trimmed : `${trimmed} LIMIT ${MAX_ROWS2}`;
|
|
3541
|
-
|
|
3542
|
-
const
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3616
|
+
if (!portalResolution.explicit) {
|
|
3617
|
+
const [portalId] = portalResolution.portalIds;
|
|
3618
|
+
try {
|
|
3619
|
+
await deps.ensureFresh(portalId);
|
|
3620
|
+
const result = await runQueryForPortal2(portalId, finalSql, deps.getEncryptionKey);
|
|
3621
|
+
if (result.rows.length === 0) {
|
|
3622
|
+
return {
|
|
3623
|
+
content: [{ type: "text", text: "Query returned no rows - nothing to chart." }],
|
|
3624
|
+
isError: true
|
|
3625
|
+
};
|
|
3626
|
+
}
|
|
3627
|
+
if (result.columns.length < 2) {
|
|
3628
|
+
return {
|
|
3629
|
+
content: [{
|
|
3630
|
+
type: "text",
|
|
3631
|
+
text: "Chart requires at least 2 columns: one for labels (x-axis) and one for values (y-axis). Your query returned only 1 column."
|
|
3632
|
+
}],
|
|
3633
|
+
isError: true
|
|
3634
|
+
};
|
|
3635
|
+
}
|
|
3636
|
+
const chartData = mapQueryResultsToChartData(result.rows, result.columns);
|
|
3637
|
+
const chartTitle = title ?? `${chart_type === "bar" ? "Bar" : "Line"} Chart - ${result.columns.slice(1).join(", ")} by ${result.columns[0]}`;
|
|
3638
|
+
const html = generateChartHtml({
|
|
3639
|
+
chartType: chart_type,
|
|
3640
|
+
data: chartData,
|
|
3641
|
+
title: chartTitle,
|
|
3642
|
+
sql: trimmed
|
|
3643
|
+
});
|
|
3644
|
+
const filePath = writeChartFile(chart_type, html);
|
|
3645
|
+
openChartFile(filePath);
|
|
3646
|
+
const dataColumns = result.columns.slice(1);
|
|
3647
|
+
const summary = [
|
|
3648
|
+
`Chart generated and opened in browser.`,
|
|
3649
|
+
``,
|
|
3650
|
+
`Type: ${chart_type}`,
|
|
3651
|
+
`Title: ${chartTitle}`,
|
|
3652
|
+
`Labels (x-axis): ${result.columns[0]} (${result.rows.length} values)`,
|
|
3653
|
+
`Datasets: ${dataColumns.join(", ")}`,
|
|
3654
|
+
`Query time: ${result.executionTimeMs}ms`,
|
|
3655
|
+
`File: ${filePath}`
|
|
3656
|
+
].join("\n");
|
|
3657
|
+
return {
|
|
3658
|
+
content: [{ type: "text", text: summary }]
|
|
3551
3659
|
};
|
|
3552
|
-
}
|
|
3553
|
-
|
|
3554
|
-
if (columns.length < 2) {
|
|
3660
|
+
} catch (e) {
|
|
3661
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
3555
3662
|
return {
|
|
3556
|
-
content: [{
|
|
3557
|
-
type: "text",
|
|
3558
|
-
text: "Chart requires at least 2 columns: one for labels (x-axis) and one for values (y-axis). Your query returned only 1 column."
|
|
3559
|
-
}],
|
|
3663
|
+
content: [{ type: "text", text: `Chart error: ${message}` }],
|
|
3560
3664
|
isError: true
|
|
3561
3665
|
};
|
|
3562
3666
|
}
|
|
3563
|
-
|
|
3564
|
-
|
|
3667
|
+
}
|
|
3668
|
+
const keyedPortalResults = {};
|
|
3669
|
+
const successfulPortals = [];
|
|
3670
|
+
let hadErrors = false;
|
|
3671
|
+
for (const portalId of portalResolution.portalIds) {
|
|
3672
|
+
try {
|
|
3673
|
+
await deps.ensureFresh(portalId);
|
|
3674
|
+
const result = await runQueryForPortal2(portalId, finalSql, deps.getEncryptionKey);
|
|
3675
|
+
if (result.rows.length === 0) {
|
|
3676
|
+
hadErrors = true;
|
|
3677
|
+
keyedPortalResults[String(portalId)] = { error: "Query returned no rows - nothing to chart." };
|
|
3678
|
+
continue;
|
|
3679
|
+
}
|
|
3680
|
+
if (result.columns.length < 2) {
|
|
3681
|
+
hadErrors = true;
|
|
3682
|
+
keyedPortalResults[String(portalId)] = {
|
|
3683
|
+
error: "Chart requires at least 2 columns: one for labels (x-axis) and one for values (y-axis)."
|
|
3684
|
+
};
|
|
3685
|
+
continue;
|
|
3686
|
+
}
|
|
3687
|
+
successfulPortals.push(result);
|
|
3688
|
+
keyedPortalResults[String(portalId)] = {
|
|
3689
|
+
rowCount: result.rows.length,
|
|
3690
|
+
executionTimeMs: result.executionTimeMs,
|
|
3691
|
+
labelColumn: result.columns[0],
|
|
3692
|
+
datasetColumns: result.columns.slice(1)
|
|
3693
|
+
};
|
|
3694
|
+
} catch (e) {
|
|
3695
|
+
hadErrors = true;
|
|
3696
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
3697
|
+
keyedPortalResults[String(portalId)] = { error: message };
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
if (successfulPortals.length === 0) {
|
|
3701
|
+
return {
|
|
3702
|
+
content: [{ type: "text", text: JSON.stringify(keyedPortalResults, null, 2) }],
|
|
3703
|
+
isError: true
|
|
3704
|
+
};
|
|
3705
|
+
}
|
|
3706
|
+
try {
|
|
3707
|
+
const chartData = buildMergedPortalChartData(successfulPortals);
|
|
3708
|
+
const chartTitle = title ?? `${chart_type === "bar" ? "Bar" : "Line"} Chart - Portal Comparison`;
|
|
3565
3709
|
const html = generateChartHtml({
|
|
3566
3710
|
chartType: chart_type,
|
|
3567
3711
|
data: chartData,
|
|
3568
3712
|
title: chartTitle,
|
|
3569
3713
|
sql: trimmed
|
|
3570
3714
|
});
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
const
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
`Labels (x-axis): ${columns[0]} (${rows.length} values)`,
|
|
3584
|
-
`Datasets: ${dataColumns.join(", ")}`,
|
|
3585
|
-
`Query time: ${elapsed}ms`,
|
|
3586
|
-
`File: ${filePath}`
|
|
3587
|
-
].join("\n");
|
|
3715
|
+
const filePath = writeChartFile(chart_type, html);
|
|
3716
|
+
openChartFile(filePath);
|
|
3717
|
+
const response = {
|
|
3718
|
+
chart: {
|
|
3719
|
+
type: chart_type,
|
|
3720
|
+
title: chartTitle,
|
|
3721
|
+
labels: chartData.labels.length,
|
|
3722
|
+
datasets: chartData.datasets.length,
|
|
3723
|
+
file: filePath
|
|
3724
|
+
},
|
|
3725
|
+
portals: keyedPortalResults
|
|
3726
|
+
};
|
|
3588
3727
|
return {
|
|
3589
|
-
content: [{ type: "text", text:
|
|
3728
|
+
content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
|
|
3729
|
+
...hadErrors ? { isError: true } : {}
|
|
3590
3730
|
};
|
|
3591
3731
|
} catch (e) {
|
|
3592
3732
|
const message = e instanceof Error ? e.message : String(e);
|
|
@@ -3616,25 +3756,21 @@ function getDbFileSize(portalId) {
|
|
|
3616
3756
|
return fs7.statSync(db).size;
|
|
3617
3757
|
}
|
|
3618
3758
|
async function buildConnectionSection(deps) {
|
|
3619
|
-
const
|
|
3759
|
+
const selectedPortalId2 = deps.getSelectedPortalId();
|
|
3620
3760
|
const connectionState = deps.getConnectionState();
|
|
3621
3761
|
const portals = deps.getPortals();
|
|
3622
3762
|
return {
|
|
3623
3763
|
connectionState,
|
|
3624
|
-
selectedPortalId,
|
|
3764
|
+
selectedPortalId: selectedPortalId2,
|
|
3625
3765
|
portals: portals.map((p) => ({
|
|
3626
3766
|
portalId: p.target_portal,
|
|
3627
3767
|
name: p.target_name,
|
|
3628
|
-
selected: p.target_portal ===
|
|
3768
|
+
selected: p.target_portal === selectedPortalId2
|
|
3629
3769
|
})),
|
|
3630
3770
|
totalPortals: portals.length
|
|
3631
3771
|
};
|
|
3632
3772
|
}
|
|
3633
|
-
async function buildSchemaSection(
|
|
3634
|
-
const portalId = getSelectedPortalId2();
|
|
3635
|
-
if (portalId === null) {
|
|
3636
|
-
return { error: "No portal selected. Use the set_portal tool first." };
|
|
3637
|
-
}
|
|
3773
|
+
async function buildSchemaSection(portalId, getEncryptionKey) {
|
|
3638
3774
|
const conn = await Effect41.runPromise(getReadConnection(portalId, getEncryptionKey()));
|
|
3639
3775
|
const allPluginTableNames = new Set(getAllTableNames());
|
|
3640
3776
|
const sectionResults = await Promise.all(
|
|
@@ -3666,25 +3802,66 @@ function registerStatusTool(server2, deps) {
|
|
|
3666
3802
|
|
|
3667
3803
|
IMPORTANT: Always call this with section "schema" before writing any queries. It reveals which CRM object types are synced (contacts, companies, deals, tickets, custom objects, etc.), their available property names, record counts, and association types. This prevents query errors from incorrect property or object names.
|
|
3668
3804
|
|
|
3805
|
+
Optional: pass portalIds to fetch schema status for specific portals. When portalIds is provided, schema output is a JSON object keyed by portal ID.
|
|
3806
|
+
|
|
3807
|
+
Data readiness: all registered portals are synced in the background. Schema status reflects which object types have completed syncing and are queryable. If a portal is still syncing, some object types may not yet appear.
|
|
3808
|
+
|
|
3669
3809
|
Use "connection" to check which portals are available and which is selected.
|
|
3670
3810
|
Use "schema" to discover object types, property keys, record counts, and associations before writing queries.
|
|
3671
3811
|
Use "all" to get everything at once.`,
|
|
3672
3812
|
inputSchema: {
|
|
3673
3813
|
section: SECTION_SCHEMA.describe(
|
|
3674
3814
|
'Which information to retrieve: "connection" for portal list and state, "schema" for database structure, "all" for both.'
|
|
3675
|
-
)
|
|
3815
|
+
),
|
|
3816
|
+
portalIds: z15.array(z15.number()).optional().describe("Optional explicit portal IDs for schema status. If provided, schema output is keyed by portal ID.")
|
|
3676
3817
|
}
|
|
3677
|
-
}, async ({ section }) => {
|
|
3818
|
+
}, async ({ section, portalIds }) => {
|
|
3678
3819
|
try {
|
|
3679
3820
|
let result = {};
|
|
3821
|
+
let explicitSchemaHadErrors = false;
|
|
3680
3822
|
if (section === "connection" || section === "all") {
|
|
3681
3823
|
result.connection = await buildConnectionSection(deps);
|
|
3682
3824
|
}
|
|
3683
3825
|
if (section === "schema" || section === "all") {
|
|
3684
|
-
|
|
3826
|
+
const portalResolution = resolvePortalIds(
|
|
3827
|
+
portalIds,
|
|
3828
|
+
deps.getSelectedPortalId(),
|
|
3829
|
+
deps.getPortals()
|
|
3830
|
+
);
|
|
3831
|
+
if ("error" in portalResolution) {
|
|
3832
|
+
if (portalIds === void 0) {
|
|
3833
|
+
result.schema = { error: portalResolution.error };
|
|
3834
|
+
} else {
|
|
3835
|
+
return {
|
|
3836
|
+
content: [{ type: "text", text: portalResolution.error }],
|
|
3837
|
+
isError: true
|
|
3838
|
+
};
|
|
3839
|
+
}
|
|
3840
|
+
} else if (portalResolution.explicit) {
|
|
3841
|
+
const schemaByPortal = {};
|
|
3842
|
+
for (const portalId of portalResolution.portalIds) {
|
|
3843
|
+
try {
|
|
3844
|
+
await deps.ensureFresh(portalId);
|
|
3845
|
+
schemaByPortal[String(portalId)] = await buildSchemaSection(
|
|
3846
|
+
portalId,
|
|
3847
|
+
deps.getEncryptionKey
|
|
3848
|
+
);
|
|
3849
|
+
} catch (e) {
|
|
3850
|
+
explicitSchemaHadErrors = true;
|
|
3851
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
3852
|
+
schemaByPortal[String(portalId)] = { error: message };
|
|
3853
|
+
}
|
|
3854
|
+
}
|
|
3855
|
+
result.schema = schemaByPortal;
|
|
3856
|
+
} else {
|
|
3857
|
+
const [portalId] = portalResolution.portalIds;
|
|
3858
|
+
await deps.ensureFresh(portalId);
|
|
3859
|
+
result.schema = await buildSchemaSection(portalId, deps.getEncryptionKey);
|
|
3860
|
+
}
|
|
3685
3861
|
}
|
|
3686
3862
|
return {
|
|
3687
|
-
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
3863
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
3864
|
+
...explicitSchemaHadErrors ? { isError: true } : {}
|
|
3688
3865
|
};
|
|
3689
3866
|
} catch (e) {
|
|
3690
3867
|
const message = e instanceof Error ? e.message : String(e);
|
|
@@ -3700,34 +3877,39 @@ Use "all" to get everything at once.`,
|
|
|
3700
3877
|
import { z as z61 } from "zod";
|
|
3701
3878
|
|
|
3702
3879
|
// src/tools/draft_state.ts
|
|
3703
|
-
var
|
|
3704
|
-
var
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
};
|
|
3709
|
-
var
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
};
|
|
3714
|
-
var
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3880
|
+
var drafts = /* @__PURE__ */ new Map();
|
|
3881
|
+
var requireDraft = (portalId) => {
|
|
3882
|
+
const draft = drafts.get(portalId);
|
|
3883
|
+
if (!draft) throw new Error("No draft plan exists");
|
|
3884
|
+
return draft;
|
|
3885
|
+
};
|
|
3886
|
+
var getDraft = (portalId) => drafts.get(portalId) ?? null;
|
|
3887
|
+
var hasDraft = (portalId) => drafts.has(portalId);
|
|
3888
|
+
var setDraft = (portalId, plan) => {
|
|
3889
|
+
drafts.set(portalId, plan);
|
|
3890
|
+
};
|
|
3891
|
+
var addOperationsToDraft = (portalId, ops) => {
|
|
3892
|
+
const draft = requireDraft(portalId);
|
|
3893
|
+
draft.operations.push(...ops);
|
|
3894
|
+
return draft.operations.length;
|
|
3895
|
+
};
|
|
3896
|
+
var replaceOperationInDraft = (portalId, index, op) => {
|
|
3897
|
+
const draft = requireDraft(portalId);
|
|
3898
|
+
if (index < 0 || index >= draft.operations.length) {
|
|
3899
|
+
throw new Error(`Operation index ${index} out of bounds (0-${draft.operations.length - 1})`);
|
|
3718
3900
|
}
|
|
3719
|
-
|
|
3901
|
+
draft.operations[index] = op;
|
|
3720
3902
|
};
|
|
3721
|
-
var removeOperationFromDraft = (index) => {
|
|
3722
|
-
|
|
3723
|
-
if (index < 0 || index >=
|
|
3724
|
-
throw new Error(`Operation index ${index} out of bounds (0-${
|
|
3903
|
+
var removeOperationFromDraft = (portalId, index) => {
|
|
3904
|
+
const draft = requireDraft(portalId);
|
|
3905
|
+
if (index < 0 || index >= draft.operations.length) {
|
|
3906
|
+
throw new Error(`Operation index ${index} out of bounds (0-${draft.operations.length - 1})`);
|
|
3725
3907
|
}
|
|
3726
|
-
|
|
3727
|
-
return
|
|
3908
|
+
draft.operations.splice(index, 1);
|
|
3909
|
+
return draft.operations.length;
|
|
3728
3910
|
};
|
|
3729
|
-
var clearDraft = () => {
|
|
3730
|
-
|
|
3911
|
+
var clearDraft = (portalId) => {
|
|
3912
|
+
drafts.delete(portalId);
|
|
3731
3913
|
};
|
|
3732
3914
|
|
|
3733
3915
|
// src/pure/operation-conversion.ts
|
|
@@ -3907,12 +4089,12 @@ var updatePropertyMeta = {
|
|
|
3907
4089
|
}
|
|
3908
4090
|
},
|
|
3909
4091
|
toNested: (description, fields44) => {
|
|
3910
|
-
const { object_type, property_name, new_label, new_description, new_group_name, options, ...rest } = fields44;
|
|
4092
|
+
const { object_type, property_name, new_label, new_description, new_group_name, options: options2, ...rest } = fields44;
|
|
3911
4093
|
const updates = {};
|
|
3912
4094
|
if (new_label !== void 0) updates.label = new_label;
|
|
3913
4095
|
if (new_description !== void 0) updates.description = new_description;
|
|
3914
4096
|
if (new_group_name !== void 0) updates.group_name = new_group_name;
|
|
3915
|
-
if (
|
|
4097
|
+
if (options2 !== void 0) updates.options = options2;
|
|
3916
4098
|
return { type: "update_property", object_type, property_name, description, updates, ...rest };
|
|
3917
4099
|
},
|
|
3918
4100
|
requiredScopes: (op) => [getWriteScope(op.object_type)],
|
|
@@ -6358,7 +6540,7 @@ var buildRuleLines = (rules, optionLabelMap) => rules.map((rule) => {
|
|
|
6358
6540
|
const labels = rule.visible_options.map((v) => optionLabelMap.get(v) ?? v).join(", ");
|
|
6359
6541
|
return ` - When "${rule.controlling_property}" = "${rule.controlling_value}": Show "${labels}"`;
|
|
6360
6542
|
}).join("\n");
|
|
6361
|
-
var buildOptionLabelMap = (
|
|
6543
|
+
var buildOptionLabelMap = (options2) => new Map(options2.map((o) => [o.value, o.label]));
|
|
6362
6544
|
var getUniqueControllingProperties = (rules) => [...new Set(rules.map((r) => r.controlling_property))];
|
|
6363
6545
|
var buildConditionalOptionsTask = (property, objectType, rules) => {
|
|
6364
6546
|
const subject = `Configure conditional options for "${property.label}" on ${formatObjectType(objectType)}`;
|
|
@@ -10765,12 +10947,18 @@ var summariseFields = (op) => {
|
|
|
10765
10947
|
return `${k}: ${String(v)}`;
|
|
10766
10948
|
}).join(", ");
|
|
10767
10949
|
};
|
|
10768
|
-
var formatPlanView = () => {
|
|
10769
|
-
const draft = getDraft();
|
|
10950
|
+
var formatPlanView = (portalId) => {
|
|
10951
|
+
const draft = getDraft(portalId);
|
|
10952
|
+
if (!draft) {
|
|
10953
|
+
return [
|
|
10954
|
+
`Portal ${portalId}: no draft plan exists.`,
|
|
10955
|
+
"Provide title and description to start a new plan."
|
|
10956
|
+
].join("\n");
|
|
10957
|
+
}
|
|
10770
10958
|
const lines = [
|
|
10771
|
-
`Draft Plan: "${draft.title}"`,
|
|
10959
|
+
`Portal ${portalId} Draft Plan: "${draft.title}"`,
|
|
10772
10960
|
`Description: ${draft.description}`,
|
|
10773
|
-
|
|
10961
|
+
""
|
|
10774
10962
|
];
|
|
10775
10963
|
if (draft.operations.length === 0) {
|
|
10776
10964
|
lines.push("Operations: (none)");
|
|
@@ -10788,9 +10976,36 @@ var formatPlanView = () => {
|
|
|
10788
10976
|
}
|
|
10789
10977
|
return lines.join("\n");
|
|
10790
10978
|
};
|
|
10791
|
-
|
|
10979
|
+
var serialiseDraft = (portalId) => {
|
|
10980
|
+
const draft = getDraft(portalId);
|
|
10981
|
+
if (!draft) return null;
|
|
10982
|
+
return {
|
|
10983
|
+
title: draft.title,
|
|
10984
|
+
description: draft.description,
|
|
10985
|
+
operationCount: draft.operations.length,
|
|
10986
|
+
operations: draft.operations.map((operation, index) => {
|
|
10987
|
+
const op = operation;
|
|
10988
|
+
return {
|
|
10989
|
+
index: index + 1,
|
|
10990
|
+
type: String(op.type ?? "unknown"),
|
|
10991
|
+
description: String(op.description ?? "")
|
|
10992
|
+
};
|
|
10993
|
+
})
|
|
10994
|
+
};
|
|
10995
|
+
};
|
|
10996
|
+
var formatDraftResponse = (portalIds, explicit) => {
|
|
10997
|
+
if (!explicit) {
|
|
10998
|
+
return formatPlanView(portalIds[0]);
|
|
10999
|
+
}
|
|
11000
|
+
const keyed = {};
|
|
11001
|
+
for (const portalId of portalIds) {
|
|
11002
|
+
keyed[String(portalId)] = serialiseDraft(portalId);
|
|
11003
|
+
}
|
|
11004
|
+
return JSON.stringify(keyed, null, 2);
|
|
11005
|
+
};
|
|
11006
|
+
function registerBuildPlanTool(server2, deps) {
|
|
10792
11007
|
server2.registerTool("build_plan", {
|
|
10793
|
-
description: `Use this tool whenever the user wants to make changes to their HubSpot CRM configuration or data. This is the ONLY way to modify HubSpot
|
|
11008
|
+
description: `Use this tool whenever the user wants to make changes to their HubSpot CRM configuration or data. This is the ONLY way to modify HubSpot - there is no direct API access. All CRM write operations must go through a plan.
|
|
10794
11009
|
|
|
10795
11010
|
Supports ${OPERATION_TYPE_COUNT} operation types across these categories:
|
|
10796
11011
|
- Properties & groups: create, update, or delete custom properties and property groups
|
|
@@ -10803,20 +11018,25 @@ Supports ${OPERATION_TYPE_COUNT} operation types across these categories:
|
|
|
10803
11018
|
|
|
10804
11019
|
CREATING A PLAN:
|
|
10805
11020
|
Provide title + description (+ optional operations) to start a new draft.
|
|
10806
|
-
If a draft already exists, pass force: true to discard it and start fresh.
|
|
11021
|
+
If a targeted draft already exists, pass force: true to discard it and start fresh.
|
|
11022
|
+
|
|
11023
|
+
PORTAL TARGETING:
|
|
11024
|
+
Optionally pass portalIds to target one or more specific portals.
|
|
11025
|
+
If omitted, the selected portal is used.
|
|
11026
|
+
When portalIds is explicit, the response is a JSON object keyed by portal ID.
|
|
10807
11027
|
|
|
10808
11028
|
ADDING OPERATIONS:
|
|
10809
|
-
Provide operations to append them to
|
|
11029
|
+
Provide operations to append them to existing targeted drafts.
|
|
10810
11030
|
Each operation needs: type, description, and fields.
|
|
10811
|
-
Use describe_operations first to discover available types and
|
|
11031
|
+
Use describe_operations first to discover available types and required fields.
|
|
10812
11032
|
|
|
10813
11033
|
EDITING OPERATIONS:
|
|
10814
11034
|
Provide edits to replace or remove operations by their 1-based index.
|
|
10815
|
-
Edits
|
|
11035
|
+
Edits require exactly one targeted portal.
|
|
11036
|
+
Edits are applied atomically - if any replacement fails validation, no changes are made.
|
|
10816
11037
|
|
|
10817
11038
|
VIEWING THE PLAN:
|
|
10818
|
-
Call with no
|
|
10819
|
-
The current plan view is always returned in the response.
|
|
11039
|
+
Call with no operations/edits to see current draft state for the targeted portal(s).
|
|
10820
11040
|
|
|
10821
11041
|
IMPORTANT: After building the plan, present it to the user and ask for confirmation before calling submit_plan.
|
|
10822
11042
|
|
|
@@ -10827,43 +11047,77 @@ WORKFLOW:
|
|
|
10827
11047
|
4. submit_plan (dry_run: true) - Validate without saving (optional)
|
|
10828
11048
|
5. submit_plan - Submit after user confirms`,
|
|
10829
11049
|
inputSchema: {
|
|
10830
|
-
title: z61.string().optional().describe("Short, descriptive title (required when creating a new
|
|
10831
|
-
description: z61.string().optional().describe("Detailed explanation of what the plan does and why (required when creating a new
|
|
10832
|
-
operations: z61.array(FlatOperationSchema).optional().describe("Operations to add to
|
|
11050
|
+
title: z61.string().optional().describe("Short, descriptive title (required when creating a new draft)"),
|
|
11051
|
+
description: z61.string().optional().describe("Detailed explanation of what the plan does and why (required when creating a new draft)"),
|
|
11052
|
+
operations: z61.array(FlatOperationSchema).optional().describe("Operations to add to targeted draft(s)"),
|
|
10833
11053
|
edits: z61.array(EditSchema).optional().describe("Edits to apply: replace or remove operations by 1-based index"),
|
|
10834
|
-
force: z61.boolean().optional().describe("If true, discard
|
|
11054
|
+
force: z61.boolean().optional().describe("If true, discard targeted draft(s) and start fresh"),
|
|
11055
|
+
portalIds: z61.array(z61.number()).optional().describe("Optional explicit portal IDs to target. If provided, responses are keyed by portal ID.")
|
|
11056
|
+
}
|
|
11057
|
+
}, async ({ title, description, operations, edits, force, portalIds }) => {
|
|
11058
|
+
const portalResolution = resolvePortalIds(
|
|
11059
|
+
portalIds,
|
|
11060
|
+
deps.getSelectedPortalId(),
|
|
11061
|
+
deps.getPortals()
|
|
11062
|
+
);
|
|
11063
|
+
if ("error" in portalResolution) {
|
|
11064
|
+
return {
|
|
11065
|
+
content: [{ type: "text", text: portalResolution.error }],
|
|
11066
|
+
isError: true
|
|
11067
|
+
};
|
|
11068
|
+
}
|
|
11069
|
+
const targetPortalIds = portalResolution.portalIds;
|
|
11070
|
+
const explicit = portalResolution.explicit;
|
|
11071
|
+
if (edits && edits.length > 0 && targetPortalIds.length !== 1) {
|
|
11072
|
+
return {
|
|
11073
|
+
content: [{ type: "text", text: "When using edits, target exactly one portal. Provide a single portal ID in portalIds." }],
|
|
11074
|
+
isError: true
|
|
11075
|
+
};
|
|
10835
11076
|
}
|
|
10836
|
-
|
|
10837
|
-
if (
|
|
11077
|
+
const missingPortalIds = targetPortalIds.filter((portalId) => !hasDraft(portalId));
|
|
11078
|
+
if (force) {
|
|
10838
11079
|
if (!title || !description) {
|
|
10839
11080
|
return {
|
|
10840
|
-
content: [{ type: "text", text: "
|
|
11081
|
+
content: [{ type: "text", text: "When using force: true, provide title and description for the new draft." }],
|
|
10841
11082
|
isError: true
|
|
10842
11083
|
};
|
|
10843
11084
|
}
|
|
10844
|
-
|
|
10845
|
-
|
|
10846
|
-
|
|
11085
|
+
for (const portalId of targetPortalIds) {
|
|
11086
|
+
clearDraft(portalId);
|
|
11087
|
+
setDraft(portalId, { title, description, operations: [] });
|
|
11088
|
+
}
|
|
11089
|
+
} else {
|
|
11090
|
+
if (missingPortalIds.length > 0 && (!title || !description)) {
|
|
11091
|
+
const plural = missingPortalIds.length > 1 ? "portals" : "portal";
|
|
10847
11092
|
return {
|
|
10848
|
-
content: [{
|
|
11093
|
+
content: [{
|
|
11094
|
+
type: "text",
|
|
11095
|
+
text: `No draft plan exists for ${plural}: ${missingPortalIds.join(", ")}. Provide title and description to start a new draft.`
|
|
11096
|
+
}],
|
|
10849
11097
|
isError: true
|
|
10850
11098
|
};
|
|
10851
11099
|
}
|
|
10852
|
-
|
|
10853
|
-
|
|
10854
|
-
|
|
10855
|
-
|
|
10856
|
-
|
|
10857
|
-
if (title
|
|
10858
|
-
|
|
11100
|
+
for (const portalId of targetPortalIds) {
|
|
11101
|
+
if (!hasDraft(portalId)) {
|
|
11102
|
+
setDraft(portalId, { title, description, operations: [] });
|
|
11103
|
+
continue;
|
|
11104
|
+
}
|
|
11105
|
+
if (title || description) {
|
|
11106
|
+
const draft = getDraft(portalId);
|
|
11107
|
+
if (title) draft.title = title;
|
|
11108
|
+
if (description) draft.description = description;
|
|
11109
|
+
}
|
|
10859
11110
|
}
|
|
10860
11111
|
}
|
|
10861
11112
|
if (edits && edits.length > 0) {
|
|
10862
|
-
const
|
|
11113
|
+
const portalId = targetPortalIds[0];
|
|
11114
|
+
const draft = getDraft(portalId);
|
|
10863
11115
|
const opCount = draft.operations.length;
|
|
10864
11116
|
if (opCount === 0) {
|
|
10865
11117
|
return {
|
|
10866
|
-
content: [{ type: "text", text:
|
|
11118
|
+
content: [{ type: "text", text: `Draft has no operations to edit. Provide operations to add them first.
|
|
11119
|
+
|
|
11120
|
+
${formatPlanView(portalId)}` }],
|
|
10867
11121
|
isError: true
|
|
10868
11122
|
};
|
|
10869
11123
|
}
|
|
@@ -10872,7 +11126,7 @@ WORKFLOW:
|
|
|
10872
11126
|
return {
|
|
10873
11127
|
content: [{ type: "text", text: `Index ${edit.index} is out of range. Draft has ${opCount} operation(s) (indices 1-${opCount}).
|
|
10874
11128
|
|
|
10875
|
-
${formatPlanView()}` }],
|
|
11129
|
+
${formatPlanView(portalId)}` }],
|
|
10876
11130
|
isError: true
|
|
10877
11131
|
};
|
|
10878
11132
|
}
|
|
@@ -10880,18 +11134,17 @@ ${formatPlanView()}` }],
|
|
|
10880
11134
|
return {
|
|
10881
11135
|
content: [{ type: "text", text: `Edit at index ${edit.index}: "replace" action requires an operation.
|
|
10882
11136
|
|
|
10883
|
-
${formatPlanView()}` }],
|
|
11137
|
+
${formatPlanView(portalId)}` }],
|
|
10884
11138
|
isError: true
|
|
10885
11139
|
};
|
|
10886
11140
|
}
|
|
10887
11141
|
}
|
|
10888
|
-
const replaces = edits.filter((
|
|
10889
|
-
const removes = edits.filter((
|
|
11142
|
+
const replaces = edits.filter((edit) => edit.action === "replace");
|
|
11143
|
+
const removes = edits.filter((edit) => edit.action === "remove");
|
|
10890
11144
|
const nestedReplacements = [];
|
|
10891
11145
|
const errors = [];
|
|
10892
11146
|
for (const edit of replaces) {
|
|
10893
|
-
const
|
|
10894
|
-
const nested = nestOperation(flat);
|
|
11147
|
+
const nested = nestOperation(edit.operation);
|
|
10895
11148
|
if (typeof nested === "string") {
|
|
10896
11149
|
errors.push(`Index ${edit.index}: ${nested}`);
|
|
10897
11150
|
continue;
|
|
@@ -10904,7 +11157,6 @@ ${formatPlanView()}` }],
|
|
|
10904
11157
|
nestedReplacements.push({ zeroIndex: edit.index - 1, nested });
|
|
10905
11158
|
}
|
|
10906
11159
|
if (errors.length > 0) {
|
|
10907
|
-
const hint = "Use describe_operations to check field requirements.";
|
|
10908
11160
|
return {
|
|
10909
11161
|
content: [{
|
|
10910
11162
|
type: "text",
|
|
@@ -10912,44 +11164,58 @@ ${formatPlanView()}` }],
|
|
|
10912
11164
|
|
|
10913
11165
|
${errors.join("\n\n")}
|
|
10914
11166
|
|
|
10915
|
-
|
|
11167
|
+
Use describe_operations to check field requirements.
|
|
10916
11168
|
|
|
10917
|
-
${formatPlanView()}`
|
|
11169
|
+
${formatPlanView(portalId)}`
|
|
10918
11170
|
}],
|
|
10919
11171
|
isError: true
|
|
10920
11172
|
};
|
|
10921
11173
|
}
|
|
10922
11174
|
for (const { zeroIndex, nested } of nestedReplacements) {
|
|
10923
|
-
replaceOperationInDraft(zeroIndex, nested);
|
|
11175
|
+
replaceOperationInDraft(portalId, zeroIndex, nested);
|
|
10924
11176
|
}
|
|
10925
|
-
const removeIndices = removes.map((
|
|
10926
|
-
for (const
|
|
10927
|
-
removeOperationFromDraft(
|
|
11177
|
+
const removeIndices = removes.map((edit) => edit.index - 1).sort((a, b) => b - a);
|
|
11178
|
+
for (const index of removeIndices) {
|
|
11179
|
+
removeOperationFromDraft(portalId, index);
|
|
10928
11180
|
}
|
|
10929
11181
|
}
|
|
10930
11182
|
if (operations && operations.length > 0) {
|
|
10931
|
-
const
|
|
10932
|
-
const
|
|
10933
|
-
const
|
|
10934
|
-
|
|
10935
|
-
const
|
|
11183
|
+
const validatedByPortal = /* @__PURE__ */ new Map();
|
|
11184
|
+
const portalErrors = [];
|
|
11185
|
+
for (const portalId of targetPortalIds) {
|
|
11186
|
+
const draft = getDraft(portalId);
|
|
11187
|
+
const startIndex = draft.operations.length + 1;
|
|
11188
|
+
const { nested, errors } = convertAndValidateOperations(operations, startIndex);
|
|
11189
|
+
if (errors.length > 0) {
|
|
11190
|
+
portalErrors.push(`Portal ${portalId}:
|
|
11191
|
+
${errors.join("\n")}`);
|
|
11192
|
+
continue;
|
|
11193
|
+
}
|
|
11194
|
+
validatedByPortal.set(portalId, nested);
|
|
11195
|
+
}
|
|
11196
|
+
if (portalErrors.length > 0) {
|
|
10936
11197
|
return {
|
|
10937
11198
|
content: [{
|
|
10938
11199
|
type: "text",
|
|
10939
|
-
text: `${
|
|
10940
|
-
|
|
10941
|
-
${errors.join("\n\n")}
|
|
11200
|
+
text: `${portalErrors.length} portal(s) failed operation validation:
|
|
10942
11201
|
|
|
10943
|
-
${
|
|
11202
|
+
${portalErrors.join("\n\n")}
|
|
10944
11203
|
|
|
10945
|
-
|
|
11204
|
+
Use describe_operations to see the required fields for each operation type.`
|
|
10946
11205
|
}],
|
|
10947
11206
|
isError: true
|
|
10948
11207
|
};
|
|
10949
11208
|
}
|
|
10950
|
-
|
|
11209
|
+
for (const portalId of targetPortalIds) {
|
|
11210
|
+
addOperationsToDraft(portalId, validatedByPortal.get(portalId) ?? []);
|
|
11211
|
+
}
|
|
10951
11212
|
}
|
|
10952
|
-
return {
|
|
11213
|
+
return {
|
|
11214
|
+
content: [{
|
|
11215
|
+
type: "text",
|
|
11216
|
+
text: formatDraftResponse(targetPortalIds, explicit)
|
|
11217
|
+
}]
|
|
11218
|
+
};
|
|
10953
11219
|
});
|
|
10954
11220
|
}
|
|
10955
11221
|
|
|
@@ -10997,16 +11263,16 @@ import { Effect as Effect89 } from "effect";
|
|
|
10997
11263
|
var formatFixHint = (issue, summary) => {
|
|
10998
11264
|
const opNum = issue.operation_index + 1;
|
|
10999
11265
|
const opInfo = summary?.operations[issue.operation_index];
|
|
11000
|
-
const label = opInfo ? `${opInfo.type} "${opInfo.description}"` :
|
|
11266
|
+
const label = opInfo ? `${opInfo.type} "${opInfo.description}"` : "operation";
|
|
11001
11267
|
const severity = issue.severity === "error" ? "ERROR" : "WARNING";
|
|
11002
|
-
return ` ${severity} [Op ${opNum}] (${label}): ${issue.code}
|
|
11268
|
+
return ` ${severity} [Op ${opNum}] (${label}): ${issue.code} - ${issue.message}`;
|
|
11003
11269
|
};
|
|
11004
11270
|
var formatValidationIssues = (response) => {
|
|
11005
11271
|
const v = response.validation;
|
|
11006
11272
|
const lines = [];
|
|
11007
11273
|
if (response.summary) {
|
|
11008
11274
|
lines.push(`Summary: ${response.summary.operation_count} operation(s)`);
|
|
11009
|
-
lines.push(
|
|
11275
|
+
lines.push("");
|
|
11010
11276
|
}
|
|
11011
11277
|
const errors = v.issues.filter((i) => i.severity === "error");
|
|
11012
11278
|
const warnings = v.issues.filter((i) => i.severity === "warning");
|
|
@@ -11015,165 +11281,272 @@ var formatValidationIssues = (response) => {
|
|
|
11015
11281
|
for (const issue of errors) {
|
|
11016
11282
|
lines.push(formatFixHint(issue, response.summary));
|
|
11017
11283
|
}
|
|
11018
|
-
lines.push(
|
|
11284
|
+
lines.push("");
|
|
11019
11285
|
}
|
|
11020
11286
|
if (warnings.length > 0) {
|
|
11021
11287
|
lines.push(`Warnings (${warnings.length}):`);
|
|
11022
11288
|
for (const issue of warnings) {
|
|
11023
11289
|
lines.push(formatFixHint(issue, response.summary));
|
|
11024
11290
|
}
|
|
11025
|
-
lines.push(
|
|
11291
|
+
lines.push("");
|
|
11026
11292
|
}
|
|
11027
11293
|
if (!v.scope_check?.valid) {
|
|
11028
11294
|
lines.push(`Scope issue: Missing scopes: ${v.scope_check.missingScopes.join(", ")}`);
|
|
11029
|
-
lines.push(
|
|
11030
|
-
lines.push(
|
|
11295
|
+
lines.push("The user may need to re-authorize the portal with write permissions.");
|
|
11296
|
+
lines.push("");
|
|
11031
11297
|
}
|
|
11032
11298
|
return lines;
|
|
11033
11299
|
};
|
|
11300
|
+
var submitPortalDraft = async (portalId, dryRun, deps) => {
|
|
11301
|
+
const draft = getDraft(portalId);
|
|
11302
|
+
if (!draft) {
|
|
11303
|
+
return {
|
|
11304
|
+
kind: "error",
|
|
11305
|
+
message: "No draft plan exists. Start one with build_plan first."
|
|
11306
|
+
};
|
|
11307
|
+
}
|
|
11308
|
+
if (draft.operations.length === 0) {
|
|
11309
|
+
return {
|
|
11310
|
+
kind: "error",
|
|
11311
|
+
message: "Draft plan has no operations. Use build_plan to add operations before submitting."
|
|
11312
|
+
};
|
|
11313
|
+
}
|
|
11314
|
+
try {
|
|
11315
|
+
const response = await Effect89.runPromise(
|
|
11316
|
+
deps.ws.sendPlanCreate(portalId, {
|
|
11317
|
+
title: draft.title,
|
|
11318
|
+
description: draft.description,
|
|
11319
|
+
operations: draft.operations,
|
|
11320
|
+
dry_run: dryRun
|
|
11321
|
+
})
|
|
11322
|
+
);
|
|
11323
|
+
if (!response.success && !response.validation) {
|
|
11324
|
+
return {
|
|
11325
|
+
kind: "error",
|
|
11326
|
+
message: `Failed to submit plan: ${response.error ?? "Unknown error"}`
|
|
11327
|
+
};
|
|
11328
|
+
}
|
|
11329
|
+
if (!response.success && response.validation) {
|
|
11330
|
+
return {
|
|
11331
|
+
kind: "validation_failed",
|
|
11332
|
+
response
|
|
11333
|
+
};
|
|
11334
|
+
}
|
|
11335
|
+
if (response.dry_run) {
|
|
11336
|
+
return {
|
|
11337
|
+
kind: "dry_run_success",
|
|
11338
|
+
response
|
|
11339
|
+
};
|
|
11340
|
+
}
|
|
11341
|
+
clearDraft(portalId);
|
|
11342
|
+
return {
|
|
11343
|
+
kind: "submit_success",
|
|
11344
|
+
response
|
|
11345
|
+
};
|
|
11346
|
+
} catch (e) {
|
|
11347
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
11348
|
+
return {
|
|
11349
|
+
kind: "error",
|
|
11350
|
+
message: `Error submitting plan: ${message}`
|
|
11351
|
+
};
|
|
11352
|
+
}
|
|
11353
|
+
};
|
|
11354
|
+
var toKeyedResult = (result) => {
|
|
11355
|
+
switch (result.kind) {
|
|
11356
|
+
case "error":
|
|
11357
|
+
return { success: false, error: result.message };
|
|
11358
|
+
case "validation_failed":
|
|
11359
|
+
return {
|
|
11360
|
+
success: false,
|
|
11361
|
+
error: "Plan validation failed",
|
|
11362
|
+
summary: result.response.summary,
|
|
11363
|
+
validation: result.response.validation,
|
|
11364
|
+
scope_warning: result.response.scope_warning
|
|
11365
|
+
};
|
|
11366
|
+
case "dry_run_success":
|
|
11367
|
+
return {
|
|
11368
|
+
success: true,
|
|
11369
|
+
dry_run: true,
|
|
11370
|
+
summary: result.response.summary,
|
|
11371
|
+
validation: result.response.validation,
|
|
11372
|
+
scope_warning: result.response.scope_warning
|
|
11373
|
+
};
|
|
11374
|
+
case "submit_success":
|
|
11375
|
+
return {
|
|
11376
|
+
success: true,
|
|
11377
|
+
dry_run: false,
|
|
11378
|
+
plan_id: result.response.plan_id,
|
|
11379
|
+
summary: result.response.summary,
|
|
11380
|
+
validation: result.response.validation,
|
|
11381
|
+
scope_warning: result.response.scope_warning
|
|
11382
|
+
};
|
|
11383
|
+
}
|
|
11384
|
+
};
|
|
11385
|
+
var formatSinglePortalResult = (result) => {
|
|
11386
|
+
if (result.kind === "error") {
|
|
11387
|
+
return {
|
|
11388
|
+
text: result.message,
|
|
11389
|
+
isError: true
|
|
11390
|
+
};
|
|
11391
|
+
}
|
|
11392
|
+
if (result.kind === "validation_failed") {
|
|
11393
|
+
const response2 = result.response;
|
|
11394
|
+
const lines2 = [
|
|
11395
|
+
"Plan validation failed - the plan was NOT saved. Fix the issues below and resubmit.",
|
|
11396
|
+
"",
|
|
11397
|
+
...formatValidationIssues(response2),
|
|
11398
|
+
"Use build_plan with edits to fix issues, then submit_plan again."
|
|
11399
|
+
];
|
|
11400
|
+
return { text: lines2.join("\n") };
|
|
11401
|
+
}
|
|
11402
|
+
if (result.kind === "dry_run_success") {
|
|
11403
|
+
const response2 = result.response;
|
|
11404
|
+
const summary2 = response2.summary;
|
|
11405
|
+
const opLines2 = summary2.operations.map(
|
|
11406
|
+
(op, i) => ` ${i + 1}. [${op.type}] ${op.description} (${op.record_count} records)`
|
|
11407
|
+
);
|
|
11408
|
+
const lines2 = [
|
|
11409
|
+
"Validation passed - plan is ready to submit.",
|
|
11410
|
+
"",
|
|
11411
|
+
"Summary:",
|
|
11412
|
+
` Operations: ${summary2.operation_count}`,
|
|
11413
|
+
` Total records affected: ${summary2.total_record_count}`,
|
|
11414
|
+
"",
|
|
11415
|
+
"Operations:",
|
|
11416
|
+
...opLines2
|
|
11417
|
+
];
|
|
11418
|
+
if (response2.validation?.issues && response2.validation.issues.length > 0) {
|
|
11419
|
+
lines2.push("", ...formatValidationIssues(response2));
|
|
11420
|
+
}
|
|
11421
|
+
if (response2.scope_warning) {
|
|
11422
|
+
lines2.push("", `WARNING: ${response2.scope_warning}`);
|
|
11423
|
+
}
|
|
11424
|
+
lines2.push("", "Draft is preserved. Call submit_plan() (without dry_run) to save the plan.");
|
|
11425
|
+
return { text: lines2.join("\n") };
|
|
11426
|
+
}
|
|
11427
|
+
const response = result.response;
|
|
11428
|
+
const summary = response.summary;
|
|
11429
|
+
const opLines = summary.operations.map(
|
|
11430
|
+
(op, i) => ` ${i + 1}. [${op.type}] ${op.description} (${op.record_count} records)`
|
|
11431
|
+
);
|
|
11432
|
+
const lines = [
|
|
11433
|
+
"Plan submitted successfully.",
|
|
11434
|
+
"",
|
|
11435
|
+
`Plan ID: ${response.plan_id}`,
|
|
11436
|
+
"Status: DRAFT (awaiting review in HubSpot UI)",
|
|
11437
|
+
"",
|
|
11438
|
+
"Summary:",
|
|
11439
|
+
` Operations: ${summary.operation_count}`,
|
|
11440
|
+
` Total records affected: ${summary.total_record_count}`,
|
|
11441
|
+
"",
|
|
11442
|
+
"Operations:",
|
|
11443
|
+
...opLines
|
|
11444
|
+
];
|
|
11445
|
+
if (response.validation) {
|
|
11446
|
+
const validation = response.validation;
|
|
11447
|
+
if (validation.issues && validation.issues.length > 0) {
|
|
11448
|
+
lines.push("", "Validation warnings:");
|
|
11449
|
+
for (const issue of validation.issues) {
|
|
11450
|
+
lines.push(` - [Op ${issue.operation_index + 1}] ${issue.code}: ${issue.message}`);
|
|
11451
|
+
}
|
|
11452
|
+
}
|
|
11453
|
+
if (!validation.scope_check?.valid) {
|
|
11454
|
+
lines.push("", `Scope warning: Missing scopes: ${validation.scope_check.missingScopes.join(", ")}`);
|
|
11455
|
+
}
|
|
11456
|
+
}
|
|
11457
|
+
if (response.scope_warning) {
|
|
11458
|
+
lines.push("", `WARNING: ${response.scope_warning}`);
|
|
11459
|
+
}
|
|
11460
|
+
lines.push("", "The user can review and execute this plan in their HubSpot settings page.");
|
|
11461
|
+
return { text: lines.join("\n") };
|
|
11462
|
+
};
|
|
11034
11463
|
function registerSubmitPlanTool(server2, deps) {
|
|
11035
11464
|
server2.registerTool("submit_plan", {
|
|
11036
|
-
description: `Validate and/or submit
|
|
11465
|
+
description: `Validate and/or submit draft write plans.
|
|
11466
|
+
|
|
11467
|
+
IMPORTANT: Always present the plan to the user and get explicit confirmation BEFORE calling this tool. Never submit a plan without user approval.
|
|
11037
11468
|
|
|
11038
|
-
|
|
11469
|
+
PORTAL TARGETING:
|
|
11470
|
+
Optionally pass portalIds to submit one or more specific portal drafts.
|
|
11471
|
+
If omitted, the selected portal is used.
|
|
11472
|
+
When portalIds is explicit, results are a JSON object keyed by portal ID.
|
|
11039
11473
|
|
|
11040
11474
|
MODES:
|
|
11041
|
-
- dry_run: true
|
|
11042
|
-
- dry_run: false (default)
|
|
11475
|
+
- dry_run: true - Validate only. Runs full server-side validation (scopes, HubSpot checks) without saving. Drafts are preserved.
|
|
11476
|
+
- dry_run: false (default) - Validate AND save. Successful portals are saved as DRAFT and their drafts are cleared.
|
|
11043
11477
|
|
|
11044
11478
|
RECOMMENDED WORKFLOW:
|
|
11045
11479
|
1. Build the plan with build_plan
|
|
11046
11480
|
2. Present the plan to the user and ask for confirmation
|
|
11047
11481
|
3. Call submit_plan(dry_run: true) to validate
|
|
11048
11482
|
4. Fix any issues with build_plan(edits: [...])
|
|
11049
|
-
5. Call submit_plan() to save after user confirms
|
|
11050
|
-
|
|
11051
|
-
After a successful submission, the user can review and execute the plan in their HubSpot settings page.`,
|
|
11483
|
+
5. Call submit_plan() to save after user confirms`,
|
|
11052
11484
|
inputSchema: {
|
|
11053
|
-
dry_run: z63.boolean().optional().describe("If true, validate only without saving. Defaults to false.")
|
|
11054
|
-
|
|
11055
|
-
|
|
11056
|
-
|
|
11057
|
-
|
|
11058
|
-
|
|
11059
|
-
|
|
11060
|
-
|
|
11061
|
-
|
|
11062
|
-
|
|
11063
|
-
if (draft.operations.length === 0) {
|
|
11064
|
-
return {
|
|
11065
|
-
content: [{ type: "text", text: "Draft plan has no operations. Use build_plan to add operations before submitting." }],
|
|
11066
|
-
isError: true
|
|
11067
|
-
};
|
|
11068
|
-
}
|
|
11069
|
-
const portalId = deps.getSelectedPortalId();
|
|
11070
|
-
if (portalId === null) {
|
|
11485
|
+
dry_run: z63.boolean().optional().describe("If true, validate only without saving. Defaults to false."),
|
|
11486
|
+
portalIds: z63.array(z63.number()).optional().describe("Optional explicit portal IDs to submit. If provided, results are keyed by portal ID.")
|
|
11487
|
+
}
|
|
11488
|
+
}, async ({ dry_run, portalIds }) => {
|
|
11489
|
+
const portalResolution = resolvePortalIds(
|
|
11490
|
+
portalIds,
|
|
11491
|
+
deps.getSelectedPortalId(),
|
|
11492
|
+
deps.getPortals()
|
|
11493
|
+
);
|
|
11494
|
+
if ("error" in portalResolution) {
|
|
11071
11495
|
return {
|
|
11072
|
-
content: [{ type: "text", text:
|
|
11496
|
+
content: [{ type: "text", text: portalResolution.error }],
|
|
11073
11497
|
isError: true
|
|
11074
11498
|
};
|
|
11075
11499
|
}
|
|
11076
|
-
|
|
11077
|
-
|
|
11078
|
-
|
|
11079
|
-
|
|
11080
|
-
|
|
11081
|
-
|
|
11082
|
-
|
|
11083
|
-
|
|
11084
|
-
|
|
11085
|
-
|
|
11086
|
-
|
|
11087
|
-
content: [{ type: "text", text: `Failed to submit plan: ${response.error ?? "Unknown error"}` }],
|
|
11088
|
-
isError: true
|
|
11089
|
-
};
|
|
11090
|
-
}
|
|
11091
|
-
if (!response.success && response.validation) {
|
|
11092
|
-
const lines2 = [
|
|
11093
|
-
`Plan validation failed \u2014 the plan was NOT saved. Fix the issues below and resubmit.`,
|
|
11094
|
-
``,
|
|
11095
|
-
...formatValidationIssues(response),
|
|
11096
|
-
`Use build_plan with edits to fix issues, then submit_plan again.`
|
|
11097
|
-
];
|
|
11098
|
-
return { content: [{ type: "text", text: lines2.join("\n") }] };
|
|
11099
|
-
}
|
|
11100
|
-
if (response.dry_run) {
|
|
11101
|
-
const summary2 = response.summary;
|
|
11102
|
-
const opLines2 = summary2.operations.map(
|
|
11103
|
-
(op, i) => ` ${i + 1}. [${op.type}] ${op.description} (${op.record_count} records)`
|
|
11104
|
-
);
|
|
11105
|
-
const lines2 = [
|
|
11106
|
-
`Validation passed \u2014 plan is ready to submit.`,
|
|
11107
|
-
``,
|
|
11108
|
-
`Summary:`,
|
|
11109
|
-
` Operations: ${summary2.operation_count}`,
|
|
11110
|
-
` Total records affected: ${summary2.total_record_count}`,
|
|
11111
|
-
``,
|
|
11112
|
-
`Operations:`,
|
|
11113
|
-
...opLines2
|
|
11114
|
-
];
|
|
11115
|
-
if (response.validation?.issues && response.validation.issues.length > 0) {
|
|
11116
|
-
lines2.push(``, ...formatValidationIssues(response));
|
|
11117
|
-
}
|
|
11118
|
-
if (response.scope_warning) {
|
|
11119
|
-
lines2.push(``, `WARNING: ${response.scope_warning}`);
|
|
11500
|
+
const targetPortalIds = portalResolution.portalIds;
|
|
11501
|
+
const dryRun = dry_run ?? false;
|
|
11502
|
+
if (portalResolution.explicit) {
|
|
11503
|
+
const keyed = {};
|
|
11504
|
+
let hadAnyErrors = false;
|
|
11505
|
+
for (const portalId2 of targetPortalIds) {
|
|
11506
|
+
const result2 = await submitPortalDraft(portalId2, dryRun, deps);
|
|
11507
|
+
const keyedResult = toKeyedResult(result2);
|
|
11508
|
+
keyed[String(portalId2)] = keyedResult;
|
|
11509
|
+
if (!keyedResult.success) {
|
|
11510
|
+
hadAnyErrors = true;
|
|
11120
11511
|
}
|
|
11121
|
-
lines2.push(``, `Draft is preserved. Call submit_plan() (without dry_run) to save the plan.`);
|
|
11122
|
-
return { content: [{ type: "text", text: lines2.join("\n") }] };
|
|
11123
11512
|
}
|
|
11124
|
-
clearDraft();
|
|
11125
|
-
const summary = response.summary;
|
|
11126
|
-
const opLines = summary.operations.map(
|
|
11127
|
-
(op, i) => ` ${i + 1}. [${op.type}] ${op.description} (${op.record_count} records)`
|
|
11128
|
-
);
|
|
11129
|
-
const lines = [
|
|
11130
|
-
`Plan submitted successfully.`,
|
|
11131
|
-
``,
|
|
11132
|
-
`Plan ID: ${response.plan_id}`,
|
|
11133
|
-
`Status: DRAFT (awaiting review in HubSpot UI)`,
|
|
11134
|
-
``,
|
|
11135
|
-
`Summary:`,
|
|
11136
|
-
` Operations: ${summary.operation_count}`,
|
|
11137
|
-
` Total records affected: ${summary.total_record_count}`,
|
|
11138
|
-
``,
|
|
11139
|
-
`Operations:`,
|
|
11140
|
-
...opLines
|
|
11141
|
-
];
|
|
11142
|
-
if (response.validation) {
|
|
11143
|
-
const v = response.validation;
|
|
11144
|
-
if (v.issues && v.issues.length > 0) {
|
|
11145
|
-
lines.push(``, `Validation warnings:`);
|
|
11146
|
-
for (const issue of v.issues) {
|
|
11147
|
-
lines.push(` - [Op ${issue.operation_index + 1}] ${issue.code}: ${issue.message}`);
|
|
11148
|
-
}
|
|
11149
|
-
}
|
|
11150
|
-
if (!v.scope_check?.valid) {
|
|
11151
|
-
lines.push(``, `Scope warning: Missing scopes: ${v.scope_check.missingScopes.join(", ")}`);
|
|
11152
|
-
}
|
|
11153
|
-
}
|
|
11154
|
-
if (response.scope_warning) {
|
|
11155
|
-
lines.push(``, `WARNING: ${response.scope_warning}`);
|
|
11156
|
-
}
|
|
11157
|
-
lines.push(``, `The user can review and execute this plan in their HubSpot settings page.`);
|
|
11158
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
11159
|
-
} catch (e) {
|
|
11160
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
11161
11513
|
return {
|
|
11162
|
-
content: [{ type: "text", text:
|
|
11163
|
-
isError: true
|
|
11514
|
+
content: [{ type: "text", text: JSON.stringify(keyed, null, 2) }],
|
|
11515
|
+
...hadAnyErrors ? { isError: true } : {}
|
|
11164
11516
|
};
|
|
11165
11517
|
}
|
|
11518
|
+
const portalId = targetPortalIds[0];
|
|
11519
|
+
const result = await submitPortalDraft(portalId, dryRun, deps);
|
|
11520
|
+
const formatted = formatSinglePortalResult(result);
|
|
11521
|
+
return {
|
|
11522
|
+
content: [{ type: "text", text: formatted.text }],
|
|
11523
|
+
...formatted.isError ? { isError: true } : {}
|
|
11524
|
+
};
|
|
11166
11525
|
});
|
|
11167
11526
|
}
|
|
11168
11527
|
|
|
11169
11528
|
// src/tools/get_plans.ts
|
|
11170
11529
|
import { z as z64 } from "zod";
|
|
11171
11530
|
import { Effect as Effect90 } from "effect";
|
|
11531
|
+
var serialiseDraft2 = (portalId) => {
|
|
11532
|
+
const draft = getDraft(portalId);
|
|
11533
|
+
if (!draft) return null;
|
|
11534
|
+
return {
|
|
11535
|
+
title: draft.title,
|
|
11536
|
+
description: draft.description,
|
|
11537
|
+
operationCount: draft.operations.length
|
|
11538
|
+
};
|
|
11539
|
+
};
|
|
11172
11540
|
function registerGetPlansTool(server2, deps) {
|
|
11173
11541
|
server2.registerTool("get_plans", {
|
|
11174
|
-
description: `List write plans for
|
|
11542
|
+
description: `List write plans for one or more HubSpot portals, including current local draft state.
|
|
11175
11543
|
|
|
11176
|
-
Use this to check
|
|
11544
|
+
Use this to check submitted plans and view draft readiness before calling submit_plan.
|
|
11545
|
+
|
|
11546
|
+
PORTAL TARGETING:
|
|
11547
|
+
Optionally pass portalIds to fetch plans for specific portals.
|
|
11548
|
+
If omitted, the selected portal is used.
|
|
11549
|
+
When portalIds is explicit, results are a JSON object keyed by portal ID.
|
|
11177
11550
|
|
|
11178
11551
|
Statuses:
|
|
11179
11552
|
- DRAFT: Awaiting user review in HubSpot UI
|
|
@@ -11182,16 +11555,59 @@ Statuses:
|
|
|
11182
11555
|
- FAILED: Execution failed
|
|
11183
11556
|
- REJECTED: User rejected the plan`,
|
|
11184
11557
|
inputSchema: {
|
|
11185
|
-
status: z64.enum(["DRAFT", "REJECTED", "EXECUTING", "EXECUTED", "FAILED"]).optional().describe("Filter by plan status. Omit to show all plans.")
|
|
11186
|
-
|
|
11187
|
-
|
|
11188
|
-
|
|
11189
|
-
|
|
11558
|
+
status: z64.enum(["DRAFT", "REJECTED", "EXECUTING", "EXECUTED", "FAILED"]).optional().describe("Filter by plan status. Omit to show all plans."),
|
|
11559
|
+
portalIds: z64.array(z64.number()).optional().describe("Optional explicit portal IDs. If provided, results are keyed by portal ID.")
|
|
11560
|
+
}
|
|
11561
|
+
}, async ({ status, portalIds }) => {
|
|
11562
|
+
const portalResolution = resolvePortalIds(
|
|
11563
|
+
portalIds,
|
|
11564
|
+
deps.getSelectedPortalId(),
|
|
11565
|
+
deps.getPortals()
|
|
11566
|
+
);
|
|
11567
|
+
if ("error" in portalResolution) {
|
|
11190
11568
|
return {
|
|
11191
|
-
content: [{ type: "text", text:
|
|
11569
|
+
content: [{ type: "text", text: portalResolution.error }],
|
|
11192
11570
|
isError: true
|
|
11193
11571
|
};
|
|
11194
11572
|
}
|
|
11573
|
+
const targetPortalIds = portalResolution.portalIds;
|
|
11574
|
+
if (portalResolution.explicit) {
|
|
11575
|
+
const keyed = {};
|
|
11576
|
+
let hadAnyErrors = false;
|
|
11577
|
+
for (const portalId2 of targetPortalIds) {
|
|
11578
|
+
const draft2 = serialiseDraft2(portalId2);
|
|
11579
|
+
try {
|
|
11580
|
+
const response = await Effect90.runPromise(
|
|
11581
|
+
deps.ws.sendPlanList(portalId2, status)
|
|
11582
|
+
);
|
|
11583
|
+
if (!response.success) {
|
|
11584
|
+
hadAnyErrors = true;
|
|
11585
|
+
keyed[String(portalId2)] = {
|
|
11586
|
+
draft: draft2,
|
|
11587
|
+
error: `Failed to list plans: ${response.error}`
|
|
11588
|
+
};
|
|
11589
|
+
continue;
|
|
11590
|
+
}
|
|
11591
|
+
keyed[String(portalId2)] = {
|
|
11592
|
+
draft: draft2,
|
|
11593
|
+
plans: response.plans ?? []
|
|
11594
|
+
};
|
|
11595
|
+
} catch (e) {
|
|
11596
|
+
hadAnyErrors = true;
|
|
11597
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
11598
|
+
keyed[String(portalId2)] = {
|
|
11599
|
+
draft: draft2,
|
|
11600
|
+
error: `Error listing plans: ${message}`
|
|
11601
|
+
};
|
|
11602
|
+
}
|
|
11603
|
+
}
|
|
11604
|
+
return {
|
|
11605
|
+
content: [{ type: "text", text: JSON.stringify(keyed, null, 2) }],
|
|
11606
|
+
...hadAnyErrors ? { isError: true } : {}
|
|
11607
|
+
};
|
|
11608
|
+
}
|
|
11609
|
+
const portalId = targetPortalIds[0];
|
|
11610
|
+
const draft = serialiseDraft2(portalId);
|
|
11195
11611
|
try {
|
|
11196
11612
|
const response = await Effect90.runPromise(
|
|
11197
11613
|
deps.ws.sendPlanList(portalId, status)
|
|
@@ -11203,19 +11619,22 @@ Statuses:
|
|
|
11203
11619
|
};
|
|
11204
11620
|
}
|
|
11205
11621
|
const plans = response.plans ?? [];
|
|
11622
|
+
const draftLine = draft ? `Current draft: "${draft.title}" (${draft.operationCount} operation(s))` : "Current draft: none";
|
|
11206
11623
|
if (plans.length === 0) {
|
|
11207
11624
|
const filterMsg = status ? ` with status ${status}` : "";
|
|
11208
11625
|
return {
|
|
11209
|
-
content: [{ type: "text", text:
|
|
11626
|
+
content: [{ type: "text", text: `${draftLine}
|
|
11627
|
+
No plans found${filterMsg}.` }]
|
|
11210
11628
|
};
|
|
11211
11629
|
}
|
|
11212
|
-
const lines = plans.map((
|
|
11213
|
-
const executed =
|
|
11214
|
-
return `- [${
|
|
11630
|
+
const lines = plans.map((plan) => {
|
|
11631
|
+
const executed = plan.executed_at ? ` | Executed: ${plan.executed_at}` : "";
|
|
11632
|
+
return `- [${plan.status}] ${plan.title} (${plan.operation_count} ops, ${plan.total_record_count} records) | Created: ${plan.created_at}${executed} | ID: ${plan.id}`;
|
|
11215
11633
|
});
|
|
11216
11634
|
const result = [
|
|
11217
11635
|
`Plans for portal ${portalId}:`,
|
|
11218
|
-
|
|
11636
|
+
draftLine,
|
|
11637
|
+
"",
|
|
11219
11638
|
...lines
|
|
11220
11639
|
].join("\n");
|
|
11221
11640
|
return { content: [{ type: "text", text: result }] };
|
|
@@ -11229,182 +11648,566 @@ Statuses:
|
|
|
11229
11648
|
});
|
|
11230
11649
|
}
|
|
11231
11650
|
|
|
11651
|
+
// src/pure/sync-lifecycle.ts
|
|
11652
|
+
var dedupePortalIds2 = (portalIds) => {
|
|
11653
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11654
|
+
const deduped = [];
|
|
11655
|
+
for (const portalId of portalIds) {
|
|
11656
|
+
if (seen.has(portalId)) continue;
|
|
11657
|
+
seen.add(portalId);
|
|
11658
|
+
deduped.push(portalId);
|
|
11659
|
+
}
|
|
11660
|
+
return deduped;
|
|
11661
|
+
};
|
|
11662
|
+
var orderPortalWarmupIds = (portals, selectedPortalId2) => {
|
|
11663
|
+
const portalIds = dedupePortalIds2(portals.map((portal) => portal.target_portal));
|
|
11664
|
+
if (selectedPortalId2 === null || !portalIds.includes(selectedPortalId2)) {
|
|
11665
|
+
return portalIds;
|
|
11666
|
+
}
|
|
11667
|
+
return [selectedPortalId2, ...portalIds.filter((portalId) => portalId !== selectedPortalId2)];
|
|
11668
|
+
};
|
|
11669
|
+
var resolveDiffFlow = (portalId, selectedPortalId2) => selectedPortalId2 === portalId ? "diff_then_subscribe" : "diff";
|
|
11670
|
+
var shouldSubscribeAfterDiffComplete = (portalId, selectedPortalId2, success) => success && selectedPortalId2 === portalId;
|
|
11671
|
+
|
|
11672
|
+
// src/effects/artifact-queue.ts
|
|
11673
|
+
import { Effect as Effect91 } from "effect";
|
|
11674
|
+
var DEFAULT_OPTIONS = {
|
|
11675
|
+
maxRetries: 2,
|
|
11676
|
+
baseBackoffMs: 500
|
|
11677
|
+
};
|
|
11678
|
+
var DEFAULT_PROCESSOR = (portalId, artifact) => Effect91.sync(() => {
|
|
11679
|
+
console.error(
|
|
11680
|
+
`[artifact-queue] No processor configured for portal ${portalId} artifact ${artifact.object_type}`
|
|
11681
|
+
);
|
|
11682
|
+
return false;
|
|
11683
|
+
});
|
|
11684
|
+
var selectedPortalId = null;
|
|
11685
|
+
var inFlightJob = null;
|
|
11686
|
+
var workerRunning = false;
|
|
11687
|
+
var workerGeneration = 0;
|
|
11688
|
+
var options = { ...DEFAULT_OPTIONS };
|
|
11689
|
+
var processArtifact2 = DEFAULT_PROCESSOR;
|
|
11690
|
+
var pendingJobs = [];
|
|
11691
|
+
var activeKeys = /* @__PURE__ */ new Set();
|
|
11692
|
+
var drainWaiters = /* @__PURE__ */ new Map();
|
|
11693
|
+
var makeJobKey = (portalId, artifactId) => `${portalId}:${artifactId}`;
|
|
11694
|
+
var sleep = (ms) => new Promise((resolve) => {
|
|
11695
|
+
setTimeout(resolve, ms);
|
|
11696
|
+
});
|
|
11697
|
+
var isPortalDrained = (portalId) => inFlightJob?.portalId !== portalId && !pendingJobs.some((job) => job.portalId === portalId);
|
|
11698
|
+
var resolvePortalWaiters = (portalId, drained) => {
|
|
11699
|
+
const waiters = drainWaiters.get(portalId);
|
|
11700
|
+
if (!waiters) return;
|
|
11701
|
+
for (const waiter of waiters) {
|
|
11702
|
+
if (waiter.timeoutId !== null) {
|
|
11703
|
+
clearTimeout(waiter.timeoutId);
|
|
11704
|
+
}
|
|
11705
|
+
waiter.resolve(drained);
|
|
11706
|
+
}
|
|
11707
|
+
drainWaiters.delete(portalId);
|
|
11708
|
+
};
|
|
11709
|
+
var maybeResolvePortalDrain = (portalId) => {
|
|
11710
|
+
if (isPortalDrained(portalId)) {
|
|
11711
|
+
resolvePortalWaiters(portalId, true);
|
|
11712
|
+
}
|
|
11713
|
+
};
|
|
11714
|
+
var dequeueNextJob = () => {
|
|
11715
|
+
if (pendingJobs.length === 0) return null;
|
|
11716
|
+
if (selectedPortalId === null) {
|
|
11717
|
+
return pendingJobs.shift() ?? null;
|
|
11718
|
+
}
|
|
11719
|
+
const selectedIdx = pendingJobs.findIndex((job) => job.portalId === selectedPortalId);
|
|
11720
|
+
if (selectedIdx === -1) {
|
|
11721
|
+
return pendingJobs.shift() ?? null;
|
|
11722
|
+
}
|
|
11723
|
+
return pendingJobs.splice(selectedIdx, 1)[0] ?? null;
|
|
11724
|
+
};
|
|
11725
|
+
var processJobWithRetry = async (job, generation) => {
|
|
11726
|
+
const maxAttempts = options.maxRetries + 1;
|
|
11727
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
11728
|
+
if (generation !== workerGeneration) return;
|
|
11729
|
+
const succeeded = await Effect91.runPromise(
|
|
11730
|
+
processArtifact2(job.portalId, job.artifact).pipe(
|
|
11731
|
+
Effect91.catchAll(
|
|
11732
|
+
(error) => Effect91.sync(() => {
|
|
11733
|
+
console.error(
|
|
11734
|
+
`[artifact-queue] Ingest failed for portal ${job.portalId} artifact ${job.artifact.object_type}:`,
|
|
11735
|
+
error
|
|
11736
|
+
);
|
|
11737
|
+
return false;
|
|
11738
|
+
})
|
|
11739
|
+
)
|
|
11740
|
+
)
|
|
11741
|
+
);
|
|
11742
|
+
if (succeeded) return;
|
|
11743
|
+
if (attempt >= maxAttempts) {
|
|
11744
|
+
console.error(
|
|
11745
|
+
`[artifact-queue] Exhausted retries for portal ${job.portalId} artifact ${job.artifact.object_type}`
|
|
11746
|
+
);
|
|
11747
|
+
return;
|
|
11748
|
+
}
|
|
11749
|
+
const backoffMs = options.baseBackoffMs * 2 ** (attempt - 1);
|
|
11750
|
+
console.error(
|
|
11751
|
+
`[artifact-queue] Retry ${attempt}/${options.maxRetries} for portal ${job.portalId} artifact ${job.artifact.object_type} in ${backoffMs}ms`
|
|
11752
|
+
);
|
|
11753
|
+
await sleep(backoffMs);
|
|
11754
|
+
}
|
|
11755
|
+
};
|
|
11756
|
+
var runWorker = async (generation) => {
|
|
11757
|
+
while (generation === workerGeneration) {
|
|
11758
|
+
const job = dequeueNextJob();
|
|
11759
|
+
if (!job) {
|
|
11760
|
+
if (generation === workerGeneration) {
|
|
11761
|
+
workerRunning = false;
|
|
11762
|
+
}
|
|
11763
|
+
return;
|
|
11764
|
+
}
|
|
11765
|
+
inFlightJob = job;
|
|
11766
|
+
try {
|
|
11767
|
+
await processJobWithRetry(job, generation);
|
|
11768
|
+
} catch (error) {
|
|
11769
|
+
console.error("[artifact-queue] Unexpected worker failure:", error);
|
|
11770
|
+
} finally {
|
|
11771
|
+
if (generation !== workerGeneration) return;
|
|
11772
|
+
inFlightJob = null;
|
|
11773
|
+
activeKeys.delete(job.key);
|
|
11774
|
+
maybeResolvePortalDrain(job.portalId);
|
|
11775
|
+
}
|
|
11776
|
+
}
|
|
11777
|
+
};
|
|
11778
|
+
var startWorkerIfNeeded = () => {
|
|
11779
|
+
if (workerRunning || pendingJobs.length === 0) return;
|
|
11780
|
+
workerRunning = true;
|
|
11781
|
+
const generation = workerGeneration;
|
|
11782
|
+
void runWorker(generation);
|
|
11783
|
+
};
|
|
11784
|
+
var configureArtifactQueue = (processor, nextOptions) => {
|
|
11785
|
+
processArtifact2 = processor;
|
|
11786
|
+
options = {
|
|
11787
|
+
...DEFAULT_OPTIONS,
|
|
11788
|
+
...nextOptions ?? {}
|
|
11789
|
+
};
|
|
11790
|
+
startWorkerIfNeeded();
|
|
11791
|
+
};
|
|
11792
|
+
var enqueueArtifacts = (portalId, artifacts) => {
|
|
11793
|
+
let enqueued = 0;
|
|
11794
|
+
for (const artifact of artifacts) {
|
|
11795
|
+
const key = makeJobKey(portalId, artifact.id);
|
|
11796
|
+
if (activeKeys.has(key)) continue;
|
|
11797
|
+
activeKeys.add(key);
|
|
11798
|
+
pendingJobs.push({ portalId, artifact, key });
|
|
11799
|
+
enqueued += 1;
|
|
11800
|
+
}
|
|
11801
|
+
if (enqueued > 0) {
|
|
11802
|
+
startWorkerIfNeeded();
|
|
11803
|
+
}
|
|
11804
|
+
};
|
|
11805
|
+
var waitForPortalDrain = (portalId, timeoutMs) => {
|
|
11806
|
+
if (!hasPendingForPortal(portalId)) {
|
|
11807
|
+
return Promise.resolve(true);
|
|
11808
|
+
}
|
|
11809
|
+
if (timeoutMs <= 0) {
|
|
11810
|
+
return Promise.resolve(false);
|
|
11811
|
+
}
|
|
11812
|
+
return new Promise((resolve) => {
|
|
11813
|
+
const waiter = {
|
|
11814
|
+
resolve,
|
|
11815
|
+
timeoutId: null
|
|
11816
|
+
};
|
|
11817
|
+
const timeoutId = setTimeout(() => {
|
|
11818
|
+
const waiters2 = drainWaiters.get(portalId);
|
|
11819
|
+
if (waiters2) {
|
|
11820
|
+
waiters2.delete(waiter);
|
|
11821
|
+
if (waiters2.size === 0) {
|
|
11822
|
+
drainWaiters.delete(portalId);
|
|
11823
|
+
}
|
|
11824
|
+
}
|
|
11825
|
+
waiter.timeoutId = null;
|
|
11826
|
+
resolve(false);
|
|
11827
|
+
}, timeoutMs);
|
|
11828
|
+
waiter.timeoutId = timeoutId;
|
|
11829
|
+
const waiters = drainWaiters.get(portalId) ?? /* @__PURE__ */ new Set();
|
|
11830
|
+
waiters.add(waiter);
|
|
11831
|
+
drainWaiters.set(portalId, waiters);
|
|
11832
|
+
maybeResolvePortalDrain(portalId);
|
|
11833
|
+
});
|
|
11834
|
+
};
|
|
11835
|
+
var hasPendingForPortal = (portalId) => inFlightJob?.portalId === portalId || pendingJobs.some((job) => job.portalId === portalId);
|
|
11836
|
+
var setSelectedPortal = (portalId) => {
|
|
11837
|
+
selectedPortalId = portalId;
|
|
11838
|
+
};
|
|
11839
|
+
|
|
11840
|
+
// src/effects/ensure-fresh.ts
|
|
11841
|
+
import fs8 from "fs";
|
|
11842
|
+
import { Effect as Effect92, pipe as pipe77 } from "effect";
|
|
11843
|
+
var DEFAULT_MAX_AGE_MS = 3e5;
|
|
11844
|
+
var DEFAULT_QUEUE_DRAIN_TIMEOUT_MS = 12e4;
|
|
11845
|
+
var DEFAULT_DIFF_TIMEOUT_MS = 3e4;
|
|
11846
|
+
var DIFF_POLL_INTERVAL_MS = 100;
|
|
11847
|
+
var LAST_DIFF_CHECKED_AT_KEY = "last_diff_checked_at";
|
|
11848
|
+
var createEnsureFresh = (deps, options2) => {
|
|
11849
|
+
const maxAgeMs = options2?.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
11850
|
+
const queueDrainTimeoutMs = options2?.queueDrainTimeoutMs ?? DEFAULT_QUEUE_DRAIN_TIMEOUT_MS;
|
|
11851
|
+
const diffTimeoutMs = options2?.diffTimeoutMs ?? DEFAULT_DIFF_TIMEOUT_MS;
|
|
11852
|
+
return async (portalId) => {
|
|
11853
|
+
const selectedPortalId2 = deps.getSelectedPortalId();
|
|
11854
|
+
if (selectedPortalId2 === portalId) {
|
|
11855
|
+
return;
|
|
11856
|
+
}
|
|
11857
|
+
const drained = await deps.waitForPortalDrain(portalId, queueDrainTimeoutMs);
|
|
11858
|
+
if (!drained) {
|
|
11859
|
+
throw new Error(`Timed out waiting for portal ${portalId} artifact queue drain.`);
|
|
11860
|
+
}
|
|
11861
|
+
if (!deps.hasLocalDatabase(portalId)) {
|
|
11862
|
+
throw new Error(
|
|
11863
|
+
`Portal ${portalId} has no synced data yet. Background sync in progress - retry shortly.`
|
|
11864
|
+
);
|
|
11865
|
+
}
|
|
11866
|
+
const checkedAt = await deps.getFreshnessCheckedAt(portalId);
|
|
11867
|
+
const stale = checkedAt === null || Date.now() - checkedAt.getTime() > maxAgeMs;
|
|
11868
|
+
if (!stale) {
|
|
11869
|
+
return;
|
|
11870
|
+
}
|
|
11871
|
+
const objectSyncTimes = await deps.getObjectSyncTimes(portalId);
|
|
11872
|
+
if (Object.keys(objectSyncTimes).length === 0) {
|
|
11873
|
+
await deps.setFreshnessCheckedAt(portalId, /* @__PURE__ */ new Date());
|
|
11874
|
+
return;
|
|
11875
|
+
}
|
|
11876
|
+
const diffRequestedAt = Date.now();
|
|
11877
|
+
await deps.sendSyncDiff(portalId, objectSyncTimes);
|
|
11878
|
+
const completed = await deps.waitForDiffCompletion(portalId, diffRequestedAt, diffTimeoutMs);
|
|
11879
|
+
if (!completed) {
|
|
11880
|
+
throw new Error(`Timed out waiting for sync diff completion for portal ${portalId}.`);
|
|
11881
|
+
}
|
|
11882
|
+
await deps.setFreshnessCheckedAt(portalId, /* @__PURE__ */ new Date());
|
|
11883
|
+
};
|
|
11884
|
+
};
|
|
11885
|
+
var parseDate = (value) => {
|
|
11886
|
+
if (!value) return null;
|
|
11887
|
+
const parsed = Date.parse(value);
|
|
11888
|
+
return Number.isNaN(parsed) ? null : new Date(parsed);
|
|
11889
|
+
};
|
|
11890
|
+
var sleep2 = (ms) => new Promise((resolve) => {
|
|
11891
|
+
setTimeout(resolve, ms);
|
|
11892
|
+
});
|
|
11893
|
+
var runDatabaseForPortal = (portalId, getEncryptionKey, run) => Effect92.runPromise(
|
|
11894
|
+
pipe77(
|
|
11895
|
+
DatabaseService,
|
|
11896
|
+
Effect92.flatMap((db) => run(db)),
|
|
11897
|
+
Effect92.provide(makeDatabaseLive(portalId, getEncryptionKey()))
|
|
11898
|
+
)
|
|
11899
|
+
);
|
|
11900
|
+
var waitForPortalSyncedAtAfter = (portalState, portalId, sinceMs, timeoutMs) => (async () => {
|
|
11901
|
+
const startedAt = Date.now();
|
|
11902
|
+
while (Date.now() - startedAt <= timeoutMs) {
|
|
11903
|
+
const syncedAtMs = await Effect92.runPromise(
|
|
11904
|
+
pipe77(
|
|
11905
|
+
portalState.get(portalId),
|
|
11906
|
+
Effect92.map((state) => state.syncedAt.getTime()),
|
|
11907
|
+
Effect92.catchAll(() => Effect92.succeed(0))
|
|
11908
|
+
)
|
|
11909
|
+
);
|
|
11910
|
+
if (syncedAtMs > sinceMs) {
|
|
11911
|
+
return true;
|
|
11912
|
+
}
|
|
11913
|
+
await sleep2(DIFF_POLL_INTERVAL_MS);
|
|
11914
|
+
}
|
|
11915
|
+
return false;
|
|
11916
|
+
})();
|
|
11917
|
+
var makeEnsureFresh = (deps, options2) => createEnsureFresh(
|
|
11918
|
+
{
|
|
11919
|
+
getSelectedPortalId: deps.getSelectedPortalId,
|
|
11920
|
+
waitForPortalDrain,
|
|
11921
|
+
hasLocalDatabase: (portalId) => fs8.existsSync(dbPath(portalId)),
|
|
11922
|
+
getFreshnessCheckedAt: (portalId) => runDatabaseForPortal(
|
|
11923
|
+
portalId,
|
|
11924
|
+
deps.getEncryptionKey,
|
|
11925
|
+
(db) => pipe77(
|
|
11926
|
+
db.getMetadata(LAST_DIFF_CHECKED_AT_KEY),
|
|
11927
|
+
Effect92.map((value) => parseDate(value))
|
|
11928
|
+
)
|
|
11929
|
+
),
|
|
11930
|
+
getObjectSyncTimes: (portalId) => runDatabaseForPortal(portalId, deps.getEncryptionKey, (db) => db.getObjectSyncTimes()),
|
|
11931
|
+
sendSyncDiff: (portalId, lastSyncTimes) => Effect92.runPromise(
|
|
11932
|
+
deps.ws.sendSyncDiff(portalId, lastSyncTimes).pipe(
|
|
11933
|
+
Effect92.mapError(
|
|
11934
|
+
(error) => error instanceof Error ? error : new Error(`Failed to request sync diff for portal ${portalId}`)
|
|
11935
|
+
)
|
|
11936
|
+
)
|
|
11937
|
+
).then(() => void 0),
|
|
11938
|
+
waitForDiffCompletion: (portalId, sinceMs, timeoutMs) => waitForPortalSyncedAtAfter(deps.portalState, portalId, sinceMs, timeoutMs),
|
|
11939
|
+
setFreshnessCheckedAt: (portalId, checkedAt) => runDatabaseForPortal(
|
|
11940
|
+
portalId,
|
|
11941
|
+
deps.getEncryptionKey,
|
|
11942
|
+
(db) => db.setMetadata(LAST_DIFF_CHECKED_AT_KEY, checkedAt.toISOString())
|
|
11943
|
+
).then(() => void 0)
|
|
11944
|
+
},
|
|
11945
|
+
options2
|
|
11946
|
+
);
|
|
11947
|
+
|
|
11232
11948
|
// src/index.ts
|
|
11233
11949
|
var server = new McpServer({
|
|
11234
11950
|
name: "mcp-pro-client",
|
|
11235
11951
|
version: "1.0.0"
|
|
11236
11952
|
});
|
|
11237
|
-
var
|
|
11953
|
+
var QUEUE_DRAIN_TIMEOUT_MS = 12e4;
|
|
11954
|
+
var mainProgram = Effect93.gen(function* () {
|
|
11238
11955
|
const ws = yield* WebSocketService;
|
|
11239
11956
|
const config = yield* ConfigService;
|
|
11240
11957
|
const portalState = yield* PortalStateService;
|
|
11241
11958
|
const getPortalId = () => getSelectedPortalId(config);
|
|
11242
|
-
const
|
|
11243
|
-
|
|
11244
|
-
|
|
11245
|
-
|
|
11959
|
+
const getPortals = () => Effect93.runSync(ws.getPortals());
|
|
11960
|
+
const getEncryptionKey = () => Effect93.runSync(ws.getEncryptionKey());
|
|
11961
|
+
const ensureFresh = makeEnsureFresh({
|
|
11962
|
+
getSelectedPortalId: getPortalId,
|
|
11963
|
+
getEncryptionKey,
|
|
11964
|
+
ws,
|
|
11965
|
+
portalState
|
|
11966
|
+
});
|
|
11967
|
+
const handoffSelectedPortal = (nextPortalId) => {
|
|
11968
|
+
const previousPortalId = getSelectedPortalId(config);
|
|
11969
|
+
if (previousPortalId !== null && previousPortalId !== nextPortalId) {
|
|
11970
|
+
Effect93.runSync(
|
|
11971
|
+
pipe78(
|
|
11972
|
+
ws.sendSyncUnsubscribe(previousPortalId),
|
|
11973
|
+
Effect93.catchAll(() => Effect93.void)
|
|
11974
|
+
)
|
|
11975
|
+
);
|
|
11976
|
+
}
|
|
11977
|
+
setSelectedPortal(nextPortalId);
|
|
11978
|
+
Effect93.runSync(config.save({ selectedPortalId: nextPortalId }));
|
|
11979
|
+
Effect93.runFork(pipe78(ws.sendSyncFull(nextPortalId), Effect93.catchAll(() => Effect93.void)));
|
|
11980
|
+
};
|
|
11981
|
+
registerPortalSelection(server, { ws, config, handoffSelectedPortal });
|
|
11982
|
+
registerQueryTool(server, {
|
|
11983
|
+
getSelectedPortalId: getPortalId,
|
|
11984
|
+
getPortals,
|
|
11985
|
+
ensureFresh,
|
|
11986
|
+
getEncryptionKey
|
|
11987
|
+
});
|
|
11988
|
+
registerChartTool(server, {
|
|
11989
|
+
getSelectedPortalId: getPortalId,
|
|
11990
|
+
getPortals,
|
|
11991
|
+
ensureFresh,
|
|
11992
|
+
getEncryptionKey
|
|
11993
|
+
});
|
|
11246
11994
|
registerStatusTool(server, {
|
|
11247
|
-
getConnectionState: () =>
|
|
11248
|
-
getPortals
|
|
11995
|
+
getConnectionState: () => Effect93.runSync(ws.getState()),
|
|
11996
|
+
getPortals,
|
|
11249
11997
|
getSelectedPortalId: getPortalId,
|
|
11998
|
+
ensureFresh,
|
|
11250
11999
|
getEncryptionKey
|
|
11251
12000
|
});
|
|
11252
|
-
registerBuildPlanTool(server
|
|
12001
|
+
registerBuildPlanTool(server, {
|
|
12002
|
+
getSelectedPortalId: getPortalId,
|
|
12003
|
+
getPortals
|
|
12004
|
+
});
|
|
11253
12005
|
registerDescribeOperationsTool(server);
|
|
11254
|
-
registerSubmitPlanTool(server, {
|
|
11255
|
-
|
|
12006
|
+
registerSubmitPlanTool(server, {
|
|
12007
|
+
getSelectedPortalId: getPortalId,
|
|
12008
|
+
getPortals,
|
|
12009
|
+
ws
|
|
12010
|
+
});
|
|
12011
|
+
registerGetPlansTool(server, {
|
|
12012
|
+
getSelectedPortalId: getPortalId,
|
|
12013
|
+
getPortals,
|
|
12014
|
+
ws
|
|
12015
|
+
});
|
|
11256
12016
|
const state = yield* config.load();
|
|
11257
12017
|
yield* config.save(state);
|
|
12018
|
+
setSelectedPortal(state.selectedPortalId);
|
|
11258
12019
|
if (state.selectedPortalId !== null) {
|
|
11259
|
-
const cached = yield* portalState.load(state.selectedPortalId).pipe(
|
|
12020
|
+
const cached = yield* portalState.load(state.selectedPortalId).pipe(Effect93.catchAll(() => Effect93.succeed(null)));
|
|
11260
12021
|
if (cached !== null) {
|
|
11261
|
-
yield*
|
|
11262
|
-
yield* Effect91.sync(
|
|
12022
|
+
yield* Effect93.sync(
|
|
11263
12023
|
() => console.error(`[startup] Loaded ${cached.artifacts.length} cached artifacts for portal ${state.selectedPortalId}`)
|
|
11264
12024
|
);
|
|
11265
12025
|
}
|
|
11266
12026
|
}
|
|
12027
|
+
const pendingPortalFollowUps = /* @__PURE__ */ new Set();
|
|
12028
|
+
const runDiffFlow = (portalId) => {
|
|
12029
|
+
const selectedPortalId2 = getSelectedPortalId(config);
|
|
12030
|
+
const flow = resolveDiffFlow(portalId, selectedPortalId2);
|
|
12031
|
+
if (flow === "diff_then_subscribe") {
|
|
12032
|
+
triggerDiffThenSubscribe(portalId);
|
|
12033
|
+
return;
|
|
12034
|
+
}
|
|
12035
|
+
triggerDiff(portalId);
|
|
12036
|
+
};
|
|
12037
|
+
const schedulePortalFollowUpAfterDrain = (portalId, source) => {
|
|
12038
|
+
if (pendingPortalFollowUps.has(portalId)) {
|
|
12039
|
+
console.error(`[live-sync] Follow-up already scheduled for portal ${portalId}, coalescing ${source}`);
|
|
12040
|
+
return;
|
|
12041
|
+
}
|
|
12042
|
+
pendingPortalFollowUps.add(portalId);
|
|
12043
|
+
Effect93.runFork(
|
|
12044
|
+
pipe78(
|
|
12045
|
+
Effect93.promise(() => waitForPortalDrain(portalId, QUEUE_DRAIN_TIMEOUT_MS)),
|
|
12046
|
+
Effect93.flatMap(
|
|
12047
|
+
(drained) => drained ? Effect93.sync(() => {
|
|
12048
|
+
console.error(`[live-sync] Queue drained for portal ${portalId} after ${source} \u2014 triggering diff flow`);
|
|
12049
|
+
runDiffFlow(portalId);
|
|
12050
|
+
}) : Effect93.sync(
|
|
12051
|
+
() => console.error(`[live-sync] Timed out waiting for portal ${portalId} queue drain after ${source}`)
|
|
12052
|
+
)
|
|
12053
|
+
),
|
|
12054
|
+
Effect93.ensuring(
|
|
12055
|
+
Effect93.sync(() => {
|
|
12056
|
+
pendingPortalFollowUps.delete(portalId);
|
|
12057
|
+
})
|
|
12058
|
+
),
|
|
12059
|
+
Effect93.catchAll(
|
|
12060
|
+
(error) => Effect93.sync(() => {
|
|
12061
|
+
console.error(`[live-sync] Failed post-queue follow-up for portal ${portalId}:`, error);
|
|
12062
|
+
})
|
|
12063
|
+
)
|
|
12064
|
+
)
|
|
12065
|
+
);
|
|
12066
|
+
};
|
|
12067
|
+
const subscribeIfStillSelected = (portalId) => {
|
|
12068
|
+
const selectedPortalId2 = getSelectedPortalId(config);
|
|
12069
|
+
if (selectedPortalId2 !== portalId) {
|
|
12070
|
+
console.error(
|
|
12071
|
+
`[live-sync] Skipping subscribe for portal ${portalId} because selected portal is ${selectedPortalId2 ?? "none"}`
|
|
12072
|
+
);
|
|
12073
|
+
return;
|
|
12074
|
+
}
|
|
12075
|
+
Effect93.runFork(pipe78(ws.sendSyncSubscribe(portalId), Effect93.catchAll(() => Effect93.void)));
|
|
12076
|
+
};
|
|
12077
|
+
const triggerDiff = (portalId) => {
|
|
12078
|
+
const dbLayer = makeDatabaseLive(portalId, getEncryptionKey());
|
|
12079
|
+
pipe78(
|
|
12080
|
+
DatabaseService,
|
|
12081
|
+
Effect93.flatMap((db) => db.getObjectSyncTimes()),
|
|
12082
|
+
Effect93.provide(dbLayer),
|
|
12083
|
+
Effect93.catchAll(() => Effect93.succeed({})),
|
|
12084
|
+
Effect93.flatMap((syncTimes) => {
|
|
12085
|
+
const count = Object.keys(syncTimes).length;
|
|
12086
|
+
if (count === 0) {
|
|
12087
|
+
return Effect93.sync(() => {
|
|
12088
|
+
console.error(`[live-sync] No watermarks found in DuckDB for portal ${portalId}, skipping diff`);
|
|
12089
|
+
});
|
|
12090
|
+
}
|
|
12091
|
+
return pipe78(
|
|
12092
|
+
Effect93.sync(() => {
|
|
12093
|
+
console.error(`[live-sync] Triggering catch-up diff for portal ${portalId} with ${count} object types: ${JSON.stringify(syncTimes)}`);
|
|
12094
|
+
}),
|
|
12095
|
+
Effect93.flatMap(() => pipe78(ws.sendSyncDiff(portalId, syncTimes), Effect93.catchAll(() => Effect93.void)))
|
|
12096
|
+
);
|
|
12097
|
+
}),
|
|
12098
|
+
Effect93.runFork
|
|
12099
|
+
);
|
|
12100
|
+
};
|
|
11267
12101
|
const triggerDiffThenSubscribe = (portalId) => {
|
|
11268
12102
|
const dbLayer = makeDatabaseLive(portalId, getEncryptionKey());
|
|
11269
|
-
|
|
12103
|
+
pipe78(
|
|
11270
12104
|
DatabaseService,
|
|
11271
|
-
|
|
11272
|
-
|
|
11273
|
-
|
|
11274
|
-
|
|
11275
|
-
|
|
11276
|
-
|
|
11277
|
-
|
|
12105
|
+
Effect93.flatMap((db) => db.getObjectSyncTimes()),
|
|
12106
|
+
Effect93.provide(dbLayer),
|
|
12107
|
+
Effect93.catchAll(() => Effect93.succeed({})),
|
|
12108
|
+
Effect93.flatMap((syncTimes) => {
|
|
12109
|
+
const count = Object.keys(syncTimes).length;
|
|
12110
|
+
if (count === 0) {
|
|
12111
|
+
return Effect93.sync(() => {
|
|
12112
|
+
console.error(`[live-sync] No watermarks found in DuckDB for portal ${portalId}, subscribing directly`);
|
|
12113
|
+
subscribeIfStillSelected(portalId);
|
|
12114
|
+
});
|
|
12115
|
+
}
|
|
12116
|
+
return pipe78(
|
|
12117
|
+
Effect93.sync(() => {
|
|
11278
12118
|
console.error(`[live-sync] Triggering catch-up diff for portal ${portalId} with ${count} object types: ${JSON.stringify(syncTimes)}`);
|
|
11279
|
-
|
|
11280
|
-
|
|
11281
|
-
|
|
11282
|
-
|
|
11283
|
-
|
|
11284
|
-
})
|
|
11285
|
-
),
|
|
11286
|
-
Effect91.runFork
|
|
12119
|
+
}),
|
|
12120
|
+
Effect93.flatMap(() => pipe78(ws.sendSyncDiff(portalId, syncTimes), Effect93.catchAll(() => Effect93.void)))
|
|
12121
|
+
);
|
|
12122
|
+
}),
|
|
12123
|
+
Effect93.runFork
|
|
11287
12124
|
);
|
|
11288
12125
|
};
|
|
12126
|
+
const makeSyncLayer = (portalId) => {
|
|
12127
|
+
const dbLayer = makeDatabaseLive(portalId, getEncryptionKey());
|
|
12128
|
+
const wsLayer = Layer6.succeed(WebSocketService, ws);
|
|
12129
|
+
const configLayer = Layer6.succeed(ConfigService, config);
|
|
12130
|
+
const portalStateLayer = Layer6.succeed(PortalStateService, portalState);
|
|
12131
|
+
return SyncLive.pipe(
|
|
12132
|
+
Layer6.provide(Layer6.mergeAll(wsLayer, dbLayer, configLayer, portalStateLayer))
|
|
12133
|
+
);
|
|
12134
|
+
};
|
|
12135
|
+
configureArtifactQueue(
|
|
12136
|
+
(portalId, artifact) => pipe78(
|
|
12137
|
+
SyncService,
|
|
12138
|
+
Effect93.flatMap((sync) => sync.syncArtifacts(portalId, [artifact], [artifact.object_type])),
|
|
12139
|
+
Effect93.flatMap(() => portalState.get(portalId)),
|
|
12140
|
+
Effect93.map((portal) => {
|
|
12141
|
+
const status = portal.artifacts.find((a) => a.object_type === artifact.object_type)?.status;
|
|
12142
|
+
if (status !== "SYNCED") {
|
|
12143
|
+
console.error(
|
|
12144
|
+
`[artifact-queue] Artifact ${artifact.object_type} for portal ${portalId} finished with status ${status ?? "MISSING"}`
|
|
12145
|
+
);
|
|
12146
|
+
}
|
|
12147
|
+
return status === "SYNCED";
|
|
12148
|
+
}),
|
|
12149
|
+
Effect93.catchAll(
|
|
12150
|
+
(err) => Effect93.sync(() => {
|
|
12151
|
+
console.error(
|
|
12152
|
+
`[artifact-queue] Failed ingest for portal ${portalId} artifact ${artifact.object_type}:`,
|
|
12153
|
+
err
|
|
12154
|
+
);
|
|
12155
|
+
return false;
|
|
12156
|
+
})
|
|
12157
|
+
),
|
|
12158
|
+
Effect93.provide(makeSyncLayer(portalId))
|
|
12159
|
+
)
|
|
12160
|
+
);
|
|
11289
12161
|
yield* ws.registerHandlers({
|
|
11290
12162
|
onSyncArtifacts: (portalId, artifacts) => {
|
|
11291
12163
|
const incomingWithStatus = artifacts.map((a) => ({ ...a, status: "NOT_STARTED" }));
|
|
11292
|
-
const updated =
|
|
12164
|
+
const updated = pipe78(
|
|
11293
12165
|
portalState.setArtifacts(portalId, incomingWithStatus),
|
|
11294
|
-
|
|
11295
|
-
|
|
11296
|
-
(s) => Effect91.sync(
|
|
12166
|
+
Effect93.tap(
|
|
12167
|
+
(s) => Effect93.sync(
|
|
11297
12168
|
() => console.error(`[sync:full] Saved ${s.artifacts.length} artifacts for portal ${portalId}`)
|
|
11298
12169
|
)
|
|
11299
12170
|
),
|
|
11300
|
-
|
|
11301
|
-
|
|
12171
|
+
Effect93.catchAll(() => Effect93.succeed(null)),
|
|
12172
|
+
Effect93.runSync
|
|
11302
12173
|
);
|
|
11303
12174
|
if (!updated) return;
|
|
11304
12175
|
const objectTypesToSync = incomingWithStatus.map((a) => a.object_type);
|
|
11305
12176
|
const toSync = filterUnsyncedArtifacts(updated.artifacts, new Set(objectTypesToSync));
|
|
11306
12177
|
if (toSync.length === 0) {
|
|
11307
|
-
console.error(
|
|
11308
|
-
|
|
11309
|
-
const wsLayer2 = Layer6.succeed(WebSocketService, ws);
|
|
11310
|
-
const configLayer2 = Layer6.succeed(ConfigService, config);
|
|
11311
|
-
const portalStateLayer2 = Layer6.succeed(PortalStateService, portalState);
|
|
11312
|
-
const syncLayer2 = SyncLive.pipe(
|
|
11313
|
-
Layer6.provide(Layer6.mergeAll(wsLayer2, dbLayer2, configLayer2, portalStateLayer2))
|
|
12178
|
+
console.error(
|
|
12179
|
+
`[artifact-queue] All ${artifacts.length} artifacts already SYNCED for portal ${portalId} \u2014 no enqueue needed`
|
|
11314
12180
|
);
|
|
11315
|
-
|
|
11316
|
-
|
|
11317
|
-
|
|
11318
|
-
Effect91.tap(
|
|
11319
|
-
() => Effect91.sync(() => {
|
|
11320
|
-
console.error(`[live-sync] Message plugins synced for portal ${portalId} \u2014 starting catch-up diff`);
|
|
11321
|
-
triggerDiffThenSubscribe(portalId);
|
|
11322
|
-
})
|
|
11323
|
-
),
|
|
11324
|
-
Effect91.catchAll(
|
|
11325
|
-
(err) => Effect91.sync(() => {
|
|
11326
|
-
console.error(`[live-sync] Message plugin sync failed:`, err);
|
|
11327
|
-
triggerDiffThenSubscribe(portalId);
|
|
11328
|
-
})
|
|
11329
|
-
),
|
|
11330
|
-
Effect91.provide(syncLayer2),
|
|
11331
|
-
Effect91.runFork
|
|
12181
|
+
} else {
|
|
12182
|
+
console.error(
|
|
12183
|
+
`[artifact-queue] Enqueueing ${toSync.length}/${artifacts.length} artifacts for portal ${portalId}`
|
|
11332
12184
|
);
|
|
11333
|
-
|
|
12185
|
+
enqueueArtifacts(portalId, toSync);
|
|
11334
12186
|
}
|
|
11335
|
-
|
|
11336
|
-
const dbLayer = makeDatabaseLive(portalId, getEncryptionKey());
|
|
11337
|
-
const wsLayer = Layer6.succeed(WebSocketService, ws);
|
|
11338
|
-
const configLayer = Layer6.succeed(ConfigService, config);
|
|
11339
|
-
const portalStateLayer = Layer6.succeed(PortalStateService, portalState);
|
|
11340
|
-
const syncLayer = SyncLive.pipe(
|
|
11341
|
-
Layer6.provide(Layer6.mergeAll(wsLayer, dbLayer, configLayer, portalStateLayer))
|
|
11342
|
-
);
|
|
11343
|
-
pipe77(
|
|
11344
|
-
SyncService,
|
|
11345
|
-
Effect91.flatMap(
|
|
11346
|
-
(sync) => Effect91.all([
|
|
11347
|
-
sync.syncArtifacts(portalId, updated.artifacts, objectTypesToSync).pipe(
|
|
11348
|
-
Effect91.tap(
|
|
11349
|
-
(progress) => Effect91.sync(() => {
|
|
11350
|
-
console.error(`[sync] Complete: ${progress.completedArtifacts}/${progress.totalArtifacts} artifacts synced`);
|
|
11351
|
-
console.error(`[live-sync] Artifact sync done for portal ${portalId} \u2014 starting catch-up diff`);
|
|
11352
|
-
triggerDiffThenSubscribe(portalId);
|
|
11353
|
-
})
|
|
11354
|
-
),
|
|
11355
|
-
Effect91.catchAll(
|
|
11356
|
-
(err) => Effect91.sync(() => console.error(`[sync] Sync failed:`, err))
|
|
11357
|
-
)
|
|
11358
|
-
),
|
|
11359
|
-
sync.syncMessagePlugins(portalId).pipe(
|
|
11360
|
-
Effect91.tap(
|
|
11361
|
-
() => Effect91.sync(() => console.error(`[sync] Message plugins synced independently for portal ${portalId}`))
|
|
11362
|
-
),
|
|
11363
|
-
Effect91.catchAll(
|
|
11364
|
-
(err) => Effect91.sync(() => console.error(`[sync] Message plugin sync failed:`, err))
|
|
11365
|
-
)
|
|
11366
|
-
)
|
|
11367
|
-
], { concurrency: 2 })
|
|
11368
|
-
),
|
|
11369
|
-
Effect91.catchAll(() => Effect91.void),
|
|
11370
|
-
Effect91.provide(syncLayer),
|
|
11371
|
-
Effect91.runFork
|
|
11372
|
-
);
|
|
12187
|
+
schedulePortalFollowUpAfterDrain(portalId, "sync:full");
|
|
11373
12188
|
},
|
|
11374
12189
|
onNewArtifacts: (portalId, artifacts) => {
|
|
11375
12190
|
console.error(`[sync:new-artifacts] Discovered ${artifacts.length} new artifact(s) for portal ${portalId}: ${artifacts.map((a) => a.object_type).join(", ")}`);
|
|
11376
12191
|
const newWithStatus = artifacts.map((a) => ({ ...a, status: "NOT_STARTED" }));
|
|
11377
12192
|
const objectTypesToSync = newWithStatus.map((a) => a.object_type);
|
|
11378
|
-
const merged =
|
|
12193
|
+
const merged = pipe78(
|
|
11379
12194
|
portalState.mergeArtifacts(portalId, newWithStatus),
|
|
11380
|
-
|
|
11381
|
-
|
|
11382
|
-
Effect91.runSync
|
|
12195
|
+
Effect93.catchAll(() => Effect93.succeed(null)),
|
|
12196
|
+
Effect93.runSync
|
|
11383
12197
|
);
|
|
11384
12198
|
if (!merged) return;
|
|
11385
|
-
const
|
|
11386
|
-
|
|
11387
|
-
|
|
11388
|
-
|
|
11389
|
-
|
|
11390
|
-
|
|
11391
|
-
|
|
11392
|
-
|
|
11393
|
-
|
|
11394
|
-
|
|
11395
|
-
|
|
11396
|
-
|
|
11397
|
-
console.error(`[sync:new-artifacts] Download complete: ${progress.completedArtifacts}/${progress.totalArtifacts} new artifacts synced`);
|
|
11398
|
-
console.error(`[sync:new-artifacts] Triggering diff for new types on portal ${portalId}`);
|
|
11399
|
-
triggerDiffThenSubscribe(portalId);
|
|
11400
|
-
})
|
|
11401
|
-
),
|
|
11402
|
-
Effect91.catchAll(
|
|
11403
|
-
(err) => Effect91.sync(() => console.error(`[sync:new-artifacts] Sync failed:`, err))
|
|
11404
|
-
),
|
|
11405
|
-
Effect91.provide(syncLayer),
|
|
11406
|
-
Effect91.runFork
|
|
11407
|
-
);
|
|
12199
|
+
const toSync = filterUnsyncedArtifacts(merged.artifacts, new Set(objectTypesToSync));
|
|
12200
|
+
if (toSync.length === 0) {
|
|
12201
|
+
console.error(
|
|
12202
|
+
`[artifact-queue] All ${artifacts.length} new artifacts already SYNCED for portal ${portalId} \u2014 no enqueue needed`
|
|
12203
|
+
);
|
|
12204
|
+
} else {
|
|
12205
|
+
console.error(
|
|
12206
|
+
`[artifact-queue] Enqueueing ${toSync.length}/${artifacts.length} new artifact(s) for portal ${portalId}`
|
|
12207
|
+
);
|
|
12208
|
+
enqueueArtifacts(portalId, toSync);
|
|
12209
|
+
}
|
|
12210
|
+
schedulePortalFollowUpAfterDrain(portalId, "sync:new-artifacts");
|
|
11408
12211
|
},
|
|
11409
12212
|
onDiffBatch: (portalId, batch) => {
|
|
11410
12213
|
const plugin = findArtifactPlugin(batch.object_type);
|
|
@@ -11413,54 +12216,82 @@ var mainProgram = Effect91.gen(function* () {
|
|
|
11413
12216
|
return;
|
|
11414
12217
|
}
|
|
11415
12218
|
const dbLayer = makeDatabaseLive(portalId, getEncryptionKey());
|
|
11416
|
-
|
|
12219
|
+
pipe78(
|
|
11417
12220
|
DatabaseService,
|
|
11418
|
-
|
|
12221
|
+
Effect93.flatMap((db) => {
|
|
11419
12222
|
const ctx = { db, ws, portalState, portalId };
|
|
11420
12223
|
return plugin.processDiffBatch(ctx, batch);
|
|
11421
12224
|
}),
|
|
11422
|
-
|
|
11423
|
-
(err) =>
|
|
12225
|
+
Effect93.catchAll(
|
|
12226
|
+
(err) => Effect93.sync(() => console.error(`[diff-sync] Error processing batch:`, err))
|
|
11424
12227
|
),
|
|
11425
|
-
|
|
11426
|
-
|
|
12228
|
+
Effect93.provide(dbLayer),
|
|
12229
|
+
Effect93.runFork
|
|
11427
12230
|
);
|
|
11428
12231
|
},
|
|
11429
12232
|
onDiffComplete: (portalId, payload) => {
|
|
11430
|
-
|
|
11431
|
-
|
|
12233
|
+
if (!payload.success) {
|
|
12234
|
+
console.error(`[live-sync] Catch-up diff failed for portal ${portalId}: ${payload.error ?? "unknown"}`);
|
|
12235
|
+
return;
|
|
12236
|
+
}
|
|
12237
|
+
Effect93.runFork(pipe78(portalState.touchSyncedAt(portalId), Effect93.catchAll(() => Effect93.void)));
|
|
12238
|
+
const selectedPortalId2 = getSelectedPortalId(config);
|
|
12239
|
+
const shouldSubscribe = shouldSubscribeAfterDiffComplete(portalId, selectedPortalId2, payload.success);
|
|
12240
|
+
if (!shouldSubscribe) {
|
|
12241
|
+
console.error(
|
|
12242
|
+
`[live-sync] Catch-up diff complete for portal ${portalId} \u2014 ${payload.object_types_processed.length} types, ${payload.total_records} records, ${payload.total_associations} associations \u2014 not subscribing (selected portal: ${selectedPortalId2 ?? "none"})`
|
|
12243
|
+
);
|
|
12244
|
+
return;
|
|
12245
|
+
}
|
|
12246
|
+
console.error(
|
|
12247
|
+
`[live-sync] Catch-up diff complete for portal ${portalId} \u2014 ${payload.object_types_processed.length} types, ${payload.total_records} records, ${payload.total_associations} associations \u2014 subscribing for real-time updates`
|
|
12248
|
+
);
|
|
12249
|
+
Effect93.runFork(pipe78(ws.sendSyncSubscribe(portalId), Effect93.catchAll(() => Effect93.void)));
|
|
11432
12250
|
},
|
|
11433
12251
|
onSubscribeAck: (_payload) => {
|
|
11434
12252
|
},
|
|
11435
|
-
autoSelectSinglePortal: (portals) =>
|
|
12253
|
+
autoSelectSinglePortal: (portals) => {
|
|
12254
|
+
const autoSelected = autoSelectSinglePortal(config, portals);
|
|
12255
|
+
if (autoSelected !== null) {
|
|
12256
|
+
setSelectedPortal(autoSelected);
|
|
12257
|
+
}
|
|
12258
|
+
return autoSelected;
|
|
12259
|
+
},
|
|
11436
12260
|
onRegisterSuccess: (portals) => {
|
|
11437
|
-
|
|
11438
|
-
|
|
11439
|
-
const
|
|
11440
|
-
if (!
|
|
11441
|
-
console.error(`[live-sync] Previously selected portal ${
|
|
11442
|
-
|
|
12261
|
+
const selectedPortalId2 = getSelectedPortalId(config);
|
|
12262
|
+
setSelectedPortal(selectedPortalId2);
|
|
12263
|
+
const warmupPortalIds = orderPortalWarmupIds(portals, selectedPortalId2);
|
|
12264
|
+
if (selectedPortalId2 !== null && !warmupPortalIds.includes(selectedPortalId2)) {
|
|
12265
|
+
console.error(`[live-sync] Previously selected portal ${selectedPortalId2} not in portal list; warming available portals only`);
|
|
12266
|
+
}
|
|
12267
|
+
console.error(`[live-sync] Registration complete \u2014 warming ${warmupPortalIds.length} portal(s): ${warmupPortalIds.join(", ")}`);
|
|
12268
|
+
for (const portalId of warmupPortalIds) {
|
|
12269
|
+
Effect93.runFork(pipe78(ws.sendSyncFull(portalId), Effect93.catchAll(() => Effect93.void)));
|
|
11443
12270
|
}
|
|
11444
|
-
console.error(`[live-sync] Portal ${portalId} reconnected \u2014 sending sync:full to refresh artifacts`);
|
|
11445
|
-
Effect91.runFork(pipe77(ws.sendSyncFull(portalId), Effect91.catchAll(() => Effect91.void)));
|
|
11446
12271
|
},
|
|
11447
12272
|
onSyncDiffNeeded: (portalId) => {
|
|
11448
|
-
|
|
11449
|
-
|
|
12273
|
+
const selectedPortalId2 = getSelectedPortalId(config);
|
|
12274
|
+
const flow = resolveDiffFlow(portalId, selectedPortalId2);
|
|
12275
|
+
console.error(`[live-sync] onSyncDiffNeeded called for portal ${portalId} \u2014 running ${flow}`);
|
|
12276
|
+
if (flow === "diff_then_subscribe") {
|
|
12277
|
+
triggerDiffThenSubscribe(portalId);
|
|
12278
|
+
return;
|
|
12279
|
+
}
|
|
12280
|
+
triggerDiff(portalId);
|
|
11450
12281
|
}
|
|
11451
12282
|
});
|
|
11452
12283
|
yield* ws.connect().pipe(
|
|
11453
|
-
|
|
12284
|
+
Effect93.catchAll((e) => Effect93.sync(() => console.error("WebSocket error:", e)))
|
|
11454
12285
|
);
|
|
11455
12286
|
const transport = new StdioServerTransport();
|
|
11456
|
-
yield*
|
|
11457
|
-
yield*
|
|
11458
|
-
yield*
|
|
12287
|
+
yield* Effect93.promise(() => server.connect(transport));
|
|
12288
|
+
yield* Effect93.sync(() => console.error("MCP Pro Client server started"));
|
|
12289
|
+
yield* Effect93.never;
|
|
11459
12290
|
});
|
|
11460
|
-
|
|
12291
|
+
pipe78(
|
|
11461
12292
|
mainProgram,
|
|
11462
|
-
|
|
11463
|
-
|
|
12293
|
+
Effect93.provide(Layer6.mergeAll(WebSocketLive, ConfigLive, PortalStateLive)),
|
|
12294
|
+
Effect93.runPromise
|
|
11464
12295
|
).catch((error) => {
|
|
11465
12296
|
console.error("Fatal error:", error);
|
|
11466
12297
|
process.exit(1);
|