@daeda/mcp-pro 0.1.29 → 0.1.31
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 +346 -44
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1549,6 +1549,7 @@ var assertWritable = (masterLock) => {
|
|
|
1549
1549
|
}
|
|
1550
1550
|
};
|
|
1551
1551
|
var escapeSqlString = (value) => value.replace(/'/g, "''");
|
|
1552
|
+
var escapeSqlIdentifier = (value) => `"${value.replace(/"/g, '""')}"`;
|
|
1552
1553
|
var buildSqlPlaceholders = (count, startAt = 1) => Array.from({ length: count }, (_, index) => `$${index + startAt}`).join(",");
|
|
1553
1554
|
var buildDatabaseCacheKey = (portalId, mode, encryptionKey) => `${portalId}:${mode}:${encryptionKey ?? ""}`;
|
|
1554
1555
|
var makeAssociationStagingTableName = (seed = Date.now()) => `_staging_assoc_sync_${seed}`;
|
|
@@ -1712,6 +1713,20 @@ async function openDatabaseConnection(portalId, _encryptionKey, options2) {
|
|
|
1712
1713
|
if (mode === "read_only" && !fs3.existsSync(sourcePath)) {
|
|
1713
1714
|
throw new Error(`Replica database not found for portal ${portalId}: ${sourcePath}`);
|
|
1714
1715
|
}
|
|
1716
|
+
if (mode === "read_only") {
|
|
1717
|
+
const instance2 = await DuckDBInstance.create(":memory:");
|
|
1718
|
+
const connection2 = await instance2.connect();
|
|
1719
|
+
const escapedPath = escapeSqlString(sourcePath);
|
|
1720
|
+
await connection2.run(`ATTACH '${escapedPath}' AS portal_replica (READ_ONLY)`);
|
|
1721
|
+
await connection2.run("USE portal_replica");
|
|
1722
|
+
return {
|
|
1723
|
+
instance: instance2,
|
|
1724
|
+
connection: connection2,
|
|
1725
|
+
mode,
|
|
1726
|
+
sourcePath,
|
|
1727
|
+
replicaFingerprint: getReplicaFingerprint(portalId)
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1715
1730
|
const instance = await DuckDBInstance.create(sourcePath);
|
|
1716
1731
|
const connection = await instance.connect();
|
|
1717
1732
|
return {
|
|
@@ -1719,7 +1734,7 @@ async function openDatabaseConnection(portalId, _encryptionKey, options2) {
|
|
|
1719
1734
|
connection,
|
|
1720
1735
|
mode,
|
|
1721
1736
|
sourcePath,
|
|
1722
|
-
replicaFingerprint:
|
|
1737
|
+
replicaFingerprint: null
|
|
1723
1738
|
};
|
|
1724
1739
|
}
|
|
1725
1740
|
var OBJECT_CSV_COLUMNS = ["id", "properties", "last_synced"];
|
|
@@ -1784,13 +1799,24 @@ async function bulkAppend(connection, tableName, columns) {
|
|
|
1784
1799
|
function cleanupDbArtifacts(filePath) {
|
|
1785
1800
|
try {
|
|
1786
1801
|
fs3.unlinkSync(filePath);
|
|
1787
|
-
} catch {
|
|
1802
|
+
} catch (error) {
|
|
1803
|
+
logFsCleanupError("remove file", filePath, error);
|
|
1788
1804
|
}
|
|
1789
1805
|
try {
|
|
1790
1806
|
fs3.unlinkSync(`${filePath}.wal`);
|
|
1791
|
-
} catch {
|
|
1807
|
+
} catch (error) {
|
|
1808
|
+
logFsCleanupError("remove wal", `${filePath}.wal`, error);
|
|
1792
1809
|
}
|
|
1793
1810
|
}
|
|
1811
|
+
function logFsCleanupError(action, filePath, error) {
|
|
1812
|
+
const code = error?.code;
|
|
1813
|
+
if (code === "ENOENT") return;
|
|
1814
|
+
console.error("[replica] Cleanup step failed", {
|
|
1815
|
+
action,
|
|
1816
|
+
filePath,
|
|
1817
|
+
error: describeError(error)
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1794
1820
|
function nextReplicaFileName(seed = Date.now()) {
|
|
1795
1821
|
return `${REPLICA_VERSION_PREFIX2}${seed}-${process.pid}-${Math.random().toString(36).slice(2, 8)}${REPLICA_VERSION_SUFFIX2}`;
|
|
1796
1822
|
}
|
|
@@ -1802,7 +1828,8 @@ function writeReplicaPointer(portalId, fileName) {
|
|
|
1802
1828
|
`, "utf-8");
|
|
1803
1829
|
try {
|
|
1804
1830
|
fs3.unlinkSync(pointerPath);
|
|
1805
|
-
} catch {
|
|
1831
|
+
} catch (error) {
|
|
1832
|
+
logFsCleanupError("remove pointer", pointerPath, error);
|
|
1806
1833
|
}
|
|
1807
1834
|
fs3.renameSync(nextPointerPath, pointerPath);
|
|
1808
1835
|
}
|
|
@@ -1818,8 +1845,51 @@ function cleanupStaleReplicaFiles(portalId, currentReplicaPath) {
|
|
|
1818
1845
|
}
|
|
1819
1846
|
cleanupDbArtifacts(path2.join(portalDir(portalId), entry));
|
|
1820
1847
|
}
|
|
1848
|
+
} catch (error) {
|
|
1849
|
+
console.error("[replica] Failed stale replica cleanup", {
|
|
1850
|
+
portalId,
|
|
1851
|
+
currentReplicaPath,
|
|
1852
|
+
error: describeError(error)
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
function getSourceDatabaseName(filePath) {
|
|
1857
|
+
return path2.parse(filePath).name;
|
|
1858
|
+
}
|
|
1859
|
+
function getFileSnapshot(filePath) {
|
|
1860
|
+
try {
|
|
1861
|
+
const stats = fs3.statSync(filePath);
|
|
1862
|
+
return {
|
|
1863
|
+
exists: true,
|
|
1864
|
+
sizeBytes: stats.size,
|
|
1865
|
+
mtimeMs: stats.mtimeMs
|
|
1866
|
+
};
|
|
1821
1867
|
} catch {
|
|
1868
|
+
return {
|
|
1869
|
+
exists: false,
|
|
1870
|
+
sizeBytes: null,
|
|
1871
|
+
mtimeMs: null
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
function describeError(error) {
|
|
1876
|
+
if (error instanceof Error) {
|
|
1877
|
+
const errnoError = error;
|
|
1878
|
+
return {
|
|
1879
|
+
name: error.name,
|
|
1880
|
+
message: error.message,
|
|
1881
|
+
stack: error.stack,
|
|
1882
|
+
code: errnoError.code,
|
|
1883
|
+
errno: errnoError.errno,
|
|
1884
|
+
syscall: errnoError.syscall,
|
|
1885
|
+
path: errnoError.path,
|
|
1886
|
+
dest: errnoError.dest,
|
|
1887
|
+
cause: errnoError.cause ? describeError(errnoError.cause) : void 0
|
|
1888
|
+
};
|
|
1822
1889
|
}
|
|
1890
|
+
return {
|
|
1891
|
+
value: String(error)
|
|
1892
|
+
};
|
|
1823
1893
|
}
|
|
1824
1894
|
async function publishReplica({
|
|
1825
1895
|
masterLock,
|
|
@@ -1830,11 +1900,87 @@ async function publishReplica({
|
|
|
1830
1900
|
const liveDbPath = dbPath(portalId);
|
|
1831
1901
|
const nextReplicaName = nextReplicaFileName();
|
|
1832
1902
|
const nextReplicaPath = replicaVersionDbPath(portalId, nextReplicaName);
|
|
1903
|
+
const nextReplicaTempPath = `${nextReplicaPath}.next`;
|
|
1904
|
+
const sourceDatabaseName = getSourceDatabaseName(liveDbPath);
|
|
1905
|
+
const targetDatabaseName = "portal_replica_next";
|
|
1906
|
+
console.error("[replica] Publish starting", {
|
|
1907
|
+
portalId,
|
|
1908
|
+
liveDbPath,
|
|
1909
|
+
nextReplicaPath,
|
|
1910
|
+
nextReplicaTempPath,
|
|
1911
|
+
liveDb: getFileSnapshot(liveDbPath)
|
|
1912
|
+
});
|
|
1833
1913
|
cleanupDbArtifacts(nextReplicaPath);
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1914
|
+
cleanupDbArtifacts(nextReplicaTempPath);
|
|
1915
|
+
try {
|
|
1916
|
+
console.error("[replica] Checkpointing live database", {
|
|
1917
|
+
portalId,
|
|
1918
|
+
liveDbPath
|
|
1919
|
+
});
|
|
1920
|
+
await handle.connection.run("CHECKPOINT");
|
|
1921
|
+
console.error("[replica] Building replica snapshot via DuckDB", {
|
|
1922
|
+
portalId,
|
|
1923
|
+
sourceDatabaseName,
|
|
1924
|
+
targetDatabaseName,
|
|
1925
|
+
nextReplicaTempPath
|
|
1926
|
+
});
|
|
1927
|
+
await handle.connection.run(
|
|
1928
|
+
`ATTACH '${escapeSqlString(nextReplicaTempPath)}' AS ${escapeSqlIdentifier(targetDatabaseName)}`
|
|
1929
|
+
);
|
|
1930
|
+
try {
|
|
1931
|
+
await handle.connection.run(
|
|
1932
|
+
`COPY FROM DATABASE ${escapeSqlIdentifier(sourceDatabaseName)} TO ${escapeSqlIdentifier(targetDatabaseName)}`
|
|
1933
|
+
);
|
|
1934
|
+
await handle.connection.run(`CHECKPOINT ${escapeSqlIdentifier(targetDatabaseName)}`);
|
|
1935
|
+
} finally {
|
|
1936
|
+
await handle.connection.run(`DETACH ${escapeSqlIdentifier(targetDatabaseName)}`);
|
|
1937
|
+
}
|
|
1938
|
+
console.error("[replica] Verifying replica snapshot", {
|
|
1939
|
+
portalId,
|
|
1940
|
+
nextReplicaTempPath,
|
|
1941
|
+
snapshot: getFileSnapshot(nextReplicaTempPath)
|
|
1942
|
+
});
|
|
1943
|
+
const verifyInstance = await DuckDBInstance.create(":memory:");
|
|
1944
|
+
const verifyConnection = await verifyInstance.connect();
|
|
1945
|
+
try {
|
|
1946
|
+
await verifyConnection.run(
|
|
1947
|
+
`ATTACH '${escapeSqlString(nextReplicaTempPath)}' AS portal_replica_verify (READ_ONLY)`
|
|
1948
|
+
);
|
|
1949
|
+
await verifyConnection.run("DETACH portal_replica_verify");
|
|
1950
|
+
} finally {
|
|
1951
|
+
try {
|
|
1952
|
+
verifyConnection.closeSync();
|
|
1953
|
+
} catch {
|
|
1954
|
+
}
|
|
1955
|
+
try {
|
|
1956
|
+
verifyInstance.closeSync();
|
|
1957
|
+
} catch {
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
fs3.renameSync(nextReplicaTempPath, nextReplicaPath);
|
|
1961
|
+
console.error("[replica] Writing replica pointer", {
|
|
1962
|
+
portalId,
|
|
1963
|
+
nextReplicaName,
|
|
1964
|
+
nextReplicaPath
|
|
1965
|
+
});
|
|
1966
|
+
writeReplicaPointer(portalId, nextReplicaName);
|
|
1967
|
+
cleanupStaleReplicaFiles(portalId, nextReplicaPath);
|
|
1968
|
+
console.error("[replica] Publish complete", {
|
|
1969
|
+
portalId,
|
|
1970
|
+
nextReplicaPath,
|
|
1971
|
+
replica: getFileSnapshot(nextReplicaPath)
|
|
1972
|
+
});
|
|
1973
|
+
} catch (error) {
|
|
1974
|
+
console.error("[replica] Publish failed", {
|
|
1975
|
+
portalId,
|
|
1976
|
+
liveDbPath,
|
|
1977
|
+
nextReplicaPath,
|
|
1978
|
+
nextReplicaTempPath,
|
|
1979
|
+
error: describeError(error)
|
|
1980
|
+
});
|
|
1981
|
+
cleanupDbArtifacts(nextReplicaTempPath);
|
|
1982
|
+
throw error;
|
|
1983
|
+
}
|
|
1838
1984
|
}
|
|
1839
1985
|
async function ensureTable(connection, ddlStatements) {
|
|
1840
1986
|
for (const statement of ddlStatements) {
|
|
@@ -2846,8 +2992,11 @@ var makeReplicaLive = (config = {}) => {
|
|
|
2846
2992
|
dirty: true
|
|
2847
2993
|
})),
|
|
2848
2994
|
Effect25.flatMap(
|
|
2849
|
-
() =>
|
|
2850
|
-
|
|
2995
|
+
() => Effect25.sync(
|
|
2996
|
+
() => console.error("[replica] Failed to publish replica", {
|
|
2997
|
+
portalId,
|
|
2998
|
+
error
|
|
2999
|
+
})
|
|
2851
3000
|
)
|
|
2852
3001
|
),
|
|
2853
3002
|
Effect25.flatMap(() => Effect25.fail(error))
|
|
@@ -3919,26 +4068,64 @@ var setSelectedPortal = (portalId) => {
|
|
|
3919
4068
|
// src/effects/read-connection.ts
|
|
3920
4069
|
import { Effect as Effect31, pipe as pipe19 } from "effect";
|
|
3921
4070
|
import fs6 from "fs";
|
|
3922
|
-
var
|
|
4071
|
+
var openLiveConnection = (portalId, cause) => Effect31.tryPromise({
|
|
4072
|
+
try: async () => {
|
|
4073
|
+
const handle = await getDatabaseConnection(portalId, null);
|
|
4074
|
+
return handle.connection;
|
|
4075
|
+
},
|
|
4076
|
+
catch: (error) => new DatabaseError({
|
|
4077
|
+
message: `Failed to open live fallback connection for portal ${portalId}`,
|
|
4078
|
+
cause: { replicaError: cause, fallbackError: error }
|
|
4079
|
+
})
|
|
4080
|
+
});
|
|
4081
|
+
var getReadConnection = (portalId, options2) => pipe19(
|
|
3923
4082
|
Effect31.sync(() => replicaDbPath(portalId)),
|
|
3924
|
-
Effect31.
|
|
3925
|
-
(
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
4083
|
+
Effect31.flatMap((dbFile) => {
|
|
4084
|
+
if (!fs6.existsSync(dbFile)) {
|
|
4085
|
+
if (options2?.allowLiveFallback === true && fs6.existsSync(dbPath(portalId))) {
|
|
4086
|
+
console.error("[read-connection] Replica missing, falling back to live database", {
|
|
4087
|
+
portalId,
|
|
4088
|
+
replicaPath: dbFile,
|
|
4089
|
+
livePath: dbPath(portalId)
|
|
4090
|
+
});
|
|
4091
|
+
return openLiveConnection(
|
|
4092
|
+
portalId,
|
|
4093
|
+
new DatabaseError({
|
|
4094
|
+
message: `Read replica not found for portal ${portalId}. Has it been synced?`
|
|
4095
|
+
})
|
|
4096
|
+
);
|
|
4097
|
+
}
|
|
4098
|
+
return Effect31.fail(
|
|
4099
|
+
new DatabaseError({
|
|
4100
|
+
message: `Read replica not found for portal ${portalId}. Has it been synced?`
|
|
4101
|
+
})
|
|
4102
|
+
);
|
|
4103
|
+
}
|
|
4104
|
+
return Effect31.tryPromise({
|
|
3932
4105
|
try: async () => {
|
|
3933
4106
|
const handle = await getDatabaseConnection(portalId, null, { readOnly: true });
|
|
3934
4107
|
return handle.connection;
|
|
3935
4108
|
},
|
|
3936
|
-
catch: (
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
4109
|
+
catch: (error) => error
|
|
4110
|
+
});
|
|
4111
|
+
}),
|
|
4112
|
+
Effect31.catchAll((error) => {
|
|
4113
|
+
if (!(options2?.allowLiveFallback === true) || !fs6.existsSync(dbPath(portalId))) {
|
|
4114
|
+
return Effect31.fail(
|
|
4115
|
+
error instanceof DatabaseError ? error : new DatabaseError({
|
|
4116
|
+
message: `Failed to open read connection for portal ${portalId}`,
|
|
4117
|
+
cause: error
|
|
4118
|
+
})
|
|
4119
|
+
);
|
|
4120
|
+
}
|
|
4121
|
+
console.error("[read-connection] Read-only replica open failed, falling back to live database", {
|
|
4122
|
+
portalId,
|
|
4123
|
+
replicaPath: replicaDbPath(portalId),
|
|
4124
|
+
livePath: dbPath(portalId),
|
|
4125
|
+
error
|
|
4126
|
+
});
|
|
4127
|
+
return openLiveConnection(portalId, error);
|
|
4128
|
+
})
|
|
3942
4129
|
);
|
|
3943
4130
|
var closeReadConnection = (portalId) => Effect31.promise(() => evictDatabaseConnections(portalId));
|
|
3944
4131
|
|
|
@@ -5620,8 +5807,10 @@ var resolvePortalIds = (explicitPortalIds, selectedPortalId2, portals) => {
|
|
|
5620
5807
|
|
|
5621
5808
|
// src/tools/query.ts
|
|
5622
5809
|
var MAX_ROWS = 200;
|
|
5623
|
-
var runQueryForPortal = async (portalId, finalSql, autoCapped) => {
|
|
5624
|
-
const conn = await Effect50.runPromise(
|
|
5810
|
+
var runQueryForPortal = async (portalId, finalSql, autoCapped, allowLiveFallback) => {
|
|
5811
|
+
const conn = await Effect50.runPromise(
|
|
5812
|
+
getReadConnection(portalId, { allowLiveFallback })
|
|
5813
|
+
);
|
|
5625
5814
|
const start = performance.now();
|
|
5626
5815
|
const reader = await conn.runAndReadAll(finalSql);
|
|
5627
5816
|
const rows = reader.getRowObjects();
|
|
@@ -5697,7 +5886,8 @@ Example queries:
|
|
|
5697
5886
|
const result = await runQueryForPortal(
|
|
5698
5887
|
portalId,
|
|
5699
5888
|
finalSql,
|
|
5700
|
-
!hasExplicitLimit
|
|
5889
|
+
!hasExplicitLimit,
|
|
5890
|
+
deps.isMasterClient()
|
|
5701
5891
|
);
|
|
5702
5892
|
return {
|
|
5703
5893
|
content: [{ type: "text", text: stringifyResult(result) }]
|
|
@@ -5718,7 +5908,8 @@ Example queries:
|
|
|
5718
5908
|
keyedResults[String(portalId)] = await runQueryForPortal(
|
|
5719
5909
|
portalId,
|
|
5720
5910
|
finalSql,
|
|
5721
|
-
!hasExplicitLimit
|
|
5911
|
+
!hasExplicitLimit,
|
|
5912
|
+
deps.isMasterClient()
|
|
5722
5913
|
);
|
|
5723
5914
|
} catch (e) {
|
|
5724
5915
|
hadAnyErrors = true;
|
|
@@ -5938,8 +6129,10 @@ function openInBrowser(target) {
|
|
|
5938
6129
|
|
|
5939
6130
|
// src/tools/chart.ts
|
|
5940
6131
|
var MAX_ROWS2 = 200;
|
|
5941
|
-
var runQueryForPortal2 = async (portalId, sql) => {
|
|
5942
|
-
const conn = await Effect51.runPromise(
|
|
6132
|
+
var runQueryForPortal2 = async (portalId, sql, allowLiveFallback) => {
|
|
6133
|
+
const conn = await Effect51.runPromise(
|
|
6134
|
+
getReadConnection(portalId, { allowLiveFallback })
|
|
6135
|
+
);
|
|
5943
6136
|
const start = performance.now();
|
|
5944
6137
|
const reader = await conn.runAndReadAll(sql);
|
|
5945
6138
|
const rows = reader.getRowObjects();
|
|
@@ -6051,7 +6244,11 @@ SELECT json_extract_string(properties, '$.dealstage') as stage, COUNT(*) as coun
|
|
|
6051
6244
|
const [portalId] = portalResolution.portalIds;
|
|
6052
6245
|
try {
|
|
6053
6246
|
await deps.ensureFresh(portalId);
|
|
6054
|
-
const result = await runQueryForPortal2(
|
|
6247
|
+
const result = await runQueryForPortal2(
|
|
6248
|
+
portalId,
|
|
6249
|
+
finalSql,
|
|
6250
|
+
deps.isMasterClient()
|
|
6251
|
+
);
|
|
6055
6252
|
if (result.rows.length === 0) {
|
|
6056
6253
|
return {
|
|
6057
6254
|
content: [{ type: "text", text: "Query returned no rows - nothing to chart." }],
|
|
@@ -6105,7 +6302,11 @@ SELECT json_extract_string(properties, '$.dealstage') as stage, COUNT(*) as coun
|
|
|
6105
6302
|
for (const portalId of portalResolution.portalIds) {
|
|
6106
6303
|
try {
|
|
6107
6304
|
await deps.ensureFresh(portalId);
|
|
6108
|
-
const result = await runQueryForPortal2(
|
|
6305
|
+
const result = await runQueryForPortal2(
|
|
6306
|
+
portalId,
|
|
6307
|
+
finalSql,
|
|
6308
|
+
deps.isMasterClient()
|
|
6309
|
+
);
|
|
6109
6310
|
if (result.rows.length === 0) {
|
|
6110
6311
|
hadErrors = true;
|
|
6111
6312
|
keyedPortalResults[String(portalId)] = { error: "Query returned no rows - nothing to chart." };
|
|
@@ -6204,8 +6405,10 @@ async function buildConnectionSection(deps) {
|
|
|
6204
6405
|
totalPortals: portals.length
|
|
6205
6406
|
};
|
|
6206
6407
|
}
|
|
6207
|
-
async function buildSchemaSection(portalId) {
|
|
6208
|
-
const conn = await Effect52.runPromise(
|
|
6408
|
+
async function buildSchemaSection(portalId, deps) {
|
|
6409
|
+
const conn = await Effect52.runPromise(
|
|
6410
|
+
getReadConnection(portalId, { allowLiveFallback: deps.isMasterClient() })
|
|
6411
|
+
);
|
|
6209
6412
|
const allPluginTableNames = new Set(getAllTableNames());
|
|
6210
6413
|
const sectionResults = await Promise.all(
|
|
6211
6414
|
getPlugins().map(async (plugin) => {
|
|
@@ -6276,7 +6479,7 @@ Use "all" to get everything at once.`,
|
|
|
6276
6479
|
for (const portalId of portalResolution.portalIds) {
|
|
6277
6480
|
try {
|
|
6278
6481
|
await deps.ensureFresh(portalId);
|
|
6279
|
-
schemaByPortal[String(portalId)] = await buildSchemaSection(portalId);
|
|
6482
|
+
schemaByPortal[String(portalId)] = await buildSchemaSection(portalId, deps);
|
|
6280
6483
|
} catch (e) {
|
|
6281
6484
|
explicitSchemaHadErrors = true;
|
|
6282
6485
|
const message = e instanceof Error ? e.message : String(e);
|
|
@@ -6287,7 +6490,7 @@ Use "all" to get everything at once.`,
|
|
|
6287
6490
|
} else {
|
|
6288
6491
|
const [portalId] = portalResolution.portalIds;
|
|
6289
6492
|
await deps.ensureFresh(portalId);
|
|
6290
|
-
result.schema = await buildSchemaSection(portalId);
|
|
6493
|
+
result.schema = await buildSchemaSection(portalId, deps);
|
|
6291
6494
|
}
|
|
6292
6495
|
}
|
|
6293
6496
|
return {
|
|
@@ -9303,7 +9506,7 @@ var BUILT_IN_ACTION_TYPES = {
|
|
|
9303
9506
|
{ name: "date", type: "object", required: true, description: "Date spec \u2014 MUST include type: { type: 'STATIC_VALUE', staticValue: '<unix ms>' } or { type: 'OBJECT_PROPERTY', propertyName: '<name>' }. Omitting type causes HTTP 500." },
|
|
9304
9507
|
{ name: "delta", type: "string", required: true, description: "Offset from the date (e.g. '0')" },
|
|
9305
9508
|
{ name: "time_unit", type: "string", required: true, description: "Unit for delta: DAYS, HOURS, MINUTES" },
|
|
9306
|
-
{ name: "time_of_day", type: "object", required: true, description: "Required time of day: { hour: number, minute: number }. HubSpot UI rejects delay-until-date actions without it." }
|
|
9509
|
+
{ name: "time_of_day", type: "object", required: true, description: "Required time of day: { hour: number, minute: number }. HubSpot UI rejects delay-until-date actions without it. If the workflow continues after this delay, the next action should be a STATIC_BRANCH on hs_delay_status rather than a direct business-action connection." }
|
|
9307
9510
|
]
|
|
9308
9511
|
},
|
|
9309
9512
|
"0-63809083": {
|
|
@@ -9623,7 +9826,7 @@ var WORKFLOW_ACTION_CREATE_METADATA_BY_ID = {
|
|
|
9623
9826
|
},
|
|
9624
9827
|
"0-35": {
|
|
9625
9828
|
resourceRequirements: [],
|
|
9626
|
-
createGuidanceText: "When using STATIC_VALUE dates, send a future Unix-milliseconds timestamp. time_of_day is required for reliable creation."
|
|
9829
|
+
createGuidanceText: "When using STATIC_VALUE dates, send a future Unix-milliseconds timestamp. time_of_day is required for reliable creation. If the delay action continues to another step, route it into a STATIC_BRANCH that reads inputValue { actionId: '<delay_action_id>', dataKey: 'hs_delay_status', type: 'FIELD_DATA' } and continues from the DATE_MET_AS_PLANNED branch instead of connecting directly to the next business action."
|
|
9627
9830
|
},
|
|
9628
9831
|
"0-14": {
|
|
9629
9832
|
resourceRequirements: [],
|
|
@@ -11774,12 +11977,12 @@ var PROPERTY_VALUE_CHANGED_PARAMETER_SCHEMA = [
|
|
|
11774
11977
|
{
|
|
11775
11978
|
name: "property_value_operator",
|
|
11776
11979
|
required: false,
|
|
11777
|
-
description: "Defaults to IS_KNOWN. Supported values: IS_KNOWN, IS_UNKNOWN, IS_EQUAL_TO, IS_NOT_EQUAL_TO."
|
|
11980
|
+
description: "Defaults to IS_KNOWN. Supported values: IS_KNOWN, IS_UNKNOWN, IS_EQUAL_TO, IS_NOT_EQUAL_TO. This only controls the event refinement filter on hs_value inside the 4-655002 property-change event branch."
|
|
11778
11981
|
},
|
|
11779
11982
|
{
|
|
11780
11983
|
name: "property_value",
|
|
11781
11984
|
required: false,
|
|
11782
|
-
description: "Required when property_value_operator is IS_EQUAL_TO or IS_NOT_EQUAL_TO."
|
|
11985
|
+
description: "Required when property_value_operator is IS_EQUAL_TO or IS_NOT_EQUAL_TO. This does not add generic object-property filters like pipeline or dealstage to the event branch."
|
|
11783
11986
|
}
|
|
11784
11987
|
];
|
|
11785
11988
|
var WORKFLOW_REFERENCE_PARAMETER_SCHEMA = [
|
|
@@ -11797,7 +12000,7 @@ var PARAMETER_SCHEMA_BY_TEMPLATE = {
|
|
|
11797
12000
|
var EVENT_TYPE_METADATA_BY_EVENT_TYPE_ID = {
|
|
11798
12001
|
"4-655002": {
|
|
11799
12002
|
requiresRefinementFilters: true,
|
|
11800
|
-
refinementNote: "Requires event-specific refinement filters
|
|
12003
|
+
refinementNote: "Requires event-specific refinement filters embedded in the event branch. For MCP these are hs_name and hs_value. Standard object-property filters like pipeline, dealstage, amount, or closedate are not valid inside the 4-655002 branch. Prefer LIST_BASED enrollment with re-enrollment triggers for 'property changed to X' scenarios that also need record eligibility filters."
|
|
11801
12004
|
}
|
|
11802
12005
|
};
|
|
11803
12006
|
var normalizedRows = normalized_triggers_default;
|
|
@@ -12235,6 +12438,13 @@ var buildEnrollmentCriteriaFromTrigger = (trigger) => {
|
|
|
12235
12438
|
};
|
|
12236
12439
|
|
|
12237
12440
|
// ../shared/pure/workflow-operation-schema.ts
|
|
12441
|
+
var ENUMERATION_DISALLOWED_OPERATORS = /* @__PURE__ */ new Set([
|
|
12442
|
+
"IS_EQUAL_TO",
|
|
12443
|
+
"IS_NOT_EQUAL_TO"
|
|
12444
|
+
]);
|
|
12445
|
+
var REFINEMENT_PROPERTIES_BY_EVENT_TYPE_ID = {
|
|
12446
|
+
"4-655002": /* @__PURE__ */ new Set(["hs_name", "hs_value"])
|
|
12447
|
+
};
|
|
12238
12448
|
var WorkflowConnectionSchema2 = z62.object({
|
|
12239
12449
|
edgeType: z62.string().min(1),
|
|
12240
12450
|
nextActionId: z62.string().min(1)
|
|
@@ -12262,6 +12472,14 @@ var WorkflowFilterSchema = z62.object({
|
|
|
12262
12472
|
message: "property filters require operation",
|
|
12263
12473
|
path: ["operation"]
|
|
12264
12474
|
});
|
|
12475
|
+
return;
|
|
12476
|
+
}
|
|
12477
|
+
if (value.operation.operationType === "ENUMERATION" && ENUMERATION_DISALLOWED_OPERATORS.has(value.operation.operator)) {
|
|
12478
|
+
ctx.addIssue({
|
|
12479
|
+
code: z62.ZodIssueCode.custom,
|
|
12480
|
+
message: `operator '${value.operation.operator}' is not valid for ENUMERATION workflow filters. Use operators like IS_ANY_OF, IS_NONE_OF, IS_EXACTLY, or IS_NOT_EXACTLY.`,
|
|
12481
|
+
path: ["operation", "operator"]
|
|
12482
|
+
});
|
|
12265
12483
|
}
|
|
12266
12484
|
});
|
|
12267
12485
|
var WorkflowBranchSchema = z62.object({
|
|
@@ -12302,6 +12520,19 @@ var WorkflowEnrollmentBranchSchema = z62.object({
|
|
|
12302
12520
|
path: ["eventTypeId"]
|
|
12303
12521
|
});
|
|
12304
12522
|
}
|
|
12523
|
+
const allowedRefinementProperties = REFINEMENT_PROPERTIES_BY_EVENT_TYPE_ID[value.eventTypeId];
|
|
12524
|
+
if (!allowedRefinementProperties) return;
|
|
12525
|
+
for (const [index, filter] of value.filters.entries()) {
|
|
12526
|
+
if (!filter || typeof filter !== "object") continue;
|
|
12527
|
+
if (filter.filterType !== "PROPERTY") continue;
|
|
12528
|
+
if (typeof filter.property !== "string") continue;
|
|
12529
|
+
if (allowedRefinementProperties.has(filter.property)) continue;
|
|
12530
|
+
ctx.addIssue({
|
|
12531
|
+
code: z62.ZodIssueCode.custom,
|
|
12532
|
+
message: `eventTypeId '${value.eventTypeId}' only supports refinement filters on ${Array.from(allowedRefinementProperties).join(", ")}. Move generic properties like '${filter.property}' into LIST_BASED criteria or use enrollment_trigger.`,
|
|
12533
|
+
path: ["filters", index, "property"]
|
|
12534
|
+
});
|
|
12535
|
+
}
|
|
12305
12536
|
});
|
|
12306
12537
|
var EventBasedEnrollmentCriteriaSchema = z62.object({
|
|
12307
12538
|
type: z62.literal("EVENT_BASED"),
|
|
@@ -12432,7 +12663,7 @@ var fields44 = [
|
|
|
12432
12663
|
name: "enrollment_criteria",
|
|
12433
12664
|
type: "object",
|
|
12434
12665
|
required: false,
|
|
12435
|
-
description: "Enrollment trigger config. Set type to 'EVENT_BASED', 'LIST_BASED', or 'MANUAL'. Prefer enrollment_trigger for supported single-trigger EVENT_BASED shortcuts. For EVENT_BASED, each eventFilterBranch needs a valid eventTypeId from the workflow_enrollment_triggers table (e.g. '4-655002' for property change, '4-1639801' for form submission). Do NOT use object type IDs like '0-3' as eventTypeId \u2014 those are CRM object types, not events. For LIST_BASED and MANUAL, query workflow_enrollment_triggers rows where shortcut_kind = 'CRITERIA_EXAMPLE' and reuse official_criteria_example_json, criteria_shape_json, usage_guidance, preferred_for_json, and avoid_for_json. IMPORTANT: Some event types (notably '4-655002' property value changed) require event-specific refinement filters
|
|
12666
|
+
description: "Enrollment trigger config. Set type to 'EVENT_BASED', 'LIST_BASED', or 'MANUAL'. Prefer enrollment_trigger for supported single-trigger EVENT_BASED shortcuts. For EVENT_BASED, each eventFilterBranch needs a valid eventTypeId from the workflow_enrollment_triggers table (e.g. '4-655002' for property change, '4-1639801' for form submission). Do NOT use object type IDs like '0-3' as eventTypeId \u2014 those are CRM object types, not events. For LIST_BASED and MANUAL, query workflow_enrollment_triggers rows where shortcut_kind = 'CRITERIA_EXAMPLE' and reuse official_criteria_example_json, criteria_shape_json, usage_guidance, preferred_for_json, and avoid_for_json. IMPORTANT: Some event types (notably '4-655002' property value changed) require event-specific refinement filters embedded in the event branch. For '4-655002', only event refinement properties like hs_name and hs_value belong in that branch; generic record properties like pipeline, dealstage, amount, and closedate do NOT belong there. For 'when property X changes to Y' scenarios that also need record eligibility checks, prefer LIST_BASED enrollment with re-enrollment triggers on that property instead. PROPERTY filters must include operation.operationType. For enum properties use 'ENUMERATION' not 'MULTISTRING'. For LIST_BASED property existence filters like IS_KNOWN / IS_UNKNOWN, prefer operationType 'ALL_PROPERTY' instead of 'TIME_POINT', even for date properties. ENUMERATION filters do not support operators like 'IS_EQUAL_TO' or 'IS_NOT_EQUAL_TO'; use enum operators like 'IS_ANY_OF', 'IS_NONE_OF', 'IS_EXACTLY', or 'IS_NOT_EXACTLY'. Query synced workflows for structural examples.",
|
|
12436
12667
|
shape: "EVENT_BASED: { shouldReEnroll: boolean, eventFilterBranches: [{ filterBranchType: 'UNIFIED_EVENTS', filterBranchOperator?, eventTypeId: string (REQUIRED, from workflow_enrollment_triggers.event_type_id), operator?, filters: [{ filterType: 'PROPERTY', property: string, operation: { operationType: string, operator: string } }], filterBranches: [...] }], listMembershipFilterBranches?: [...] } | LIST_BASED: { shouldReEnroll: boolean, listFilterBranch: { filterBranchType, filters: [...], filterBranches: [...] }, reEnrollmentTriggersFilterBranches?: [...], unEnrollObjectsNotMeetingCriteria?: boolean } | MANUAL: { shouldReEnroll: boolean }"
|
|
12437
12668
|
},
|
|
12438
12669
|
{ name: "suppression_list_ids", type: "array", required: false, description: "Array of list IDs to suppress from enrollment" },
|
|
@@ -12539,7 +12770,7 @@ var fields45 = [
|
|
|
12539
12770
|
description: "Replacement single-trigger shortcut. Query workflow_enrollment_triggers first and only use rows where shortcut_kind = 'EVENT_TRIGGER_SHORTCUT'. Then use catalog_id plus parameter_schema_json, official_filter_template, official_filters_example_json, and required_object_type_ids_json.",
|
|
12540
12771
|
shape: "{ catalog_id: string, should_re_enroll?: boolean, ...template-specific parameters from parameter_schema_json such as property_name/property_value_operator/property_value or target_workflow_id }"
|
|
12541
12772
|
},
|
|
12542
|
-
{ name: "enrollment_criteria", type: "object", required: false, description: "Replacement enrollment criteria (replaces existing). Set type to EVENT_BASED, LIST_BASED, or MANUAL. Prefer enrollment_trigger for supported single-trigger EVENT_BASED shortcuts. For EVENT_BASED, each UNIFIED_EVENTS branch requires a valid eventTypeId from workflow_enrollment_triggers.event_type_id (e.g. '4-655002' for property change). Do NOT use object type IDs like '0-3' as eventTypeId. For LIST_BASED and MANUAL, query workflow_enrollment_triggers rows where shortcut_kind = 'CRITERIA_EXAMPLE' and reuse official_criteria_example_json, criteria_shape_json, and usage_guidance. IMPORTANT: Event types like '4-655002'
|
|
12773
|
+
{ name: "enrollment_criteria", type: "object", required: false, description: "Replacement enrollment criteria (replaces existing). Set type to EVENT_BASED, LIST_BASED, or MANUAL. Prefer enrollment_trigger for supported single-trigger EVENT_BASED shortcuts. For EVENT_BASED, each UNIFIED_EVENTS branch requires a valid eventTypeId from workflow_enrollment_triggers.event_type_id (e.g. '4-655002' for property change). Do NOT use object type IDs like '0-3' as eventTypeId. For LIST_BASED and MANUAL, query workflow_enrollment_triggers rows where shortcut_kind = 'CRITERIA_EXAMPLE' and reuse official_criteria_example_json, criteria_shape_json, and usage_guidance. IMPORTANT: Event types like '4-655002' require event-specific refinement filters only. In MCP this means hs_name and hs_value inside the event branch; generic properties like pipeline, dealstage, amount, or closedate do not belong there. ENUMERATION filters also do not support operators like IS_EQUAL_TO or IS_NOT_EQUAL_TO; use IS_ANY_OF, IS_NONE_OF, IS_EXACTLY, or IS_NOT_EXACTLY instead. Prefer LIST_BASED enrollment with re-enrollment triggers for property-change scenarios that also need record eligibility filters." },
|
|
12543
12774
|
{ name: "suppression_list_ids", type: "array", required: false, description: "Replacement suppression list IDs" },
|
|
12544
12775
|
{ name: "time_windows", type: "array", required: false, description: "Replacement execution time windows" },
|
|
12545
12776
|
{ name: "blocked_dates", type: "array", required: false, description: "Replacement blocked date ranges" }
|
|
@@ -16949,6 +17180,10 @@ var OPERATION_TYPE_FOR_PROPERTY_TYPE2 = {
|
|
|
16949
17180
|
date: "TIME_POINT",
|
|
16950
17181
|
datetime: "TIME_POINT"
|
|
16951
17182
|
};
|
|
17183
|
+
var ENUMERATION_INVALID_OPERATORS = /* @__PURE__ */ new Set([
|
|
17184
|
+
"IS_EQUAL_TO",
|
|
17185
|
+
"IS_NOT_EQUAL_TO"
|
|
17186
|
+
]);
|
|
16952
17187
|
var isCompatibleEnrollmentOperationType = (operationType, expectedOperationType, propertyType) => operationType === expectedOperationType || operationType === "ALL_PROPERTY" || operationType === "TIME_RANGED" && (propertyType === "date" || propertyType === "datetime");
|
|
16953
17188
|
var collectPropertyFiltersFromBranches = (branches, parentObjectTypeId, parentEventTypeId) => {
|
|
16954
17189
|
const refs = [];
|
|
@@ -16963,6 +17198,7 @@ var collectPropertyFiltersFromBranches = (branches, parentObjectTypeId, parentEv
|
|
|
16963
17198
|
refs.push({
|
|
16964
17199
|
property: filter.property,
|
|
16965
17200
|
operationType: operation.operationType,
|
|
17201
|
+
operator: typeof operation.operator === "string" ? operation.operator : void 0,
|
|
16966
17202
|
objectTypeId: contextObjectTypeId,
|
|
16967
17203
|
eventTypeId: branchEventTypeId
|
|
16968
17204
|
});
|
|
@@ -17029,6 +17265,15 @@ var validateWorkflowEnrollmentMetadata = (params) => {
|
|
|
17029
17265
|
});
|
|
17030
17266
|
continue;
|
|
17031
17267
|
}
|
|
17268
|
+
if (filterRef.eventTypeId === "4-655002") {
|
|
17269
|
+
add({
|
|
17270
|
+
severity: "error",
|
|
17271
|
+
operation_index: operationIndex,
|
|
17272
|
+
code: "INVALID_ENROLLMENT_REFINEMENT_FILTER",
|
|
17273
|
+
message: `eventTypeId '4-655002' only supports event refinement filters like hs_name and hs_value. Property '${filterRef.property}' should not appear in that event branch; move it into LIST_BASED criteria or use enrollment_trigger.`
|
|
17274
|
+
});
|
|
17275
|
+
continue;
|
|
17276
|
+
}
|
|
17032
17277
|
if (!enabledObjectTypeIds.has(filterRef.objectTypeId)) continue;
|
|
17033
17278
|
const propertyDefinitions = propertyDefinitionsByObjectType.get(filterRef.objectTypeId);
|
|
17034
17279
|
const propertyDefinition = propertyDefinitions?.get(filterRef.property);
|
|
@@ -17043,7 +17288,24 @@ var validateWorkflowEnrollmentMetadata = (params) => {
|
|
|
17043
17288
|
}
|
|
17044
17289
|
const expectedOperationType = OPERATION_TYPE_FOR_PROPERTY_TYPE2[propertyDefinition.type];
|
|
17045
17290
|
if (!expectedOperationType) continue;
|
|
17291
|
+
if (filterRef.eventTypeId === void 0 && (filterRef.operator === "IS_KNOWN" || filterRef.operator === "IS_UNKNOWN") && filterRef.operationType === "TIME_POINT" && (propertyDefinition.type === "date" || propertyDefinition.type === "datetime")) {
|
|
17292
|
+
add({
|
|
17293
|
+
severity: "error",
|
|
17294
|
+
operation_index: operationIndex,
|
|
17295
|
+
code: "INVALID_ENROLLMENT_FILTER_OPERATION_TYPE",
|
|
17296
|
+
message: `Workflow enrollment filter property '${filterRef.property}' uses operator '${filterRef.operator}' with operationType 'TIME_POINT'. For LIST_BASED property existence checks, use operationType 'ALL_PROPERTY' instead.`
|
|
17297
|
+
});
|
|
17298
|
+
continue;
|
|
17299
|
+
}
|
|
17046
17300
|
if (isCompatibleEnrollmentOperationType(filterRef.operationType, expectedOperationType, propertyDefinition.type)) {
|
|
17301
|
+
if (propertyDefinition.type === "enumeration" && filterRef.operator !== void 0 && ENUMERATION_INVALID_OPERATORS.has(filterRef.operator)) {
|
|
17302
|
+
add({
|
|
17303
|
+
severity: "error",
|
|
17304
|
+
operation_index: operationIndex,
|
|
17305
|
+
code: "INVALID_ENROLLMENT_FILTER_OPERATOR",
|
|
17306
|
+
message: `Workflow enrollment filter property '${filterRef.property}' has type 'enumeration' but uses operator '${filterRef.operator}'. Use enumeration operators like IS_ANY_OF, IS_NONE_OF, IS_EXACTLY, or IS_NOT_EXACTLY.`
|
|
17307
|
+
});
|
|
17308
|
+
}
|
|
17047
17309
|
continue;
|
|
17048
17310
|
}
|
|
17049
17311
|
add({
|
|
@@ -17166,6 +17428,42 @@ var validateDelayUntilDateAction = (params) => {
|
|
|
17166
17428
|
}
|
|
17167
17429
|
return issues;
|
|
17168
17430
|
};
|
|
17431
|
+
var validateDelayUntilDateBranching = (params) => {
|
|
17432
|
+
const { actions, operationIndex } = params;
|
|
17433
|
+
const issues = [];
|
|
17434
|
+
const actionsById = new Map(actions.flatMap(
|
|
17435
|
+
(action) => typeof action.actionId === "string" && action.actionId.length > 0 ? [[action.actionId, action]] : []
|
|
17436
|
+
));
|
|
17437
|
+
for (const action of actions) {
|
|
17438
|
+
if (action.actionTypeId !== "0-35") continue;
|
|
17439
|
+
const delayActionId = action.actionId ?? "<missing>";
|
|
17440
|
+
const branchActionId = action.connection?.nextActionId;
|
|
17441
|
+
if (!branchActionId) continue;
|
|
17442
|
+
const branchAction = actionsById.get(branchActionId);
|
|
17443
|
+
if (!branchAction || branchAction.type !== "STATIC_BRANCH") {
|
|
17444
|
+
issues.push({
|
|
17445
|
+
severity: "error",
|
|
17446
|
+
operation_index: operationIndex,
|
|
17447
|
+
code: "WORKFLOW_DELAY_UNTIL_DATE_BRANCH_REQUIRED",
|
|
17448
|
+
message: `Action '${delayActionId}' (0-35 delay until date) must flow into a STATIC_BRANCH that reads hs_delay_status before continuing. Do not connect the delay action directly to the next business action.`
|
|
17449
|
+
});
|
|
17450
|
+
continue;
|
|
17451
|
+
}
|
|
17452
|
+
const inputValue = branchAction.inputValue;
|
|
17453
|
+
const hasExpectedInputValue = inputValue?.actionId === action.actionId && inputValue?.dataKey === "hs_delay_status" && inputValue?.type === "FIELD_DATA";
|
|
17454
|
+
const hasDateMetAsPlannedBranch = Array.isArray(branchAction.staticBranches) && branchAction.staticBranches.some(
|
|
17455
|
+
(branch) => isRecord(branch) && branch.branchValue === "DATE_MET_AS_PLANNED" && typeof getNestedNextActionId(branch) === "string"
|
|
17456
|
+
);
|
|
17457
|
+
if (hasExpectedInputValue && hasDateMetAsPlannedBranch) continue;
|
|
17458
|
+
issues.push({
|
|
17459
|
+
severity: "error",
|
|
17460
|
+
operation_index: operationIndex,
|
|
17461
|
+
code: "WORKFLOW_DELAY_UNTIL_DATE_BRANCH_REQUIRED",
|
|
17462
|
+
message: `Action '${delayActionId}' (0-35 delay until date) must be followed by a STATIC_BRANCH with inputValue { actionId: '${delayActionId}', dataKey: 'hs_delay_status', type: 'FIELD_DATA' } and a DATE_MET_AS_PLANNED branch connection.`
|
|
17463
|
+
});
|
|
17464
|
+
}
|
|
17465
|
+
return issues;
|
|
17466
|
+
};
|
|
17169
17467
|
var validateRotateToOwnerAction = (params) => {
|
|
17170
17468
|
const { action, actionId, operationIndex } = params;
|
|
17171
17469
|
if (action.actionTypeId !== "0-11") return [];
|
|
@@ -17312,6 +17610,7 @@ var validateWorkflowActions = (params) => {
|
|
|
17312
17610
|
issues.push(...validateRotateToOwnerAction({ action, actionId, operationIndex }));
|
|
17313
17611
|
issues.push(...validateCreateTaskAssociations({ action, actionId, operationIndex, context }));
|
|
17314
17612
|
}
|
|
17613
|
+
issues.push(...validateDelayUntilDateBranching({ actions, operationIndex }));
|
|
17315
17614
|
for (const action of actions) {
|
|
17316
17615
|
for (const nextActionId of collectActionNextActionIds(action)) {
|
|
17317
17616
|
if (actionIds.has(nextActionId)) continue;
|
|
@@ -19884,17 +20183,20 @@ var mainProgram = Effect108.gen(function* () {
|
|
|
19884
20183
|
};
|
|
19885
20184
|
registerPortalSelection(server, { ws, config, handoffSelectedPortal });
|
|
19886
20185
|
registerQueryTool(server, {
|
|
20186
|
+
isMasterClient,
|
|
19887
20187
|
getSelectedPortalId: getPortalId,
|
|
19888
20188
|
getPortals,
|
|
19889
20189
|
ensureFresh
|
|
19890
20190
|
});
|
|
19891
20191
|
registerChartTool(server, {
|
|
20192
|
+
isMasterClient,
|
|
19892
20193
|
getSelectedPortalId: getPortalId,
|
|
19893
20194
|
getPortals,
|
|
19894
20195
|
ensureFresh
|
|
19895
20196
|
});
|
|
19896
20197
|
registerStatusTool(server, {
|
|
19897
20198
|
getConnectionState: () => Effect108.runSync(ws.getState()),
|
|
20199
|
+
isMasterClient,
|
|
19898
20200
|
getPortals,
|
|
19899
20201
|
getSelectedPortalId: getPortalId,
|
|
19900
20202
|
ensureFresh
|