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