@promptowl/contextnest-community 1.15.0 → 1.16.1
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/CONFIGURATION.md +1 -0
- package/dist/{chunk-GWDOLYFJ.js → chunk-CLKZZZS2.js} +2 -2
- package/dist/{chunk-JWVZ3T35.js → chunk-D6HICFVW.js} +21 -2
- package/dist/{chunk-BCFKLY4H.js → chunk-JMJLNEXE.js} +24 -0
- package/dist/{chunk-7BOG3S5H.js → chunk-L7UCMGWZ.js} +89 -92
- package/dist/{chunk-5IAKUUZF.js → chunk-OTGZQZKK.js} +1 -1
- package/dist/{chunk-C4U75IUR.js → chunk-QJVRZT6M.js} +21 -58
- package/dist/{client-NFUHPLLM.js → client-OBS5HOTA.js} +1 -1
- package/dist/{grants-service-O4AO326A.js → grants-service-I535QVTW.js} +2 -2
- package/dist/index.js +961 -493
- package/dist/{review-service-JUAFYB73.js → review-service-V2GR6VM3.js} +6 -6
- package/dist/{stewardship-service-FHJ2RBKN.js → stewardship-service-LTREGVSJ.js} +3 -3
- package/dist/{version-service-ECYCBVSE.js → version-service-V6K5SZ3P.js} +7 -3
- package/dist/web3/assets/index-DnQARUL3.css +1 -0
- package/dist/web3/assets/index-_m7WyozM.js +1051 -0
- package/dist/web3/index.html +2 -2
- package/package.json +4 -3
- package/dist/web3/assets/index-BaMJ4tEz.css +0 -1
- package/dist/web3/assets/index-D950G4hR.js +0 -1052
package/dist/index.js
CHANGED
|
@@ -35,14 +35,11 @@ import {
|
|
|
35
35
|
requireWorkflowPlane,
|
|
36
36
|
resolveCallerEmail,
|
|
37
37
|
safeJson,
|
|
38
|
-
safePublishDocument,
|
|
39
38
|
seedDefaultEdgeTypes,
|
|
40
39
|
sendTestNotification,
|
|
41
|
-
serializeNodeSafe,
|
|
42
|
-
stripUndefinedDeep,
|
|
43
40
|
submitForReview,
|
|
44
41
|
withNodeWriteLock
|
|
45
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-QJVRZT6M.js";
|
|
46
43
|
import {
|
|
47
44
|
createGrant,
|
|
48
45
|
deleteGrant,
|
|
@@ -51,7 +48,7 @@ import {
|
|
|
51
48
|
listGrants,
|
|
52
49
|
listUserGrants,
|
|
53
50
|
resolveNodeGrant
|
|
54
|
-
} from "./chunk-
|
|
51
|
+
} from "./chunk-OTGZQZKK.js";
|
|
55
52
|
import {
|
|
56
53
|
generateApiKey,
|
|
57
54
|
getKeyPrefix,
|
|
@@ -64,13 +61,15 @@ import {
|
|
|
64
61
|
checkConflict,
|
|
65
62
|
createVersion,
|
|
66
63
|
getApprovedVersion,
|
|
64
|
+
getApprovedVersions,
|
|
67
65
|
getCurrentVersion,
|
|
68
66
|
getDisplayStatus,
|
|
67
|
+
getTrackedNodeIds,
|
|
69
68
|
getVersions,
|
|
70
69
|
listMyDrafts,
|
|
71
70
|
setApprovedVersion,
|
|
72
71
|
upsertVersion
|
|
73
|
-
} from "./chunk-
|
|
72
|
+
} from "./chunk-D6HICFVW.js";
|
|
74
73
|
import {
|
|
75
74
|
canCreateInNest,
|
|
76
75
|
canManageStewards,
|
|
@@ -90,7 +89,7 @@ import {
|
|
|
90
89
|
resolveUserRoles,
|
|
91
90
|
syncFromConfig,
|
|
92
91
|
updateSteward
|
|
93
|
-
} from "./chunk-
|
|
92
|
+
} from "./chunk-CLKZZZS2.js";
|
|
94
93
|
import {
|
|
95
94
|
addMember,
|
|
96
95
|
buildTitleMap,
|
|
@@ -101,6 +100,7 @@ import {
|
|
|
101
100
|
deleteTeam,
|
|
102
101
|
disableStewardshipAndWipeGovernance,
|
|
103
102
|
docLink,
|
|
103
|
+
engineApi,
|
|
104
104
|
engineCache,
|
|
105
105
|
findTeamByExternalId,
|
|
106
106
|
folderForNode,
|
|
@@ -136,6 +136,7 @@ import {
|
|
|
136
136
|
nestStorageRoot,
|
|
137
137
|
normalizeTag,
|
|
138
138
|
nowExpr,
|
|
139
|
+
opContext,
|
|
139
140
|
permissionLevel,
|
|
140
141
|
persistSetting,
|
|
141
142
|
removeMember,
|
|
@@ -161,7 +162,7 @@ import {
|
|
|
161
162
|
updateMemberRole,
|
|
162
163
|
validateLicense,
|
|
163
164
|
verifySmtp
|
|
164
|
-
} from "./chunk-
|
|
165
|
+
} from "./chunk-L7UCMGWZ.js";
|
|
165
166
|
import {
|
|
166
167
|
AppError,
|
|
167
168
|
ConflictError,
|
|
@@ -177,7 +178,7 @@ import {
|
|
|
177
178
|
initDb,
|
|
178
179
|
isEmailListish,
|
|
179
180
|
isEmailish
|
|
180
|
-
} from "./chunk-
|
|
181
|
+
} from "./chunk-JMJLNEXE.js";
|
|
181
182
|
import {
|
|
182
183
|
ANON_EMAIL,
|
|
183
184
|
ANON_USER_ID
|
|
@@ -195,6 +196,29 @@ import { LinearRouter } from "hono/router/linear-router";
|
|
|
195
196
|
import { createMiddleware as createMiddleware2 } from "hono/factory";
|
|
196
197
|
import { cors } from "hono/cors";
|
|
197
198
|
|
|
199
|
+
// src/shared/artifact-sandbox.ts
|
|
200
|
+
var ARTIFACT_SANDBOX_CSP = [
|
|
201
|
+
"sandbox allow-scripts allow-popups allow-modals",
|
|
202
|
+
"default-src 'none'",
|
|
203
|
+
"script-src 'unsafe-inline'",
|
|
204
|
+
"style-src 'unsafe-inline'",
|
|
205
|
+
"img-src data: blob:",
|
|
206
|
+
"font-src data:",
|
|
207
|
+
"connect-src 'none'",
|
|
208
|
+
"base-uri 'none'",
|
|
209
|
+
"form-action 'none'"
|
|
210
|
+
].join("; ");
|
|
211
|
+
function mergeFrameAncestors(existing, directive) {
|
|
212
|
+
if (!existing) return directive;
|
|
213
|
+
if (/(^|;)\s*frame-ancestors\b/i.test(existing)) return existing;
|
|
214
|
+
return `${existing}; ${directive}`;
|
|
215
|
+
}
|
|
216
|
+
function applyArtifactSandboxHeaders(c) {
|
|
217
|
+
c.header("Content-Security-Policy", ARTIFACT_SANDBOX_CSP);
|
|
218
|
+
c.header("X-Content-Type-Options", "nosniff");
|
|
219
|
+
c.header("Referrer-Policy", "no-referrer");
|
|
220
|
+
}
|
|
221
|
+
|
|
198
222
|
// src/auth/routes.ts
|
|
199
223
|
import { Hono as Hono2 } from "hono";
|
|
200
224
|
import { v4 as uuid2 } from "uuid";
|
|
@@ -1580,17 +1604,42 @@ authRoutes.get("/teammates", async (c) => {
|
|
|
1580
1604
|
|
|
1581
1605
|
// src/nests/routes.ts
|
|
1582
1606
|
import { Hono as Hono3 } from "hono";
|
|
1583
|
-
import { streamText } from "hono/streaming";
|
|
1584
1607
|
|
|
1585
1608
|
// src/nodes/service.ts
|
|
1586
1609
|
import {
|
|
1587
|
-
serializeDocument,
|
|
1588
1610
|
parseDocument as parseDocument2,
|
|
1589
1611
|
normalizeStatus,
|
|
1612
|
+
isRejected,
|
|
1590
1613
|
DocumentNotFoundError
|
|
1591
1614
|
} from "@promptowl/contextnest-engine";
|
|
1592
|
-
import { rename, mkdir
|
|
1593
|
-
import { dirname, join as join2
|
|
1615
|
+
import { rename, mkdir } from "fs/promises";
|
|
1616
|
+
import { dirname, join as join2 } from "path";
|
|
1617
|
+
|
|
1618
|
+
// src/governance/safe-publish.ts
|
|
1619
|
+
import {
|
|
1620
|
+
publishDocument,
|
|
1621
|
+
serializeDocument
|
|
1622
|
+
} from "@promptowl/contextnest-engine";
|
|
1623
|
+
function serializeNodeSafe(node) {
|
|
1624
|
+
return serializeDocument({
|
|
1625
|
+
...node,
|
|
1626
|
+
frontmatter: stripUndefinedDeep(node.frontmatter)
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
function stripUndefinedDeep(value) {
|
|
1630
|
+
if (Array.isArray(value)) {
|
|
1631
|
+
return value.filter((v) => v !== void 0).map((v) => stripUndefinedDeep(v));
|
|
1632
|
+
}
|
|
1633
|
+
if (value && typeof value === "object") {
|
|
1634
|
+
const out = {};
|
|
1635
|
+
for (const [k, v] of Object.entries(value)) {
|
|
1636
|
+
if (v === void 0) continue;
|
|
1637
|
+
out[k] = stripUndefinedDeep(v);
|
|
1638
|
+
}
|
|
1639
|
+
return out;
|
|
1640
|
+
}
|
|
1641
|
+
return value;
|
|
1642
|
+
}
|
|
1594
1643
|
|
|
1595
1644
|
// src/governance/prime-service.ts
|
|
1596
1645
|
async function isPrimeDocument(nestId, tags, metadata) {
|
|
@@ -1615,6 +1664,7 @@ import { join } from "path";
|
|
|
1615
1664
|
import {
|
|
1616
1665
|
detectDrift,
|
|
1617
1666
|
stageSuggestion,
|
|
1667
|
+
ChainEventLog,
|
|
1618
1668
|
approveSuggestion,
|
|
1619
1669
|
rejectSuggestion,
|
|
1620
1670
|
listSuggestions,
|
|
@@ -1697,7 +1747,8 @@ async function scanDocumentForDriftInternal(nestId, documentId, actor) {
|
|
|
1697
1747
|
return { meta: result.meta, created: true };
|
|
1698
1748
|
}
|
|
1699
1749
|
async function scanNestForDrift(nestId, actor = "system:scanner") {
|
|
1700
|
-
const { storage } = await engineCache.get(nestId);
|
|
1750
|
+
const { storage, invalidateDiscovery } = await engineCache.get(nestId);
|
|
1751
|
+
invalidateDiscovery();
|
|
1701
1752
|
const docs = await storage.discoverDocuments();
|
|
1702
1753
|
const results = await Promise.all(
|
|
1703
1754
|
docs.map((doc) => scanDocumentForDriftInternal(nestId, doc.id, actor))
|
|
@@ -1768,6 +1819,68 @@ async function getExternalEditDetail(nestId, documentId, suggestionId) {
|
|
|
1768
1819
|
patch: found.patch
|
|
1769
1820
|
};
|
|
1770
1821
|
}
|
|
1822
|
+
async function recordVerdict(nestId, event) {
|
|
1823
|
+
try {
|
|
1824
|
+
const { storage } = await engineCache.get(nestId);
|
|
1825
|
+
await new ChainEventLog(storage).append(event);
|
|
1826
|
+
return true;
|
|
1827
|
+
} catch (err) {
|
|
1828
|
+
console.error(
|
|
1829
|
+
`[external-edit] failed to record verdict ${event.event_id} for nest ${nestId}:`,
|
|
1830
|
+
err
|
|
1831
|
+
);
|
|
1832
|
+
return false;
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
async function listExternalEditVerdicts(nestId, documentId) {
|
|
1836
|
+
try {
|
|
1837
|
+
const { storage } = await engineCache.get(nestId);
|
|
1838
|
+
const events = await new ChainEventLog(storage).readByDocument(documentId);
|
|
1839
|
+
return events.flatMap((e) => {
|
|
1840
|
+
const meta = e.action_metadata || {};
|
|
1841
|
+
if (typeof meta.suggestion_id !== "string") return [];
|
|
1842
|
+
const reason = meta.rejection_reason;
|
|
1843
|
+
const comment = meta.approval_comment;
|
|
1844
|
+
return [
|
|
1845
|
+
{
|
|
1846
|
+
suggestion_id: meta.suggestion_id,
|
|
1847
|
+
status: typeof reason === "string" ? "rejected" : "approved",
|
|
1848
|
+
actor: e.actor,
|
|
1849
|
+
at: e.timestamp,
|
|
1850
|
+
note: typeof reason === "string" ? reason : typeof comment === "string" ? comment : void 0,
|
|
1851
|
+
source: typeof meta.source === "string" ? meta.source : void 0
|
|
1852
|
+
}
|
|
1853
|
+
];
|
|
1854
|
+
});
|
|
1855
|
+
} catch (err) {
|
|
1856
|
+
console.error(
|
|
1857
|
+
`[external-edit] failed to read verdicts for ${nestId}/${documentId}:`,
|
|
1858
|
+
err
|
|
1859
|
+
);
|
|
1860
|
+
return [];
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
async function mirrorVersion(input) {
|
|
1864
|
+
const { storage } = await engineCache.get(input.nestId);
|
|
1865
|
+
const node = await storage.readDocument(input.documentId);
|
|
1866
|
+
const { upsertVersion: upsertVersion2, setApprovedVersion: setApprovedVersion2 } = await import("./version-service-V6K5SZ3P.js");
|
|
1867
|
+
await upsertVersion2({
|
|
1868
|
+
nestId: input.nestId,
|
|
1869
|
+
nodeId: input.documentId,
|
|
1870
|
+
version: input.version,
|
|
1871
|
+
content: node.body || "",
|
|
1872
|
+
author: input.actor,
|
|
1873
|
+
status: "published",
|
|
1874
|
+
tags: node.frontmatter.tags || [],
|
|
1875
|
+
changeNote: input.note
|
|
1876
|
+
});
|
|
1877
|
+
await setApprovedVersion2(
|
|
1878
|
+
input.nestId,
|
|
1879
|
+
input.documentId,
|
|
1880
|
+
input.version,
|
|
1881
|
+
input.actor
|
|
1882
|
+
);
|
|
1883
|
+
}
|
|
1771
1884
|
async function approveExternalEdit(input) {
|
|
1772
1885
|
const { storage } = await engineCache.get(input.nestId);
|
|
1773
1886
|
let result;
|
|
@@ -1789,33 +1902,21 @@ async function approveExternalEdit(input) {
|
|
|
1789
1902
|
throw err;
|
|
1790
1903
|
}
|
|
1791
1904
|
try {
|
|
1792
|
-
|
|
1793
|
-
const versionNum = result.versionEntry.version;
|
|
1794
|
-
const tags = node.frontmatter.tags || [];
|
|
1795
|
-
const { createVersion: createVersion2, setApprovedVersion: setApprovedVersion2 } = await import("./version-service-ECYCBVSE.js");
|
|
1796
|
-
await createVersion2({
|
|
1905
|
+
await mirrorVersion({
|
|
1797
1906
|
nestId: input.nestId,
|
|
1798
|
-
|
|
1799
|
-
version:
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
status: "published",
|
|
1803
|
-
tags,
|
|
1804
|
-
changeNote: input.comment || "External edit approved"
|
|
1907
|
+
documentId: input.documentId,
|
|
1908
|
+
version: result.versionEntry.version,
|
|
1909
|
+
actor: input.actor,
|
|
1910
|
+
note: input.comment ? `External edit approved: ${input.comment}` : "External edit approved"
|
|
1805
1911
|
});
|
|
1806
|
-
await setApprovedVersion2(
|
|
1807
|
-
input.nestId,
|
|
1808
|
-
input.documentId,
|
|
1809
|
-
versionNum,
|
|
1810
|
-
input.actor
|
|
1811
|
-
);
|
|
1812
1912
|
} catch (err) {
|
|
1813
1913
|
console.error(
|
|
1814
1914
|
`[external-edit] failed to mirror approved version into node_versions for ${input.nestId}/${input.documentId}:`,
|
|
1815
1915
|
err
|
|
1816
1916
|
);
|
|
1817
1917
|
}
|
|
1818
|
-
|
|
1918
|
+
const historyRecorded = await recordVerdict(input.nestId, result.chainEvent);
|
|
1919
|
+
return { ...result, historyRecorded };
|
|
1819
1920
|
}
|
|
1820
1921
|
async function rejectExternalEdit(input) {
|
|
1821
1922
|
const { storage } = await engineCache.get(input.nestId);
|
|
@@ -1828,6 +1929,7 @@ async function rejectExternalEdit(input) {
|
|
|
1828
1929
|
zone: "default",
|
|
1829
1930
|
reason: input.reason
|
|
1830
1931
|
});
|
|
1932
|
+
const historyRecorded = await recordVerdict(input.nestId, result.chainEvent);
|
|
1831
1933
|
try {
|
|
1832
1934
|
const approved = await loadChainHead(storage, input.documentId);
|
|
1833
1935
|
if (approved) {
|
|
@@ -1839,7 +1941,7 @@ async function rejectExternalEdit(input) {
|
|
|
1839
1941
|
err
|
|
1840
1942
|
);
|
|
1841
1943
|
}
|
|
1842
|
-
return result;
|
|
1944
|
+
return { ...result, historyRecorded };
|
|
1843
1945
|
}
|
|
1844
1946
|
var scannerTimer = null;
|
|
1845
1947
|
async function scanAllNests() {
|
|
@@ -1881,12 +1983,24 @@ function assertTitleLength(title) {
|
|
|
1881
1983
|
);
|
|
1882
1984
|
}
|
|
1883
1985
|
}
|
|
1986
|
+
function assertTitleSluggable(title) {
|
|
1987
|
+
if (!slugify(title)) {
|
|
1988
|
+
throw new ValidationError(
|
|
1989
|
+
"the document id is built from the title using a-z and 0-9, so the title needs at least one of those"
|
|
1990
|
+
);
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1884
1993
|
var slugify = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1885
1994
|
function isIdUnderFolder(id, folder) {
|
|
1886
1995
|
return id.startsWith(`nodes/${folder}/`) || id.startsWith(`${folder}/`);
|
|
1887
1996
|
}
|
|
1888
1997
|
function folderToSegments(folder) {
|
|
1889
1998
|
const segments = folder.split("/").map(slugify).filter(Boolean);
|
|
1999
|
+
if (folder.trim() && segments.length === 0) {
|
|
2000
|
+
throw new ValidationError(
|
|
2001
|
+
"the folder path is built from the name using a-z and 0-9, so the name needs at least one of those"
|
|
2002
|
+
);
|
|
2003
|
+
}
|
|
1890
2004
|
if (segments[0] === "nodes") segments.shift();
|
|
1891
2005
|
if (segments.length > 8) {
|
|
1892
2006
|
throw new ValidationError("folder may nest at most 8 levels deep");
|
|
@@ -1898,7 +2012,6 @@ function folderToSegments(folder) {
|
|
|
1898
2012
|
}
|
|
1899
2013
|
return segments;
|
|
1900
2014
|
}
|
|
1901
|
-
var stripUndefined = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
|
|
1902
2015
|
async function departmentTagFor(userEmail) {
|
|
1903
2016
|
if (!config.OIDC_DEPARTMENT_TAGGING) return null;
|
|
1904
2017
|
try {
|
|
@@ -1915,6 +2028,12 @@ async function departmentTagFor(userEmail) {
|
|
|
1915
2028
|
}
|
|
1916
2029
|
function toSafeError(err, context, message) {
|
|
1917
2030
|
if (err instanceof AppError) return err;
|
|
2031
|
+
if (err?.code === "REJECTED_DOCUMENT") {
|
|
2032
|
+
console.error(`${context}:`, err);
|
|
2033
|
+
return new ConflictError(
|
|
2034
|
+
"This document is rejected. Change its status before saving."
|
|
2035
|
+
);
|
|
2036
|
+
}
|
|
1918
2037
|
console.error(`${context}:`, err);
|
|
1919
2038
|
return new AppError(500, message);
|
|
1920
2039
|
}
|
|
@@ -2005,28 +2124,89 @@ async function resolveAuthorship(nestId, nodes) {
|
|
|
2005
2124
|
}
|
|
2006
2125
|
return result;
|
|
2007
2126
|
}
|
|
2008
|
-
async function
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
const wanted = new Set(
|
|
2013
|
-
Array.isArray(filters.type) ? filters.type : [filters.type]
|
|
2127
|
+
async function getNodeForCaller(nestId, selector, userId, userEmail, opts = {}) {
|
|
2128
|
+
if (selector.id && !await canReadNode(nestId, selector.id, userId, userEmail)) {
|
|
2129
|
+
throw new ForbiddenError(
|
|
2130
|
+
"Access denied \u2014 no steward assignment for this node"
|
|
2014
2131
|
);
|
|
2015
|
-
documents = documents.filter((n) => wanted.has(n.frontmatter.type));
|
|
2016
2132
|
}
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2133
|
+
let got;
|
|
2134
|
+
try {
|
|
2135
|
+
got = await engineApi.run(
|
|
2136
|
+
"context_get",
|
|
2137
|
+
{
|
|
2138
|
+
...selector,
|
|
2139
|
+
include_raw: true,
|
|
2140
|
+
// A retired node stays readable here: its stewards decide whether to
|
|
2141
|
+
// revive it, and they cannot do that if the read refuses.
|
|
2142
|
+
allow_rejected: true,
|
|
2143
|
+
...opts.verifyChecksum ? { verify_checksum: true } : {}
|
|
2144
|
+
},
|
|
2145
|
+
await opContext(nestId, userEmail)
|
|
2146
|
+
);
|
|
2147
|
+
} catch (err) {
|
|
2148
|
+
if (err?.code !== "DOCUMENT_NOT_FOUND") {
|
|
2149
|
+
console.error(`getNodeForCaller failed (${nestId}/${selector.id ?? selector.title})`, err);
|
|
2150
|
+
}
|
|
2151
|
+
throw new NotFoundError(`Node not found: ${selector.id ?? selector.title}`);
|
|
2152
|
+
}
|
|
2153
|
+
if (!selector.id && !await canReadNode(nestId, got.id, userId, userEmail)) {
|
|
2154
|
+
throw new ForbiddenError(
|
|
2155
|
+
"Access denied \u2014 no steward assignment for this node"
|
|
2021
2156
|
);
|
|
2022
2157
|
}
|
|
2158
|
+
return {
|
|
2159
|
+
id: got.id,
|
|
2160
|
+
filePath: "",
|
|
2161
|
+
rawContent: got.raw ?? "",
|
|
2162
|
+
frontmatter: got.frontmatter,
|
|
2163
|
+
body: got.body,
|
|
2164
|
+
...got.pendingChange ? { pendingChange: got.pendingChange } : {}
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
async function listNodesForCaller(nestId, userId, filters = {}) {
|
|
2168
|
+
const { versions: versionManager } = await engineCache.get(nestId);
|
|
2023
2169
|
const userEmail = await resolveCallerEmail(userId);
|
|
2024
|
-
|
|
2170
|
+
const { documents } = await engineApi.run(
|
|
2171
|
+
"context_list",
|
|
2172
|
+
{
|
|
2173
|
+
...filters.type ? { type: filters.type } : {},
|
|
2174
|
+
...filters.tag ? { tag: filters.tag } : {},
|
|
2175
|
+
...filters.status ? { status: filters.status } : {},
|
|
2176
|
+
// Unlike the CLI and mcp-server, a retired node stays in this listing:
|
|
2177
|
+
// here it is an unpublished document its stewards may still need to act
|
|
2178
|
+
// on, not one removed from retrieval. An imported vault carrying
|
|
2179
|
+
// `status: rejected` registers exactly that way.
|
|
2180
|
+
include_retired: true,
|
|
2181
|
+
full: true
|
|
2182
|
+
},
|
|
2183
|
+
await opContext(nestId, userEmail)
|
|
2184
|
+
);
|
|
2185
|
+
const nodes = documents.map((d) => ({
|
|
2186
|
+
id: d.id,
|
|
2187
|
+
filePath: "",
|
|
2188
|
+
rawContent: "",
|
|
2189
|
+
frontmatter: d.frontmatter,
|
|
2190
|
+
body: d.body
|
|
2191
|
+
}));
|
|
2192
|
+
const publicReader = await isPublicReader(nestId, userId);
|
|
2193
|
+
const approvedOnly = publicReader || filters.approvedOnly === true;
|
|
2194
|
+
const approvedVersions = approvedOnly ? await getApprovedVersions(nestId) : null;
|
|
2195
|
+
let accessible = await filterAccessible(
|
|
2196
|
+
nestId,
|
|
2197
|
+
userId,
|
|
2198
|
+
userEmail,
|
|
2199
|
+
nodes,
|
|
2200
|
+
approvedVersions ?? void 0
|
|
2201
|
+
);
|
|
2025
2202
|
if (await resolveNestPermission(nestId, userId) === "none" && !await isStewardshipEnabled(nestId)) {
|
|
2026
2203
|
const grants = await listUserGrants(nestId, userId);
|
|
2027
|
-
accessible = grants.length ?
|
|
2204
|
+
accessible = grants.length ? nodes.filter((d) => grantCoversNode(grants, d.id)) : accessible;
|
|
2028
2205
|
}
|
|
2029
|
-
|
|
2206
|
+
if (approvedVersions && !publicReader) {
|
|
2207
|
+
accessible = accessible.filter((d) => approvedVersions.has(d.id));
|
|
2208
|
+
}
|
|
2209
|
+
if (filters.limit) accessible = accessible.slice(0, filters.limit);
|
|
2030
2210
|
const authorship = publicReader ? null : await resolveAuthorship(
|
|
2031
2211
|
nestId,
|
|
2032
2212
|
accessible.map((d) => ({
|
|
@@ -2042,8 +2222,8 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
|
|
|
2042
2222
|
r.author = people.author;
|
|
2043
2223
|
r.collaborators = people.collaborators;
|
|
2044
2224
|
}
|
|
2045
|
-
if (
|
|
2046
|
-
const approved =
|
|
2225
|
+
if (approvedVersions) {
|
|
2226
|
+
const approved = approvedVersions.get(doc.id);
|
|
2047
2227
|
if (approved != null) {
|
|
2048
2228
|
try {
|
|
2049
2229
|
const raw = await versionManager.reconstructVersion(
|
|
@@ -2080,7 +2260,7 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
|
|
|
2080
2260
|
return r;
|
|
2081
2261
|
})
|
|
2082
2262
|
);
|
|
2083
|
-
return
|
|
2263
|
+
return enriched;
|
|
2084
2264
|
}
|
|
2085
2265
|
async function listNodesForCallerByEmail(nestId, userEmail, filters = {}) {
|
|
2086
2266
|
return listNodesForCaller(nestId, await userIdFromEmail(userEmail), filters);
|
|
@@ -2109,13 +2289,13 @@ async function autoPublishReason(nestId, userEmail, doc) {
|
|
|
2109
2289
|
async function createNode(nestId, input, userEmail) {
|
|
2110
2290
|
const { storage, versions: versionManager } = await engineCache.get(nestId);
|
|
2111
2291
|
assertTitleLength(input.title);
|
|
2292
|
+
if (!input.id) assertTitleSluggable(input.title);
|
|
2112
2293
|
const slug = slugify(input.title).slice(0, 100).replace(/-+$/, "");
|
|
2113
2294
|
let id = input.id;
|
|
2114
2295
|
if (!id) {
|
|
2115
2296
|
const folderSegments = folderToSegments(input.folder ?? "");
|
|
2116
2297
|
id = folderSegments.length > 0 ? `nodes/${folderSegments.join("/")}/${slug}` : `nodes/${slug}`;
|
|
2117
2298
|
}
|
|
2118
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2119
2299
|
const tags = (input.tags || []).map(normalizeTag2);
|
|
2120
2300
|
const requestedType = input.type || "document";
|
|
2121
2301
|
if (requestedType === "artifact" && !config.TYPE_ARTIFACT_ENABLED) {
|
|
@@ -2172,33 +2352,62 @@ async function createNode(nestId, input, userEmail) {
|
|
|
2172
2352
|
tags,
|
|
2173
2353
|
metadata
|
|
2174
2354
|
});
|
|
2175
|
-
const
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2355
|
+
const publishNote = publishReason === null ? void 0 : `Auto-published on create (${publishReason})`;
|
|
2356
|
+
let published = publishReason !== null;
|
|
2357
|
+
try {
|
|
2358
|
+
await engineApi.run(
|
|
2359
|
+
"context_create",
|
|
2360
|
+
{
|
|
2361
|
+
id,
|
|
2362
|
+
title: input.title,
|
|
2363
|
+
content: input.content,
|
|
2364
|
+
type: requestedType,
|
|
2365
|
+
// A skill node is invalid without a skill block, and this surface has
|
|
2366
|
+
// no field for one — default the trigger from the title, same as the
|
|
2367
|
+
// CLI and mcp-server do.
|
|
2368
|
+
...requestedType === "skill" ? { trigger: `when asked to ${input.title.toLowerCase()}` } : {},
|
|
2369
|
+
...tags.length ? { tags } : {},
|
|
2370
|
+
...input.status ? { status: input.status } : {},
|
|
2371
|
+
metadata,
|
|
2372
|
+
publish: published,
|
|
2373
|
+
...publishNote ? { note: publishNote } : {}
|
|
2374
|
+
},
|
|
2375
|
+
await opContext(nestId, userEmail)
|
|
2376
|
+
);
|
|
2377
|
+
} catch (err) {
|
|
2378
|
+
let onDisk = true;
|
|
2379
|
+
try {
|
|
2380
|
+
await storage.readDocument(id);
|
|
2381
|
+
} catch {
|
|
2382
|
+
onDisk = false;
|
|
2383
|
+
}
|
|
2384
|
+
if (!onDisk) throw err;
|
|
2385
|
+
console.error("publishDocument failed (node create auto-publish)", err);
|
|
2386
|
+
published = false;
|
|
2387
|
+
}
|
|
2388
|
+
const node = await storage.readDocument(id);
|
|
2195
2389
|
await syncNodeTags(nestId, id, tags);
|
|
2196
2390
|
let savedVersion = 1;
|
|
2197
|
-
if (
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2391
|
+
if (published) {
|
|
2392
|
+
savedVersion = node.frontmatter.version || 1;
|
|
2393
|
+
await createVersion({
|
|
2394
|
+
nestId,
|
|
2395
|
+
nodeId: id,
|
|
2396
|
+
version: savedVersion,
|
|
2397
|
+
content: node.body || "",
|
|
2398
|
+
author: userEmail,
|
|
2399
|
+
status: "published",
|
|
2400
|
+
tags,
|
|
2401
|
+
changeNote: publishNote
|
|
2402
|
+
});
|
|
2403
|
+
await setApprovedVersion(nestId, id, savedVersion, userEmail);
|
|
2404
|
+
} else {
|
|
2405
|
+
if (publishReason === null) {
|
|
2406
|
+
try {
|
|
2407
|
+
await versionManager.createVersion(node, userEmail);
|
|
2408
|
+
} catch (err) {
|
|
2409
|
+
console.error("VersionManager.createVersion failed (node create)", err);
|
|
2410
|
+
}
|
|
2202
2411
|
}
|
|
2203
2412
|
await createVersion({
|
|
2204
2413
|
nestId,
|
|
@@ -2209,170 +2418,100 @@ async function createNode(nestId, input, userEmail) {
|
|
|
2209
2418
|
status: "draft",
|
|
2210
2419
|
tags
|
|
2211
2420
|
});
|
|
2212
|
-
} else {
|
|
2213
|
-
const publishNote = `Auto-published on create (${publishReason})`;
|
|
2214
|
-
try {
|
|
2215
|
-
const result = await safePublishDocument(storage, id, {
|
|
2216
|
-
editedBy: userEmail,
|
|
2217
|
-
note: publishNote
|
|
2218
|
-
});
|
|
2219
|
-
savedVersion = result.node.frontmatter.version || 1;
|
|
2220
|
-
await createVersion({
|
|
2221
|
-
nestId,
|
|
2222
|
-
nodeId: id,
|
|
2223
|
-
version: savedVersion,
|
|
2224
|
-
content: result.node.body || "",
|
|
2225
|
-
author: userEmail,
|
|
2226
|
-
status: "published",
|
|
2227
|
-
tags,
|
|
2228
|
-
changeNote: publishNote
|
|
2229
|
-
});
|
|
2230
|
-
await setApprovedVersion(nestId, id, savedVersion, userEmail);
|
|
2231
|
-
node = result.node;
|
|
2232
|
-
} catch (err) {
|
|
2233
|
-
console.error("publishDocument failed (node create auto-publish)", err);
|
|
2234
|
-
await createVersion({
|
|
2235
|
-
nestId,
|
|
2236
|
-
nodeId: id,
|
|
2237
|
-
version: 1,
|
|
2238
|
-
content: input.content,
|
|
2239
|
-
author: userEmail,
|
|
2240
|
-
status: "draft",
|
|
2241
|
-
tags
|
|
2242
|
-
});
|
|
2243
|
-
}
|
|
2244
2421
|
}
|
|
2245
2422
|
await trackEvent("node.create", { nestId, nodeId: id });
|
|
2246
2423
|
return { node, version: savedVersion };
|
|
2247
2424
|
});
|
|
2248
2425
|
}
|
|
2249
|
-
function
|
|
2250
|
-
const
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
return
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
async function registerImportedDocuments(nestId, userEmail, onProgress) {
|
|
2270
|
-
const { storage } = await engineCache.get(nestId);
|
|
2271
|
-
let docs;
|
|
2426
|
+
async function stageImportedFiles(nestId, files, userEmail) {
|
|
2427
|
+
const carried = files.filter(
|
|
2428
|
+
(f) => !String(f?.path || "").split(/[\\/]/).includes("_suggestions")
|
|
2429
|
+
);
|
|
2430
|
+
if (!carried.length) return { written: 0, rejected: [] };
|
|
2431
|
+
const result = await engineApi.run(
|
|
2432
|
+
"context_import",
|
|
2433
|
+
{ files: carried, publish: false },
|
|
2434
|
+
await opContext(nestId, userEmail)
|
|
2435
|
+
);
|
|
2436
|
+
const rejected = (result.failed ?? []).map((f) => f.id ?? "(unnamed)");
|
|
2437
|
+
if (rejected.length) {
|
|
2438
|
+
console.warn(
|
|
2439
|
+
`[import] ${rejected.length} file(s) rejected:`,
|
|
2440
|
+
(result.failed ?? []).map((f) => `${f.id}: ${f.error}`).join(", ")
|
|
2441
|
+
);
|
|
2442
|
+
}
|
|
2443
|
+
return { written: result.written ?? 0, rejected };
|
|
2444
|
+
}
|
|
2445
|
+
async function alignOnDiskStatus(nestId, documentId, status) {
|
|
2272
2446
|
try {
|
|
2273
|
-
|
|
2447
|
+
const { storage } = await engineCache.get(nestId);
|
|
2448
|
+
const node = await storage.readDocument(documentId);
|
|
2449
|
+
if (node.frontmatter.status === status) return;
|
|
2450
|
+
await storage.writeDocument(
|
|
2451
|
+
documentId,
|
|
2452
|
+
serializeNodeSafe({
|
|
2453
|
+
...node,
|
|
2454
|
+
frontmatter: { ...node.frontmatter, status }
|
|
2455
|
+
})
|
|
2456
|
+
);
|
|
2274
2457
|
} catch (err) {
|
|
2275
|
-
console.
|
|
2276
|
-
|
|
2458
|
+
console.warn(
|
|
2459
|
+
`[import] could not align on-disk status for ${nestId}/${documentId}:`,
|
|
2460
|
+
err
|
|
2461
|
+
);
|
|
2277
2462
|
}
|
|
2463
|
+
}
|
|
2464
|
+
async function registerImportedDocuments(nestId, userEmail) {
|
|
2278
2465
|
const startedAt = Date.now();
|
|
2279
|
-
|
|
2280
|
-
await
|
|
2281
|
-
let
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
console.error("import: failed to persist import metadata", nodeId, err);
|
|
2300
|
-
}
|
|
2301
|
-
const rawTags = Array.isArray(doc.frontmatter?.tags) ? doc.frontmatter.tags : [];
|
|
2302
|
-
const tags = rawTags.map((t) => normalizeTag2(String(t)));
|
|
2303
|
-
const fmVersion = Number(doc.frontmatter?.version);
|
|
2304
|
-
let version = Number.isInteger(fmVersion) && fmVersion > 0 ? fmVersion : 1;
|
|
2305
|
-
let content = doc.body || "";
|
|
2306
|
-
const status = explicitFrontmatterStatus(doc.rawContent);
|
|
2307
|
-
if (status && status !== "published" && status !== "approved") {
|
|
2308
|
-
await createVersion({
|
|
2309
|
-
nestId,
|
|
2310
|
-
nodeId,
|
|
2311
|
-
version,
|
|
2312
|
-
content,
|
|
2313
|
-
author: userEmail,
|
|
2314
|
-
status: "draft",
|
|
2315
|
-
changeNote: "Imported from existing folder (kept unpublished)",
|
|
2316
|
-
tags
|
|
2317
|
-
});
|
|
2318
|
-
await syncNodeTags(nestId, nodeId, tags);
|
|
2319
|
-
keptUnpublished++;
|
|
2320
|
-
registered++;
|
|
2321
|
-
continue;
|
|
2322
|
-
}
|
|
2323
|
-
let published = false;
|
|
2324
|
-
try {
|
|
2325
|
-
const result = await safePublishDocument(storage, nodeId, {
|
|
2326
|
-
editedBy: userEmail,
|
|
2327
|
-
note: "Imported from existing folder"
|
|
2328
|
-
});
|
|
2329
|
-
version = result.node.frontmatter.version || version;
|
|
2330
|
-
content = result.node.body || content;
|
|
2331
|
-
published = true;
|
|
2332
|
-
} catch (err) {
|
|
2333
|
-
console.warn(
|
|
2334
|
-
`[import] publish retry needed for ${nodeId}: ${err?.message ?? err}`
|
|
2335
|
-
);
|
|
2336
|
-
try {
|
|
2337
|
-
await resetImportedDocForPublish(storage, nestId, nodeId);
|
|
2338
|
-
const result = await safePublishDocument(storage, nodeId, {
|
|
2339
|
-
editedBy: userEmail,
|
|
2340
|
-
note: "Imported from existing folder (reset chain)"
|
|
2341
|
-
});
|
|
2342
|
-
version = result.node.frontmatter.version || 1;
|
|
2343
|
-
content = result.node.body || content;
|
|
2344
|
-
published = true;
|
|
2345
|
-
publishRecovered++;
|
|
2346
|
-
} catch (err2) {
|
|
2347
|
-
publishFailed++;
|
|
2348
|
-
console.error(
|
|
2349
|
-
"reset+republish failed (import register)",
|
|
2350
|
-
nodeId,
|
|
2351
|
-
err2
|
|
2352
|
-
);
|
|
2353
|
-
}
|
|
2354
|
-
}
|
|
2466
|
+
const tracked = await getTrackedNodeIds(nestId);
|
|
2467
|
+
(await engineCache.get(nestId)).invalidateDiscovery();
|
|
2468
|
+
let result;
|
|
2469
|
+
try {
|
|
2470
|
+
result = await engineApi.run(
|
|
2471
|
+
"context_import",
|
|
2472
|
+
{ discover: true, author: userEmail, exclude_ids: [...tracked] },
|
|
2473
|
+
await opContext(nestId, userEmail)
|
|
2474
|
+
);
|
|
2475
|
+
} catch (err) {
|
|
2476
|
+
console.error("[import] register failed", nestId, err);
|
|
2477
|
+
throw new Error(
|
|
2478
|
+
`Failed to import documents: ${err?.message ?? String(err)}`
|
|
2479
|
+
);
|
|
2480
|
+
}
|
|
2481
|
+
const docs = result.documents ?? [];
|
|
2482
|
+
const engineMs = Date.now() - startedAt;
|
|
2483
|
+
const dbStartedAt = Date.now();
|
|
2484
|
+
for (const d of docs) {
|
|
2485
|
+
const tags = d.tags.map((t) => normalizeTag2(String(t)));
|
|
2355
2486
|
await createVersion({
|
|
2356
2487
|
nestId,
|
|
2357
|
-
nodeId,
|
|
2358
|
-
version,
|
|
2359
|
-
content,
|
|
2488
|
+
nodeId: d.id,
|
|
2489
|
+
version: d.version,
|
|
2490
|
+
content: d.content,
|
|
2360
2491
|
author: userEmail,
|
|
2361
|
-
status:
|
|
2362
|
-
changeNote: "Imported from existing folder",
|
|
2492
|
+
status: d.status,
|
|
2493
|
+
changeNote: d.status === "published" ? "Imported from existing folder" : "Imported from existing folder \u2014 could not be published, kept as a draft",
|
|
2363
2494
|
tags
|
|
2364
2495
|
});
|
|
2365
|
-
if (published) {
|
|
2366
|
-
await setApprovedVersion(nestId,
|
|
2496
|
+
if (d.status === "published") {
|
|
2497
|
+
await setApprovedVersion(nestId, d.id, d.version, userEmail);
|
|
2498
|
+
} else {
|
|
2499
|
+
await alignOnDiskStatus(nestId, d.id, d.status);
|
|
2367
2500
|
}
|
|
2368
|
-
await syncNodeTags(nestId,
|
|
2369
|
-
registered++;
|
|
2501
|
+
await syncNodeTags(nestId, d.id, tags);
|
|
2370
2502
|
}
|
|
2503
|
+
const published = docs.filter((d) => d.status === "published").length;
|
|
2371
2504
|
console.log(
|
|
2372
|
-
`[import] register done: registered=${
|
|
2505
|
+
`[import] register done: registered=${docs.length} published=${published} kept=${docs.length - published} failed=${result.failed.length} alreadyTracked=${tracked.size} engineMs=${engineMs} dbMs=${Date.now() - dbStartedAt}`
|
|
2373
2506
|
);
|
|
2374
|
-
|
|
2375
|
-
|
|
2507
|
+
if (result.failed.length) {
|
|
2508
|
+
console.warn(
|
|
2509
|
+
"[import] documents that would not publish:",
|
|
2510
|
+
result.failed.map((f) => `${f.id}: ${f.error}`).join(", ")
|
|
2511
|
+
);
|
|
2512
|
+
}
|
|
2513
|
+
await trackEvent("nest.import.documents", { nestId, registered: docs.length });
|
|
2514
|
+
return docs.length;
|
|
2376
2515
|
}
|
|
2377
2516
|
async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
2378
2517
|
return withNodeWriteLock(nodeWriteKey(nestId, nodeId), async () => {
|
|
@@ -2386,33 +2525,43 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
|
2386
2525
|
} catch {
|
|
2387
2526
|
throw new NotFoundError(`Node not found: ${nodeId}`);
|
|
2388
2527
|
}
|
|
2528
|
+
const opInput = {};
|
|
2389
2529
|
if (patch.content !== void 0) {
|
|
2390
2530
|
node = { ...node, body: patch.content };
|
|
2531
|
+
opInput.content = patch.content;
|
|
2391
2532
|
}
|
|
2392
2533
|
if (patch.append) {
|
|
2393
2534
|
node = { ...node, body: (node.body || "") + "\n\n" + patch.append };
|
|
2535
|
+
opInput.append = "\n" + patch.append;
|
|
2394
2536
|
}
|
|
2395
2537
|
if (patch.tags) {
|
|
2396
|
-
const
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
];
|
|
2400
|
-
node = { ...node, frontmatter: { ...node.frontmatter, tags: merged } };
|
|
2538
|
+
const nextTags = [...new Set(patch.tags.map(normalizeTag2))];
|
|
2539
|
+
node = { ...node, frontmatter: { ...node.frontmatter, tags: nextTags } };
|
|
2540
|
+
opInput.tags = nextTags;
|
|
2401
2541
|
}
|
|
2402
2542
|
if (patch.status) {
|
|
2403
2543
|
node = {
|
|
2404
2544
|
...node,
|
|
2405
2545
|
frontmatter: { ...node.frontmatter, status: patch.status }
|
|
2406
2546
|
};
|
|
2547
|
+
opInput.status = normalizeStatus(patch.status);
|
|
2548
|
+
} else if (isRejected(node)) {
|
|
2549
|
+
node = {
|
|
2550
|
+
...node,
|
|
2551
|
+
frontmatter: { ...node.frontmatter, status: "draft" }
|
|
2552
|
+
};
|
|
2553
|
+
opInput.status = "draft";
|
|
2407
2554
|
}
|
|
2408
2555
|
if (patch.title) {
|
|
2409
2556
|
if (patch.title !== node.frontmatter.title) {
|
|
2410
2557
|
assertTitleLength(patch.title);
|
|
2558
|
+
assertTitleSluggable(patch.title);
|
|
2411
2559
|
}
|
|
2412
2560
|
node = {
|
|
2413
2561
|
...node,
|
|
2414
2562
|
frontmatter: { ...node.frontmatter, title: patch.title }
|
|
2415
2563
|
};
|
|
2564
|
+
opInput.title = patch.title;
|
|
2416
2565
|
}
|
|
2417
2566
|
if (patch.schedule !== void 0) {
|
|
2418
2567
|
if (!isRunnableType(node.frontmatter.type)) {
|
|
@@ -2426,6 +2575,10 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
|
2426
2575
|
if (patch.schedule === "") delete metadata.schedule;
|
|
2427
2576
|
else metadata.schedule = patch.schedule;
|
|
2428
2577
|
node = { ...node, frontmatter: { ...node.frontmatter, metadata } };
|
|
2578
|
+
opInput.metadata = {
|
|
2579
|
+
...opInput.metadata,
|
|
2580
|
+
schedule: patch.schedule === "" ? null : patch.schedule
|
|
2581
|
+
};
|
|
2429
2582
|
}
|
|
2430
2583
|
if (patch.prime !== void 0) {
|
|
2431
2584
|
await assertMayFlagPrime(nestId, userEmail);
|
|
@@ -2433,6 +2586,7 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
|
2433
2586
|
if (patch.prime === null) delete metadata.prime;
|
|
2434
2587
|
else metadata.prime = patch.prime;
|
|
2435
2588
|
node = { ...node, frontmatter: { ...node.frontmatter, metadata } };
|
|
2589
|
+
opInput.metadata = { ...opInput.metadata, prime: patch.prime };
|
|
2436
2590
|
}
|
|
2437
2591
|
const hasStewards = await isStewardshipEnabled(nestId);
|
|
2438
2592
|
const currentTags = node.frontmatter.tags || [];
|
|
@@ -2451,19 +2605,12 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
|
2451
2605
|
if (publishReason === null || isAuthorEditingOwnReview) {
|
|
2452
2606
|
const currentVersion = await getCurrentVersion(nestId, nodeId);
|
|
2453
2607
|
const newVersion = await versionManager.nextVersion(nodeId, currentVersion);
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
// Drop stale published-state checksum so the next verified read
|
|
2461
|
-
// doesn't flag this write as external drift.
|
|
2462
|
-
checksum: void 0
|
|
2463
|
-
}
|
|
2464
|
-
};
|
|
2465
|
-
node = { ...node, frontmatter: stripUndefined(node.frontmatter) };
|
|
2466
|
-
await storage.writeDocument(nodeId, serializeDocument(node));
|
|
2608
|
+
await engineApi.run(
|
|
2609
|
+
"context_update",
|
|
2610
|
+
{ id: nodeId, ...opInput, publish: false, version: newVersion },
|
|
2611
|
+
await opContext(nestId, userEmail)
|
|
2612
|
+
);
|
|
2613
|
+
node = await storage.readDocument(nodeId);
|
|
2467
2614
|
await syncNodeTags(nestId, nodeId, currentTags);
|
|
2468
2615
|
try {
|
|
2469
2616
|
await versionManager.createVersion(node, userEmail, {
|
|
@@ -2491,26 +2638,15 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
|
2491
2638
|
}
|
|
2492
2639
|
responseVersion = newVersion;
|
|
2493
2640
|
} else {
|
|
2494
|
-
node = {
|
|
2495
|
-
...node,
|
|
2496
|
-
frontmatter: {
|
|
2497
|
-
...node.frontmatter,
|
|
2498
|
-
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2499
|
-
checksum: void 0
|
|
2500
|
-
}
|
|
2501
|
-
};
|
|
2502
|
-
node = { ...node, frontmatter: stripUndefined(node.frontmatter) };
|
|
2503
|
-
await storage.writeDocument(nodeId, serializeDocument(node));
|
|
2504
|
-
await syncNodeTags(nestId, nodeId, currentTags);
|
|
2505
|
-
let publishedVersion = (node.frontmatter.version || 0) + 1;
|
|
2506
2641
|
const publishNote = patch.changeNote || `Auto-published on edit (${publishReason})`;
|
|
2642
|
+
let publishedVersion;
|
|
2507
2643
|
try {
|
|
2508
|
-
const result = await
|
|
2509
|
-
|
|
2510
|
-
note: publishNote
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2644
|
+
const result = await engineApi.run(
|
|
2645
|
+
"context_update",
|
|
2646
|
+
{ id: nodeId, ...opInput, publish: true, note: publishNote },
|
|
2647
|
+
await opContext(nestId, userEmail)
|
|
2648
|
+
);
|
|
2649
|
+
publishedVersion = result.version;
|
|
2514
2650
|
} catch (err) {
|
|
2515
2651
|
console.error(
|
|
2516
2652
|
"publishDocument failed (node patch auto-publish)",
|
|
@@ -2518,6 +2654,8 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
|
2518
2654
|
);
|
|
2519
2655
|
throw err;
|
|
2520
2656
|
}
|
|
2657
|
+
node = await storage.readDocument(nodeId);
|
|
2658
|
+
await syncNodeTags(nestId, nodeId, currentTags);
|
|
2521
2659
|
await upsertVersion({
|
|
2522
2660
|
nestId,
|
|
2523
2661
|
nodeId,
|
|
@@ -2685,14 +2823,22 @@ async function deleteNode(nestId, nodeId, userEmail) {
|
|
|
2685
2823
|
const { storage } = await engineCache.get(nestId);
|
|
2686
2824
|
await assertNotReviewLocked(nestId, nodeId, userEmail, "deleting");
|
|
2687
2825
|
try {
|
|
2688
|
-
await
|
|
2826
|
+
await engineApi.run(
|
|
2827
|
+
"context_delete",
|
|
2828
|
+
{ id: nodeId },
|
|
2829
|
+
await opContext(nestId, userEmail)
|
|
2830
|
+
);
|
|
2689
2831
|
} catch {
|
|
2690
2832
|
throw new NotFoundError(`Node not found: ${nodeId}`);
|
|
2691
2833
|
}
|
|
2692
2834
|
await removeNodeFromTagIndex(nestId, nodeId);
|
|
2693
2835
|
const derivedId = derivedIdOf(nodeId);
|
|
2694
2836
|
try {
|
|
2695
|
-
await
|
|
2837
|
+
await engineApi.run(
|
|
2838
|
+
"context_delete",
|
|
2839
|
+
{ id: derivedId },
|
|
2840
|
+
await opContext(nestId, userEmail)
|
|
2841
|
+
);
|
|
2696
2842
|
await removeNodeFromTagIndex(nestId, derivedId);
|
|
2697
2843
|
} catch {
|
|
2698
2844
|
}
|
|
@@ -2738,8 +2884,9 @@ async function deleteFolder(nestId, folder, userEmail) {
|
|
|
2738
2884
|
}
|
|
2739
2885
|
|
|
2740
2886
|
// src/nests/unsynced-service.ts
|
|
2741
|
-
import { readdirSync, readFileSync,
|
|
2887
|
+
import { readdirSync, readFileSync, statSync } from "fs";
|
|
2742
2888
|
import { join as join3, relative, resolve } from "path";
|
|
2889
|
+
import { rm } from "fs/promises";
|
|
2743
2890
|
var RESERVED = /* @__PURE__ */ new Set(["nests"]);
|
|
2744
2891
|
function isNestStorageRoot(absPath) {
|
|
2745
2892
|
return resolve(absPath) === resolve(nestStorageRoot());
|
|
@@ -2907,23 +3054,31 @@ async function syncUnsyncedFolder(userId, folderName, callerEmail) {
|
|
|
2907
3054
|
`[unsynced] name "${baseName}" already in use, using "${nestName2}" instead`
|
|
2908
3055
|
);
|
|
2909
3056
|
}
|
|
2910
|
-
const nest = await importNest(userId, nestName2
|
|
3057
|
+
const nest = await importNest(userId, nestName2);
|
|
2911
3058
|
console.log(
|
|
2912
3059
|
`[unsynced] nest created id=${nest.id} name="${nest.name}" from folder="${folderName}"`
|
|
2913
3060
|
);
|
|
2914
|
-
|
|
3061
|
+
let documents;
|
|
3062
|
+
try {
|
|
3063
|
+
await stageImportedFiles(nest.id, files, callerEmail);
|
|
3064
|
+
documents = await registerImportedDocuments(nest.id, callerEmail);
|
|
3065
|
+
} catch (err) {
|
|
3066
|
+
await deleteNest(nest.id).catch(() => {
|
|
3067
|
+
});
|
|
3068
|
+
throw err;
|
|
3069
|
+
}
|
|
2915
3070
|
console.log(
|
|
2916
3071
|
`[unsynced] registered ${documents} document(s) for nest ${nest.id}`
|
|
2917
3072
|
);
|
|
2918
3073
|
try {
|
|
2919
|
-
|
|
3074
|
+
await rm(src, { recursive: true, force: true });
|
|
2920
3075
|
console.log(`[unsynced] removed source folder ${src}`);
|
|
2921
3076
|
} catch (err) {
|
|
2922
3077
|
console.error("[unsynced] failed to remove source folder", src, err);
|
|
2923
3078
|
}
|
|
2924
3079
|
return { nest, documents };
|
|
2925
3080
|
}
|
|
2926
|
-
function deleteUnsyncedFolder(folderName) {
|
|
3081
|
+
async function deleteUnsyncedFolder(folderName) {
|
|
2927
3082
|
console.log(`[unsynced] delete requested folder="${folderName}"`);
|
|
2928
3083
|
assertSafeFolderName(folderName);
|
|
2929
3084
|
const src = join3(config.DATA_ROOT, folderName);
|
|
@@ -2939,7 +3094,7 @@ function deleteUnsyncedFolder(folderName) {
|
|
|
2939
3094
|
if (!stat.isDirectory()) {
|
|
2940
3095
|
throw new ValidationError(`Not a directory: ${folderName}`);
|
|
2941
3096
|
}
|
|
2942
|
-
|
|
3097
|
+
await rm(src, { recursive: true, force: true });
|
|
2943
3098
|
console.log(`[unsynced] deleted folder ${src}`);
|
|
2944
3099
|
}
|
|
2945
3100
|
|
|
@@ -3046,42 +3201,50 @@ nestRoutes.post("/:nestId", async (c) => {
|
|
|
3046
3201
|
throw new NotFoundError("Not found");
|
|
3047
3202
|
}
|
|
3048
3203
|
const body = await c.req.json();
|
|
3204
|
+
const files = Array.isArray(body.files) ? body.files : [];
|
|
3205
|
+
const userId = c.get("userId");
|
|
3206
|
+
if (body.nestId) {
|
|
3207
|
+
const target = body.nestId;
|
|
3208
|
+
const nest2 = await getNest(target);
|
|
3209
|
+
if (!nest2 || !nest2.is_imported || await effectivePermission(target, userId) !== "owner") {
|
|
3210
|
+
throw new NotFoundError("Nest not found");
|
|
3211
|
+
}
|
|
3212
|
+
const email2 = await resolveCallerEmail(userId);
|
|
3213
|
+
if (body.finalize) {
|
|
3214
|
+
return c.json({ nest: nest2, documents: await registerImportedDocuments(target, email2) });
|
|
3215
|
+
}
|
|
3216
|
+
const registered = await getDb().get(
|
|
3217
|
+
"SELECT COUNT(*) AS c FROM node_versions WHERE nest_id = ?",
|
|
3218
|
+
[target]
|
|
3219
|
+
);
|
|
3220
|
+
if (Number(registered?.c ?? 0) > 0) {
|
|
3221
|
+
throw new ValidationError(
|
|
3222
|
+
"This nest has already been imported. Start a new import instead of adding files to a finished one."
|
|
3223
|
+
);
|
|
3224
|
+
}
|
|
3225
|
+
console.log(`[import] chunk: nest=${target} files=${files.length}`);
|
|
3226
|
+
return c.json(await stageImportedFiles(target, files, email2));
|
|
3227
|
+
}
|
|
3049
3228
|
if (!body.name) {
|
|
3050
3229
|
throw new ValidationError("name is required");
|
|
3051
3230
|
}
|
|
3052
|
-
const files = Array.isArray(body.files) ? body.files : [];
|
|
3053
|
-
const userId = c.get("userId");
|
|
3054
3231
|
console.log(
|
|
3055
|
-
`[import] request: name=${JSON.stringify(body.name)} files=${files.length}`
|
|
3232
|
+
`[import] request: name=${JSON.stringify(body.name)} files=${files.length}` + (body.partial ? " (first chunk)" : "")
|
|
3056
3233
|
);
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
await stream.writeln(JSON.stringify({ phase: "register", done, total }));
|
|
3069
|
-
}
|
|
3070
|
-
);
|
|
3071
|
-
await stream.writeln(JSON.stringify({ complete: true, nest: nest2, documents: documents2 }));
|
|
3072
|
-
} catch (err) {
|
|
3073
|
-
await stream.writeln(
|
|
3074
|
-
JSON.stringify({ error: err?.message || "Import failed" })
|
|
3075
|
-
);
|
|
3076
|
-
}
|
|
3234
|
+
const email = await resolveCallerEmail(userId);
|
|
3235
|
+
const nest = await importNest(userId, body.name);
|
|
3236
|
+
try {
|
|
3237
|
+
const { rejected } = await stageImportedFiles(nest.id, files, email);
|
|
3238
|
+
if (body.partial) {
|
|
3239
|
+
return c.json({ nest, rejected }, 201);
|
|
3240
|
+
}
|
|
3241
|
+
const documents = await registerImportedDocuments(nest.id, email);
|
|
3242
|
+
return c.json({ nest, documents, rejected }, 201);
|
|
3243
|
+
} catch (err) {
|
|
3244
|
+
await deleteNest(nest.id).catch(() => {
|
|
3077
3245
|
});
|
|
3246
|
+
throw err;
|
|
3078
3247
|
}
|
|
3079
|
-
const nest = await importNest(userId, body.name, files);
|
|
3080
|
-
const documents = await registerImportedDocuments(
|
|
3081
|
-
nest.id,
|
|
3082
|
-
await resolveCallerEmail(userId)
|
|
3083
|
-
);
|
|
3084
|
-
return c.json({ nest, documents }, 201);
|
|
3085
3248
|
});
|
|
3086
3249
|
async function handleUnsyncedList(c) {
|
|
3087
3250
|
const userId = c.get("userId");
|
|
@@ -3121,7 +3284,7 @@ nestRoutes.delete("/:nestId/sync", async (c) => {
|
|
|
3121
3284
|
if (!body?.name) {
|
|
3122
3285
|
throw new ValidationError("name is required");
|
|
3123
3286
|
}
|
|
3124
|
-
deleteUnsyncedFolder(body.name);
|
|
3287
|
+
await deleteUnsyncedFolder(body.name);
|
|
3125
3288
|
return c.json({ ok: true });
|
|
3126
3289
|
});
|
|
3127
3290
|
nestRoutes.get("/:nestId", async (c) => {
|
|
@@ -3835,30 +3998,36 @@ teamRoutes.delete("/:teamId/members/:userId", async (c) => {
|
|
|
3835
3998
|
callerUserId: userId,
|
|
3836
3999
|
allowAdmin: await isLicenseAdminUserId(userId)
|
|
3837
4000
|
});
|
|
3838
|
-
return c.json({ team });
|
|
3839
|
-
});
|
|
4001
|
+
return c.json({ team });
|
|
4002
|
+
});
|
|
4003
|
+
|
|
4004
|
+
// src/nodes/search.ts
|
|
4005
|
+
function matchDocuments(documents, query) {
|
|
4006
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
4007
|
+
if (terms.length === 0) return [];
|
|
4008
|
+
return documents.filter((node) => {
|
|
4009
|
+
const haystack = [
|
|
4010
|
+
node.frontmatter.title,
|
|
4011
|
+
node.body || "",
|
|
4012
|
+
node.frontmatter.type || "",
|
|
4013
|
+
...node.frontmatter.tags || []
|
|
4014
|
+
].join(" ").toLowerCase();
|
|
4015
|
+
return terms.every((term) => haystack.includes(term));
|
|
4016
|
+
});
|
|
4017
|
+
}
|
|
4018
|
+
function toSearchHit(node) {
|
|
4019
|
+
return {
|
|
4020
|
+
id: node.id,
|
|
4021
|
+
title: node.frontmatter.title,
|
|
4022
|
+
type: node.frontmatter.type || "document",
|
|
4023
|
+
tags: node.frontmatter.tags || [],
|
|
4024
|
+
snippet: (node.body || "").slice(0, 200).replace(/\n/g, " ")
|
|
4025
|
+
};
|
|
4026
|
+
}
|
|
3840
4027
|
|
|
3841
4028
|
// src/nodes/routes.ts
|
|
3842
4029
|
import { Hono as Hono8 } from "hono";
|
|
3843
4030
|
|
|
3844
|
-
// src/shared/artifact-sandbox.ts
|
|
3845
|
-
var ARTIFACT_SANDBOX_CSP = [
|
|
3846
|
-
"sandbox allow-scripts allow-popups allow-modals",
|
|
3847
|
-
"default-src 'none'",
|
|
3848
|
-
"script-src 'unsafe-inline'",
|
|
3849
|
-
"style-src 'unsafe-inline'",
|
|
3850
|
-
"img-src data: blob:",
|
|
3851
|
-
"font-src data:",
|
|
3852
|
-
"connect-src 'none'",
|
|
3853
|
-
"base-uri 'none'",
|
|
3854
|
-
"form-action 'none'"
|
|
3855
|
-
].join("; ");
|
|
3856
|
-
function applyArtifactSandboxHeaders(c) {
|
|
3857
|
-
c.header("Content-Security-Policy", ARTIFACT_SANDBOX_CSP);
|
|
3858
|
-
c.header("X-Content-Type-Options", "nosniff");
|
|
3859
|
-
c.header("Referrer-Policy", "no-referrer");
|
|
3860
|
-
}
|
|
3861
|
-
|
|
3862
4031
|
// src/nodes/markdown-export.ts
|
|
3863
4032
|
import { parseDocument as parseDocument3 } from "@promptowl/contextnest-engine";
|
|
3864
4033
|
function nodeToMarkdown(node) {
|
|
@@ -3933,7 +4102,17 @@ function rawNodeBody(c, response, storedRaw) {
|
|
|
3933
4102
|
nodeRoutes.get("/", async (c) => {
|
|
3934
4103
|
const nestId = c.req.param("nestId");
|
|
3935
4104
|
const userId = c.get("userId");
|
|
3936
|
-
const
|
|
4105
|
+
const q = c.req.query();
|
|
4106
|
+
const limit = q.limit ? parseInt(q.limit, 10) : void 0;
|
|
4107
|
+
const nodes = await listNodesForCaller(nestId, userId, {
|
|
4108
|
+
...q.type ? { type: q.type } : {},
|
|
4109
|
+
...q.tag ? { tag: q.tag } : {},
|
|
4110
|
+
...q.status ? { status: q.status } : {},
|
|
4111
|
+
// Lets an owner preview what making the nest public would expose.
|
|
4112
|
+
...q.approved_only === "1" || q.approved_only === "true" ? { approvedOnly: true } : {},
|
|
4113
|
+
// Ignore junk rather than 400 — a bad ?limit should not fail a listing.
|
|
4114
|
+
...limit !== void 0 && Number.isFinite(limit) && limit > 0 ? { limit } : {}
|
|
4115
|
+
});
|
|
3937
4116
|
return c.json({ count: nodes.length, nodes });
|
|
3938
4117
|
});
|
|
3939
4118
|
nodeRoutes.post("/", async (c) => {
|
|
@@ -3974,7 +4153,7 @@ nodeRoutes.post("/", async (c) => {
|
|
|
3974
4153
|
nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
3975
4154
|
const nestId = c.req.param("nestId");
|
|
3976
4155
|
const nodeId = c.req.param("nodeId");
|
|
3977
|
-
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-
|
|
4156
|
+
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-LTREGVSJ.js");
|
|
3978
4157
|
const { stewards, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback2(
|
|
3979
4158
|
nestId,
|
|
3980
4159
|
nodeId
|
|
@@ -3995,7 +4174,7 @@ nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
|
3995
4174
|
nodeRoutes.get("/:nodeId{.+}/reviews", async (c) => {
|
|
3996
4175
|
const nestId = c.req.param("nestId");
|
|
3997
4176
|
const nodeId = c.req.param("nodeId");
|
|
3998
|
-
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-
|
|
4177
|
+
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-V2GR6VM3.js");
|
|
3999
4178
|
const history = await getReviewHistory2(nestId, nodeId);
|
|
4000
4179
|
return c.json({ reviews: history });
|
|
4001
4180
|
});
|
|
@@ -4084,18 +4263,13 @@ nodeRoutes.get("/:nodeId{.+}", async (c) => {
|
|
|
4084
4263
|
const { storage, versions: versionManager } = await engineCache.get(nestId);
|
|
4085
4264
|
const userId = c.get("userId");
|
|
4086
4265
|
const userEmail = await resolveCallerEmail(userId);
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
try {
|
|
4095
|
-
node = await storage.readDocument(nodeId, { verifyChecksum: true });
|
|
4096
|
-
} catch {
|
|
4097
|
-
throw new NotFoundError(`Node not found: ${nodeId}`);
|
|
4098
|
-
}
|
|
4266
|
+
let node = await getNodeForCaller(
|
|
4267
|
+
nestId,
|
|
4268
|
+
{ id: nodeId },
|
|
4269
|
+
userId,
|
|
4270
|
+
userEmail,
|
|
4271
|
+
{ verifyChecksum: true }
|
|
4272
|
+
);
|
|
4099
4273
|
if (node.pendingChange) {
|
|
4100
4274
|
try {
|
|
4101
4275
|
await scanDocumentForDrift(nestId, nodeId, userEmail || "system:read");
|
|
@@ -5612,29 +5786,14 @@ queryRoutes.get("/search", async (c) => {
|
|
|
5612
5786
|
const nestId = c.req.param("nestId");
|
|
5613
5787
|
const { storage } = await engineCache.get(nestId);
|
|
5614
5788
|
const documents = await storage.discoverDocuments();
|
|
5615
|
-
const
|
|
5616
|
-
const matches = documents.filter((node) => {
|
|
5617
|
-
const haystack = [
|
|
5618
|
-
node.frontmatter.title,
|
|
5619
|
-
node.body || "",
|
|
5620
|
-
node.frontmatter.type || "",
|
|
5621
|
-
...node.frontmatter.tags || []
|
|
5622
|
-
].join(" ").toLowerCase();
|
|
5623
|
-
return terms.every((term) => haystack.includes(term));
|
|
5624
|
-
});
|
|
5789
|
+
const matches = matchDocuments(documents, q);
|
|
5625
5790
|
const userId = c.get("userId");
|
|
5626
5791
|
const userEmail = await resolveCallerEmail(userId);
|
|
5627
5792
|
const accessible = await filterAccessible(nestId, userId, userEmail, matches);
|
|
5628
5793
|
return c.json({
|
|
5629
5794
|
query: q,
|
|
5630
5795
|
count: accessible.length,
|
|
5631
|
-
nodes: accessible.map(
|
|
5632
|
-
id: n.id,
|
|
5633
|
-
title: n.frontmatter.title,
|
|
5634
|
-
type: n.frontmatter.type || "document",
|
|
5635
|
-
tags: n.frontmatter.tags || [],
|
|
5636
|
-
snippet: (n.body || "").slice(0, 200).replace(/\n/g, " ")
|
|
5637
|
-
}))
|
|
5796
|
+
nodes: accessible.map(toSearchHit)
|
|
5638
5797
|
});
|
|
5639
5798
|
});
|
|
5640
5799
|
queryRoutes.get("/overview", async (c) => {
|
|
@@ -5793,6 +5952,7 @@ queryRoutes.post("/publish", async (c) => {
|
|
|
5793
5952
|
await syncNodeTags(nestId, id, tags);
|
|
5794
5953
|
created.push(id);
|
|
5795
5954
|
}
|
|
5955
|
+
if (created.length > 0 || body.context_md) await storage.regenerateIndex();
|
|
5796
5956
|
trackEvent("nest.publish", { nestId, count: created.length });
|
|
5797
5957
|
return c.json(
|
|
5798
5958
|
{
|
|
@@ -5809,6 +5969,30 @@ import { Hono as Hono12 } from "hono";
|
|
|
5809
5969
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5810
5970
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
5811
5971
|
|
|
5972
|
+
// src/shared/node-id.ts
|
|
5973
|
+
function assertSafeNodeId(rawId) {
|
|
5974
|
+
let id = rawId;
|
|
5975
|
+
try {
|
|
5976
|
+
id = decodeURIComponent(rawId);
|
|
5977
|
+
} catch {
|
|
5978
|
+
}
|
|
5979
|
+
if (!id || id.length > 512) {
|
|
5980
|
+
throw new ValidationError("invalid node id");
|
|
5981
|
+
}
|
|
5982
|
+
if (id.includes("\0") || id.includes("\\")) {
|
|
5983
|
+
throw new ValidationError("invalid node id");
|
|
5984
|
+
}
|
|
5985
|
+
if (id.startsWith("/")) {
|
|
5986
|
+
throw new ValidationError("invalid node id (absolute path)");
|
|
5987
|
+
}
|
|
5988
|
+
for (const seg of id.split("/")) {
|
|
5989
|
+
if (seg === "" || seg === "." || seg === "..") {
|
|
5990
|
+
throw new ValidationError("invalid node id (path traversal)");
|
|
5991
|
+
}
|
|
5992
|
+
}
|
|
5993
|
+
return id;
|
|
5994
|
+
}
|
|
5995
|
+
|
|
5812
5996
|
// src/telemetry/trace-log.ts
|
|
5813
5997
|
var PRUNE_EVERY = 500;
|
|
5814
5998
|
var insertsSincePrune = 0;
|
|
@@ -5887,30 +6071,6 @@ async function listTraceEvents(filters = {}) {
|
|
|
5887
6071
|
// src/mcp/workflow-tools.ts
|
|
5888
6072
|
import { v4 as uuid8 } from "uuid";
|
|
5889
6073
|
|
|
5890
|
-
// src/shared/node-id.ts
|
|
5891
|
-
function assertSafeNodeId(rawId) {
|
|
5892
|
-
let id = rawId;
|
|
5893
|
-
try {
|
|
5894
|
-
id = decodeURIComponent(rawId);
|
|
5895
|
-
} catch {
|
|
5896
|
-
}
|
|
5897
|
-
if (!id || id.length > 512) {
|
|
5898
|
-
throw new ValidationError("invalid node id");
|
|
5899
|
-
}
|
|
5900
|
-
if (id.includes("\0") || id.includes("\\")) {
|
|
5901
|
-
throw new ValidationError("invalid node id");
|
|
5902
|
-
}
|
|
5903
|
-
if (id.startsWith("/")) {
|
|
5904
|
-
throw new ValidationError("invalid node id (absolute path)");
|
|
5905
|
-
}
|
|
5906
|
-
for (const seg of id.split("/")) {
|
|
5907
|
-
if (seg === "" || seg === "." || seg === "..") {
|
|
5908
|
-
throw new ValidationError("invalid node id (path traversal)");
|
|
5909
|
-
}
|
|
5910
|
-
}
|
|
5911
|
-
return id;
|
|
5912
|
-
}
|
|
5913
|
-
|
|
5914
6074
|
// src/workflow/run-service.ts
|
|
5915
6075
|
import { v4 as uuid6 } from "uuid";
|
|
5916
6076
|
var RUNNABLE_TYPES = ["agent", "skill"];
|
|
@@ -6306,6 +6466,18 @@ async function assertNoFlowCycle(db, nestId, fromNode, toNode) {
|
|
|
6306
6466
|
}
|
|
6307
6467
|
}
|
|
6308
6468
|
}
|
|
6469
|
+
async function assertNoDuplicateEdge(db, nestId, fromNode, toNode, type, excludeId) {
|
|
6470
|
+
const row = await db.get(
|
|
6471
|
+
`SELECT id FROM edges
|
|
6472
|
+
WHERE nest_id = ? AND from_node = ? AND to_node = ? AND type_id = ?` + (excludeId ? " AND id != ?" : ""),
|
|
6473
|
+
excludeId ? [nestId, fromNode, toNode, type.id, excludeId] : [nestId, fromNode, toNode, type.id]
|
|
6474
|
+
);
|
|
6475
|
+
if (row) {
|
|
6476
|
+
throw new ConflictError(
|
|
6477
|
+
`a "${type.name}" connection from "${fromNode}" to "${toNode}" already exists`
|
|
6478
|
+
);
|
|
6479
|
+
}
|
|
6480
|
+
}
|
|
6309
6481
|
var edgeRoutes = new Hono11();
|
|
6310
6482
|
edgeRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
6311
6483
|
const nestId = c.req.param("nestId");
|
|
@@ -6376,6 +6548,7 @@ edgeRoutes.post("/", requireWorkflowPlane, async (c) => {
|
|
|
6376
6548
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6377
6549
|
const id = uuid7();
|
|
6378
6550
|
await db.transaction(async (tx) => {
|
|
6551
|
+
await assertNoDuplicateEdge(tx, nestId, fromNode, toNode, type);
|
|
6379
6552
|
if (type.is_flow) {
|
|
6380
6553
|
await assertNoFlowCycle(tx, nestId, fromNode, toNode);
|
|
6381
6554
|
}
|
|
@@ -6403,6 +6576,8 @@ edgeRoutes.patch("/:id", requireWorkflowPlane, async (c) => {
|
|
|
6403
6576
|
"SELECT * FROM edge_types WHERE id = ?",
|
|
6404
6577
|
[row.type_id]
|
|
6405
6578
|
);
|
|
6579
|
+
const wasFlow = !!type.is_flow;
|
|
6580
|
+
let retype = null;
|
|
6406
6581
|
if (typeof body.type === "string" && body.type.trim()) {
|
|
6407
6582
|
const next = await db.get(
|
|
6408
6583
|
"SELECT * FROM edge_types WHERE nest_id = ? AND (id = ? OR LOWER(name) = LOWER(?))",
|
|
@@ -6413,9 +6588,7 @@ edgeRoutes.patch("/:id", requireWorkflowPlane, async (c) => {
|
|
|
6413
6588
|
`unknown edge type "${body.type}" \u2014 define it in the registry first`
|
|
6414
6589
|
);
|
|
6415
6590
|
}
|
|
6416
|
-
if (next.
|
|
6417
|
-
await assertNoFlowCycle(db, nestId, row.from_node, row.to_node);
|
|
6418
|
-
}
|
|
6591
|
+
if (next.id !== type.id) retype = next;
|
|
6419
6592
|
type = next;
|
|
6420
6593
|
}
|
|
6421
6594
|
let mode = row.condition_mode;
|
|
@@ -6429,11 +6602,19 @@ edgeRoutes.patch("/:id", requireWorkflowPlane, async (c) => {
|
|
|
6429
6602
|
mode = v.mode;
|
|
6430
6603
|
stored = v.stored;
|
|
6431
6604
|
}
|
|
6432
|
-
await db.
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6605
|
+
await db.transaction(async (tx) => {
|
|
6606
|
+
if (retype) {
|
|
6607
|
+
await assertNoDuplicateEdge(tx, nestId, row.from_node, row.to_node, retype, id);
|
|
6608
|
+
if (retype.is_flow && !wasFlow) {
|
|
6609
|
+
await assertNoFlowCycle(tx, nestId, row.from_node, row.to_node);
|
|
6610
|
+
}
|
|
6611
|
+
}
|
|
6612
|
+
await tx.run(
|
|
6613
|
+
`UPDATE edges SET type_id = ?, condition_mode = ?, condition = ?, updated_at = ?
|
|
6614
|
+
WHERE id = ?`,
|
|
6615
|
+
[type.id, mode, stored, (/* @__PURE__ */ new Date()).toISOString(), id]
|
|
6616
|
+
);
|
|
6617
|
+
});
|
|
6437
6618
|
const updated = await db.get("SELECT * FROM edges WHERE id = ?", [id]);
|
|
6438
6619
|
return c.json({ edge: toResponse(updated, type) });
|
|
6439
6620
|
});
|
|
@@ -6768,6 +6949,7 @@ async function createEdge(ctx, args) {
|
|
|
6768
6949
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6769
6950
|
const id = uuid8();
|
|
6770
6951
|
await db.transaction(async (tx) => {
|
|
6952
|
+
await assertNoDuplicateEdge(tx, ctx.nestId, fromNode, toNode, type);
|
|
6771
6953
|
if (type.is_flow) {
|
|
6772
6954
|
await assertNoFlowCycle(tx, ctx.nestId, fromNode, toNode);
|
|
6773
6955
|
}
|
|
@@ -6868,6 +7050,30 @@ function normalizeHops(raw) {
|
|
|
6868
7050
|
if (!Number.isFinite(n)) return 2;
|
|
6869
7051
|
return Math.max(0, Math.min(MAX_HOPS, Math.floor(n)));
|
|
6870
7052
|
}
|
|
7053
|
+
function toolError(text, code) {
|
|
7054
|
+
return { text, isError: true, data: { code, message: text } };
|
|
7055
|
+
}
|
|
7056
|
+
function toCallToolResult(out) {
|
|
7057
|
+
if (typeof out === "string") {
|
|
7058
|
+
return { content: [{ type: "text", text: out }] };
|
|
7059
|
+
}
|
|
7060
|
+
return {
|
|
7061
|
+
content: [{ type: "text", text: out.text }],
|
|
7062
|
+
structuredContent: out.data,
|
|
7063
|
+
...out.isError ? { isError: true } : {}
|
|
7064
|
+
};
|
|
7065
|
+
}
|
|
7066
|
+
function summarizeNode(node, body) {
|
|
7067
|
+
return {
|
|
7068
|
+
id: node.id,
|
|
7069
|
+
title: node.frontmatter.title,
|
|
7070
|
+
description: node.frontmatter.description,
|
|
7071
|
+
type: node.frontmatter.type || "document",
|
|
7072
|
+
status: node.frontmatter.status || "draft",
|
|
7073
|
+
tags: node.frontmatter.tags,
|
|
7074
|
+
body: body ?? void 0
|
|
7075
|
+
};
|
|
7076
|
+
}
|
|
6871
7077
|
var TOOL_DEFINITIONS = [
|
|
6872
7078
|
{
|
|
6873
7079
|
name: "context_init",
|
|
@@ -6936,12 +7142,23 @@ var TOOL_DEFINITIONS = [
|
|
|
6936
7142
|
},
|
|
6937
7143
|
{
|
|
6938
7144
|
name: "context_list",
|
|
6939
|
-
description: "Browse vault contents with optional type, tag, or limit filters.",
|
|
7145
|
+
description: "Browse vault contents with optional type, tag, status, or limit filters.",
|
|
6940
7146
|
inputSchema: {
|
|
6941
7147
|
type: "object",
|
|
6942
7148
|
properties: {
|
|
6943
7149
|
type: { type: "string", description: "Filter by node type" },
|
|
6944
|
-
tag: {
|
|
7150
|
+
tag: {
|
|
7151
|
+
type: "string",
|
|
7152
|
+
description: "Filter by tag (leading # optional, case-insensitive)"
|
|
7153
|
+
},
|
|
7154
|
+
status: {
|
|
7155
|
+
type: "string",
|
|
7156
|
+
description: "Filter by status: draft | pending_review | approved | published | rejected. Aliases normalized. Unlike the CLI and the OSS server, retired nodes stay in this listing whether or not you ask \u2014 here they are unpublished documents their stewards may still act on."
|
|
7157
|
+
},
|
|
7158
|
+
approved_only: {
|
|
7159
|
+
type: "boolean",
|
|
7160
|
+
description: "Return only nodes with an approved version, each served at that version rather than the live file \u2014 the view a public nest gives a stranger."
|
|
7161
|
+
},
|
|
6945
7162
|
limit: { type: "number", description: "Max nodes to return" }
|
|
6946
7163
|
}
|
|
6947
7164
|
}
|
|
@@ -7023,13 +7240,20 @@ var TOOL_DEFINITIONS = [
|
|
|
7023
7240
|
},
|
|
7024
7241
|
{
|
|
7025
7242
|
name: "context_update",
|
|
7026
|
-
description: "Update an existing node \u2014 append,
|
|
7243
|
+
description: "Update an existing node \u2014 replace or append content, set tags, move it through its lifecycle. Whether the edit publishes is decided by this nest's stewardship rules, not by the caller.",
|
|
7027
7244
|
inputSchema: {
|
|
7028
7245
|
type: "object",
|
|
7029
7246
|
properties: {
|
|
7030
7247
|
title: {
|
|
7248
|
+
// Selects the node; it does NOT rename it. Titles are unique here
|
|
7249
|
+
// precisely so MCP callers can address a document this way. Renaming
|
|
7250
|
+
// is not reachable from this tool.
|
|
7031
7251
|
type: "string",
|
|
7032
|
-
description: "Title of node to update"
|
|
7252
|
+
description: "Title of node to update (selects it; does not rename)"
|
|
7253
|
+
},
|
|
7254
|
+
id: {
|
|
7255
|
+
type: "string",
|
|
7256
|
+
description: 'Node id / path (e.g. "nodes/gtm/deals/win-loss-log"). Use instead of title when two nodes share a title or the title is unknown.'
|
|
7033
7257
|
},
|
|
7034
7258
|
content: {
|
|
7035
7259
|
type: "string",
|
|
@@ -7041,9 +7265,19 @@ var TOOL_DEFINITIONS = [
|
|
|
7041
7265
|
items: { type: "string" },
|
|
7042
7266
|
description: "Tags to add"
|
|
7043
7267
|
},
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7268
|
+
status: {
|
|
7269
|
+
type: "string",
|
|
7270
|
+
description: "New lifecycle status: draft | pending_review | approved | published | rejected. Aliases are normalized."
|
|
7271
|
+
},
|
|
7272
|
+
changeNote: {
|
|
7273
|
+
type: "string",
|
|
7274
|
+
description: "Why this changed \u2014 recorded in version history"
|
|
7275
|
+
}
|
|
7276
|
+
}
|
|
7277
|
+
// Neither is required alone — one of them is (the engine catalog's
|
|
7278
|
+
// `.refine(id || title)`, which JSON Schema can't express here).
|
|
7279
|
+
// `title` was required, which made this tool unreachable for `ctx`:
|
|
7280
|
+
// it addresses nodes by id and never sends a title.
|
|
7047
7281
|
}
|
|
7048
7282
|
},
|
|
7049
7283
|
{
|
|
@@ -7110,7 +7344,7 @@ var TOOL_DEFINITIONS = [
|
|
|
7110
7344
|
// repo's governance tables, which the engine doesn't own. Adopt an engine
|
|
7111
7345
|
// descriptor here if the operation is ever promoted upstream.
|
|
7112
7346
|
name: "context_request_deletion",
|
|
7113
|
-
description: "Ask for something to be deleted when you can't delete it yourself. Flags a document, a folder (with everything in it), or the whole nest for a nest owner/admin to action. Nothing is removed by this call \u2014 it opens a request, and the person who resolves it either deletes the target or
|
|
7347
|
+
description: "Ask for something to be deleted when you can't delete it yourself. Flags a document, a folder (with everything in it), or the whole nest for a nest owner/admin to action. Nothing is removed by this call \u2014 it opens a request, and the person who resolves it either deletes the target or rejects it with a note. Use this instead of context_delete when that returns a permission error, or when the document belongs to someone else.",
|
|
7114
7348
|
inputSchema: {
|
|
7115
7349
|
type: "object",
|
|
7116
7350
|
properties: {
|
|
@@ -7143,14 +7377,14 @@ var TOOL_DEFINITIONS = [
|
|
|
7143
7377
|
properties: {
|
|
7144
7378
|
status: {
|
|
7145
7379
|
type: "string",
|
|
7146
|
-
description: "pending (default) or declined \u2014
|
|
7380
|
+
description: '"pending" (default) or "declined" \u2014 past rejections and their notes.'
|
|
7147
7381
|
}
|
|
7148
7382
|
}
|
|
7149
7383
|
}
|
|
7150
7384
|
},
|
|
7151
7385
|
{
|
|
7152
7386
|
name: "context_resolve_deletion",
|
|
7153
|
-
description: "Action a deletion request: delete the flagged document/folder/nest, or
|
|
7387
|
+
description: "Action a deletion request: delete the flagged document/folder/nest, or reject it with a note explaining why it stays. Owner/admin only. Rejecting tells the requester the verdict; deleting is irreversible.",
|
|
7154
7388
|
inputSchema: {
|
|
7155
7389
|
type: "object",
|
|
7156
7390
|
properties: {
|
|
@@ -7160,11 +7394,11 @@ var TOOL_DEFINITIONS = [
|
|
|
7160
7394
|
},
|
|
7161
7395
|
action: {
|
|
7162
7396
|
type: "string",
|
|
7163
|
-
description: '"delete" to honour it, "decline" to keep the target'
|
|
7397
|
+
description: '"delete" to honour it, "decline" to reject it and keep the target'
|
|
7164
7398
|
},
|
|
7165
7399
|
note: {
|
|
7166
7400
|
type: "string",
|
|
7167
|
-
description:
|
|
7401
|
+
description: 'Why you rejected it. Required when action is "decline".'
|
|
7168
7402
|
}
|
|
7169
7403
|
},
|
|
7170
7404
|
required: ["request_id", "action"]
|
|
@@ -7234,9 +7468,13 @@ var TOOL_DEFINITIONS = [
|
|
|
7234
7468
|
inputSchema: {
|
|
7235
7469
|
type: "object",
|
|
7236
7470
|
properties: {
|
|
7237
|
-
title: { type: "string", description: "Title of the node" }
|
|
7238
|
-
|
|
7239
|
-
|
|
7471
|
+
title: { type: "string", description: "Title of the node" },
|
|
7472
|
+
id: {
|
|
7473
|
+
type: "string",
|
|
7474
|
+
description: 'Node id / path (e.g. "nodes/gtm/deals/win-loss-log"). Use instead of title when two nodes share a title or the title is unknown.'
|
|
7475
|
+
}
|
|
7476
|
+
}
|
|
7477
|
+
// One of id or title — see context_update.
|
|
7240
7478
|
}
|
|
7241
7479
|
},
|
|
7242
7480
|
{
|
|
@@ -7347,7 +7585,7 @@ async function resolveTargetNode(storage, args) {
|
|
|
7347
7585
|
if (!args.id && !args.title) return "Provide a title or an id.";
|
|
7348
7586
|
if (args.id) {
|
|
7349
7587
|
try {
|
|
7350
|
-
return await storage.readDocument(args.id);
|
|
7588
|
+
return await storage.readDocument(assertSafeNodeId(args.id));
|
|
7351
7589
|
} catch {
|
|
7352
7590
|
return `Node not found: ${args.id}`;
|
|
7353
7591
|
}
|
|
@@ -7365,36 +7603,47 @@ async function runTool(toolName, args, ctx) {
|
|
|
7365
7603
|
const { storage, queryEngine, versionManager, nestId, userId, userEmail } = ctx;
|
|
7366
7604
|
switch (toolName) {
|
|
7367
7605
|
case "context_init": {
|
|
7368
|
-
const
|
|
7369
|
-
|
|
7606
|
+
const { context_md } = await engineApi.run(
|
|
7607
|
+
"context_init",
|
|
7608
|
+
{},
|
|
7609
|
+
await opContext(nestId, userEmail)
|
|
7610
|
+
);
|
|
7611
|
+
if (!context_md)
|
|
7370
7612
|
return "No CONTEXT.md found. Use context_overview to see what's available.";
|
|
7371
7613
|
return `# Vault Instructions (CONTEXT.md)
|
|
7372
7614
|
|
|
7373
|
-
${
|
|
7615
|
+
${context_md}`;
|
|
7374
7616
|
}
|
|
7375
7617
|
case "context_overview": {
|
|
7376
|
-
const
|
|
7618
|
+
const { total } = await engineApi.run(
|
|
7619
|
+
"context_init",
|
|
7620
|
+
{},
|
|
7621
|
+
await opContext(nestId, userEmail)
|
|
7622
|
+
);
|
|
7623
|
+
const visible = await listNodesForCallerByEmail(nestId, userEmail, {});
|
|
7377
7624
|
const types = {};
|
|
7378
7625
|
const tags = {};
|
|
7379
|
-
for (const n of
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
}
|
|
7384
|
-
const
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
(n, i) => `${i + 1}. **${n.frontmatter.title}** [${n.frontmatter.type || "document"}] ${(n.frontmatter.tags || []).slice(0, 4).join(" ")}
|
|
7388
|
-
${(n.body || "").slice(0, 120).replace(/\n/g, " ")}`
|
|
7626
|
+
for (const n of visible) {
|
|
7627
|
+
types[n.type] = (types[n.type] || 0) + 1;
|
|
7628
|
+
for (const tag of n.tags || []) tags[tag] = (tags[tag] || 0) + 1;
|
|
7629
|
+
}
|
|
7630
|
+
const fmt = (counts) => Object.entries(counts).sort((a, b) => b[1] - a[1]).map(([k, c]) => ` ${k}: ${c}`).join("\n");
|
|
7631
|
+
const nodeList = visible.map(
|
|
7632
|
+
(n, i) => `${i + 1}. **${n.title}** [${n.type}] ${(n.tags || []).slice(0, 4).join(" ")}
|
|
7633
|
+
${(n.content || "").slice(0, 120).replace(/\n/g, " ")}`
|
|
7389
7634
|
).join("\n\n");
|
|
7635
|
+
const withheld = total - visible.length;
|
|
7636
|
+
const note = withheld > 0 ? `
|
|
7637
|
+
_${withheld} node(s) not visible to you._
|
|
7638
|
+
` : "";
|
|
7390
7639
|
return `# Vault Overview
|
|
7391
|
-
**Total nodes:** ${
|
|
7392
|
-
|
|
7640
|
+
**Total nodes:** ${visible.length}
|
|
7641
|
+
${note}
|
|
7393
7642
|
## Types
|
|
7394
|
-
${
|
|
7643
|
+
${fmt(types)}
|
|
7395
7644
|
|
|
7396
7645
|
## Tags
|
|
7397
|
-
${
|
|
7646
|
+
${fmt(tags)}
|
|
7398
7647
|
|
|
7399
7648
|
## All Nodes
|
|
7400
7649
|
|
|
@@ -7416,14 +7665,26 @@ ${nodeList}`;
|
|
|
7416
7665
|
].join(" ").toLowerCase();
|
|
7417
7666
|
return terms.every((t) => hay.includes(t));
|
|
7418
7667
|
});
|
|
7419
|
-
if (!matches.length)
|
|
7668
|
+
if (!matches.length)
|
|
7669
|
+
return {
|
|
7670
|
+
text: `No nodes matched search: "${args.query}"`,
|
|
7671
|
+
data: { results: [] }
|
|
7672
|
+
};
|
|
7420
7673
|
const results = matches.map(
|
|
7421
7674
|
({ node: n, body }, i) => `${i + 1}. **${n.frontmatter.title}** [${n.frontmatter.type || "document"}] ${(n.frontmatter.tags || []).join(" ")}
|
|
7422
7675
|
${(body || "").slice(0, 200).replace(/\n/g, " ")}`
|
|
7423
7676
|
).join("\n\n");
|
|
7424
|
-
return
|
|
7425
|
-
|
|
7426
|
-
|
|
7677
|
+
return {
|
|
7678
|
+
text: `Found ${matches.length} node(s) matching "${args.query}":
|
|
7679
|
+
|
|
7680
|
+
${results}`,
|
|
7681
|
+
// Same gated bodies the prose is built from — `resolveLlmBody`
|
|
7682
|
+
// already refused everything this caller may not read. No `score`:
|
|
7683
|
+
// this is a term-containment filter, not a ranked search.
|
|
7684
|
+
data: {
|
|
7685
|
+
results: matches.map(({ node: n, body }) => summarizeNode(n, body))
|
|
7686
|
+
}
|
|
7687
|
+
};
|
|
7427
7688
|
}
|
|
7428
7689
|
case "context_table_query": {
|
|
7429
7690
|
const result = await queryTableNode(
|
|
@@ -7440,61 +7701,119 @@ ${results}`;
|
|
|
7440
7701
|
${lines.join("\n")}`;
|
|
7441
7702
|
}
|
|
7442
7703
|
case "context_query": {
|
|
7443
|
-
const
|
|
7444
|
-
|
|
7445
|
-
|
|
7704
|
+
const { documents, traversal } = await engineApi.run(
|
|
7705
|
+
"context_query",
|
|
7706
|
+
{ query: args.query, hops: normalizeHops(args.hops) },
|
|
7707
|
+
await opContext(nestId, userEmail)
|
|
7708
|
+
);
|
|
7446
7709
|
const visibility = await Promise.all(
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7710
|
+
documents.map(async (d) => ({
|
|
7711
|
+
doc: d,
|
|
7712
|
+
body: await resolveLlmBody(ctx, {
|
|
7713
|
+
id: d.id,
|
|
7714
|
+
body: d.body ?? "",
|
|
7715
|
+
frontmatter: { title: d.title }
|
|
7716
|
+
})
|
|
7450
7717
|
}))
|
|
7451
7718
|
);
|
|
7452
|
-
const
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
${
|
|
7719
|
+
const visible = visibility.filter((e) => e.body !== null);
|
|
7720
|
+
const nodes = visible.map((e) => e.doc);
|
|
7721
|
+
if (!nodes.length)
|
|
7722
|
+
return {
|
|
7723
|
+
text: `No nodes matched: ${args.query}`,
|
|
7724
|
+
data: { documents: [], traversal }
|
|
7725
|
+
};
|
|
7726
|
+
const list = nodes.map((n, i) => `${i + 1}. **${n.title}** [${n.type}] ${(n.tags || []).join(" ")}`).join("\n\n");
|
|
7727
|
+
return {
|
|
7728
|
+
text: `Found ${nodes.length} node(s) for \`${args.query}\`:
|
|
7729
|
+
|
|
7730
|
+
${list}`,
|
|
7731
|
+
// The engine already hands back catalog `nodeSummary` shapes — only
|
|
7732
|
+
// `body` is swapped for the gated one resolveLlmBody returned.
|
|
7733
|
+
// `source_nodes` is deliberately absent: this handler never gates
|
|
7734
|
+
// them, so serving them would hand out content the prose refuses.
|
|
7735
|
+
data: {
|
|
7736
|
+
documents: visible.map((e) => ({ ...e.doc, body: e.body ?? void 0 })),
|
|
7737
|
+
traversal
|
|
7738
|
+
}
|
|
7739
|
+
};
|
|
7460
7740
|
}
|
|
7461
7741
|
case "context_get": {
|
|
7462
|
-
const docs = await storage.discoverDocuments();
|
|
7463
7742
|
let node;
|
|
7464
|
-
|
|
7465
|
-
node =
|
|
7466
|
-
|
|
7743
|
+
try {
|
|
7744
|
+
node = await getNodeForCaller(
|
|
7745
|
+
nestId,
|
|
7746
|
+
args.title ? { title: args.title } : { id: args.id },
|
|
7747
|
+
ctx.userId,
|
|
7748
|
+
userEmail
|
|
7749
|
+
);
|
|
7750
|
+
} catch {
|
|
7751
|
+
return toolError(
|
|
7752
|
+
`Node not found: ${args.title || args.id}`,
|
|
7753
|
+
"DOCUMENT_NOT_FOUND"
|
|
7467
7754
|
);
|
|
7468
|
-
} else if (args.id) {
|
|
7469
|
-
node = docs.find((n) => n.id === args.id);
|
|
7470
7755
|
}
|
|
7471
|
-
if (!node) return `Node not found: ${args.title || args.id}`;
|
|
7472
7756
|
const body = await resolveLlmBody(ctx, node);
|
|
7473
7757
|
if (body === null) {
|
|
7474
|
-
return
|
|
7758
|
+
return toolError(
|
|
7759
|
+
`Node "${node.frontmatter.title}" has no approved version yet \u2014 not available to AI.`,
|
|
7760
|
+
"UNAUTHORIZED_ACTION"
|
|
7761
|
+
);
|
|
7475
7762
|
}
|
|
7476
7763
|
const meta = [
|
|
7477
7764
|
`**Title:** ${node.frontmatter.title}`,
|
|
7478
7765
|
`**Type:** ${node.frontmatter.type || "document"}`,
|
|
7479
7766
|
node.frontmatter.tags?.length ? `**Tags:** ${node.frontmatter.tags.join(" ")}` : null
|
|
7480
7767
|
].filter(Boolean).join("\n");
|
|
7481
|
-
return
|
|
7768
|
+
return {
|
|
7769
|
+
text: `${meta}
|
|
7482
7770
|
|
|
7483
7771
|
---
|
|
7484
7772
|
|
|
7485
|
-
${body || "(no content)"}
|
|
7773
|
+
${body || "(no content)"}`,
|
|
7774
|
+
// `body` is the gated one, not node.body — under stewardship that is
|
|
7775
|
+
// the APPROVED snapshot, which can be older than the live draft this
|
|
7776
|
+
// frontmatter came from. So the payload carries only the fields the
|
|
7777
|
+
// prose above already reports, none of which claim to describe a
|
|
7778
|
+
// specific version. `version`, `status`, `checksum` and the
|
|
7779
|
+
// timestamps are deliberately absent: they describe the draft, and
|
|
7780
|
+
// pairing them with an approved body would misdescribe it (checksum
|
|
7781
|
+
// would also be a draft hash — see context_versions). `raw` is absent
|
|
7782
|
+
// too: no include_raw input here (Phase 2).
|
|
7783
|
+
data: {
|
|
7784
|
+
id: node.id,
|
|
7785
|
+
frontmatter: {
|
|
7786
|
+
title: node.frontmatter.title,
|
|
7787
|
+
description: node.frontmatter.description,
|
|
7788
|
+
type: node.frontmatter.type || "document",
|
|
7789
|
+
tags: node.frontmatter.tags
|
|
7790
|
+
},
|
|
7791
|
+
body
|
|
7792
|
+
}
|
|
7793
|
+
};
|
|
7486
7794
|
}
|
|
7487
7795
|
case "context_list": {
|
|
7488
7796
|
const nodes = await listNodesForCallerByEmail(nestId, userEmail, {
|
|
7489
7797
|
type: args.type,
|
|
7490
7798
|
tag: args.tag,
|
|
7799
|
+
status: args.status,
|
|
7800
|
+
approvedOnly: args.approved_only === true,
|
|
7491
7801
|
limit: args.limit || 50
|
|
7492
7802
|
});
|
|
7493
|
-
|
|
7803
|
+
const documents = nodes.map((n) => ({
|
|
7804
|
+
id: n.id,
|
|
7805
|
+
title: n.title,
|
|
7806
|
+
description: n.description,
|
|
7807
|
+
type: n.type || "document",
|
|
7808
|
+
status: n.status || "draft",
|
|
7809
|
+
tags: n.tags
|
|
7810
|
+
}));
|
|
7811
|
+
if (!nodes.length)
|
|
7812
|
+
return { text: "No nodes found with the given filters.", data: { documents } };
|
|
7494
7813
|
const list = nodes.map((n, i) => `${i + 1}. **${n.title}** [${n.type}]`).join("\n");
|
|
7495
|
-
return `${nodes.length} node(s):
|
|
7814
|
+
return { text: `${nodes.length} node(s):
|
|
7496
7815
|
|
|
7497
|
-
${list}
|
|
7816
|
+
${list}`, data: { documents } };
|
|
7498
7817
|
}
|
|
7499
7818
|
case "context_resolve": {
|
|
7500
7819
|
const result = await queryEngine.query(args.selector, {
|
|
@@ -7592,7 +7911,10 @@ ${sections}`;
|
|
|
7592
7911
|
}
|
|
7593
7912
|
case "context_create": {
|
|
7594
7913
|
if (!await canCreateInNest(nestId, userEmail)) {
|
|
7595
|
-
return
|
|
7914
|
+
return toolError(
|
|
7915
|
+
"You don't have permission to create documents in this nest.",
|
|
7916
|
+
"UNAUTHORIZED_ACTION"
|
|
7917
|
+
);
|
|
7596
7918
|
}
|
|
7597
7919
|
const wanted = String(args.title ?? "").toLowerCase();
|
|
7598
7920
|
return withNodeWriteLock(nodeWriteKey(nestId, "*create*"), async () => {
|
|
@@ -7601,9 +7923,12 @@ ${sections}`;
|
|
|
7601
7923
|
(n) => String(n.frontmatter.title ?? "").toLowerCase() === wanted
|
|
7602
7924
|
);
|
|
7603
7925
|
if (twin) {
|
|
7604
|
-
return
|
|
7926
|
+
return toolError(
|
|
7927
|
+
`A node titled "${twin.frontmatter.title}" already exists (${twin.id}). Titles must be unique \u2014 MCP tools resolve documents by title, so a second one would be unreachable. Use context_update to change it, context_move to refile it, or create this one under a different title.`,
|
|
7928
|
+
"DOCUMENT_ALREADY_EXISTS"
|
|
7929
|
+
);
|
|
7605
7930
|
}
|
|
7606
|
-
const { node } = await createNode(
|
|
7931
|
+
const { node, version } = await createNode(
|
|
7607
7932
|
nestId,
|
|
7608
7933
|
{
|
|
7609
7934
|
title: args.title,
|
|
@@ -7615,42 +7940,75 @@ ${sections}`;
|
|
|
7615
7940
|
},
|
|
7616
7941
|
userEmail
|
|
7617
7942
|
);
|
|
7618
|
-
return
|
|
7943
|
+
return {
|
|
7944
|
+
text: `Created node: **${args.title}** (${node.id}) \u2014 status: ${node.frontmatter.status}`,
|
|
7945
|
+
// checkpoint is null by construction: this server seals nothing —
|
|
7946
|
+
// a write becomes live through steward review, not a checkpoint.
|
|
7947
|
+
data: {
|
|
7948
|
+
id: node.id,
|
|
7949
|
+
version,
|
|
7950
|
+
status: node.frontmatter.status,
|
|
7951
|
+
checkpoint: null
|
|
7952
|
+
}
|
|
7953
|
+
};
|
|
7619
7954
|
});
|
|
7620
7955
|
}
|
|
7621
7956
|
case "context_update": {
|
|
7622
|
-
const
|
|
7623
|
-
|
|
7624
|
-
|
|
7625
|
-
);
|
|
7626
|
-
if (!node) return `Node not found: ${args.title}`;
|
|
7957
|
+
const node = await resolveTargetNode(storage, args);
|
|
7958
|
+
if (typeof node === "string") return toolError(node, "DOCUMENT_NOT_FOUND");
|
|
7959
|
+
const label = args.id ? node.frontmatter.title : args.title;
|
|
7627
7960
|
const editCheck = await canUserEdit(nestId, node.id, userEmail);
|
|
7628
7961
|
if (!editCheck.allowed) {
|
|
7629
|
-
return
|
|
7962
|
+
return toolError(
|
|
7963
|
+
`You don't have permission to edit "${label}": ${editCheck.reason}`,
|
|
7964
|
+
"UNAUTHORIZED_ACTION"
|
|
7965
|
+
);
|
|
7630
7966
|
}
|
|
7631
|
-
const
|
|
7967
|
+
const mergedTags = args.tags ? [.../* @__PURE__ */ new Set([...node.frontmatter.tags || [], ...args.tags])] : void 0;
|
|
7968
|
+
const { node: updated, version } = await updateNode(
|
|
7632
7969
|
nestId,
|
|
7633
7970
|
node.id,
|
|
7634
7971
|
{
|
|
7635
7972
|
content: args.content,
|
|
7636
7973
|
append: args.append,
|
|
7637
|
-
tags:
|
|
7974
|
+
tags: mergedTags,
|
|
7975
|
+
status: args.status,
|
|
7976
|
+
changeNote: args.changeNote
|
|
7638
7977
|
},
|
|
7639
7978
|
userEmail
|
|
7640
7979
|
);
|
|
7641
|
-
return
|
|
7980
|
+
return {
|
|
7981
|
+
text: `Updated node: **${updated.frontmatter.title}** \u2014 v${version}, status: ${updated.frontmatter.status}`,
|
|
7982
|
+
// checkpoint null — see context_create.
|
|
7983
|
+
data: {
|
|
7984
|
+
id: updated.id,
|
|
7985
|
+
version,
|
|
7986
|
+
status: updated.frontmatter.status,
|
|
7987
|
+
checkpoint: null
|
|
7988
|
+
}
|
|
7989
|
+
};
|
|
7642
7990
|
}
|
|
7643
7991
|
case "context_delete": {
|
|
7644
7992
|
const node = await resolveTargetNode(storage, args);
|
|
7645
|
-
if (typeof node === "string") return node;
|
|
7993
|
+
if (typeof node === "string") return toolError(node, "DOCUMENT_NOT_FOUND");
|
|
7646
7994
|
const deleteCheck = await canUserEdit(nestId, node.id, userEmail);
|
|
7647
7995
|
if (!deleteCheck.allowed) {
|
|
7648
|
-
return
|
|
7996
|
+
return toolError(
|
|
7997
|
+
`You don't have permission to delete "${node.frontmatter.title}": ${deleteCheck.reason}
|
|
7649
7998
|
|
|
7650
|
-
Use context_request_deletion to ask a nest owner/admin to delete it, with a reason
|
|
7999
|
+
Use context_request_deletion to ask a nest owner/admin to delete it, with a reason.`,
|
|
8000
|
+
"UNAUTHORIZED_ACTION"
|
|
8001
|
+
);
|
|
7651
8002
|
}
|
|
7652
8003
|
await deleteNode(nestId, node.id, userEmail);
|
|
7653
|
-
return
|
|
8004
|
+
return {
|
|
8005
|
+
text: `Deleted node: **${node.frontmatter.title}** (${node.id})`,
|
|
8006
|
+
data: {
|
|
8007
|
+
id: node.id,
|
|
8008
|
+
title: node.frontmatter.title,
|
|
8009
|
+
deleted: true
|
|
8010
|
+
}
|
|
8011
|
+
};
|
|
7654
8012
|
}
|
|
7655
8013
|
case "context_move": {
|
|
7656
8014
|
if (typeof args.folder !== "string") {
|
|
@@ -7726,7 +8084,7 @@ ${sections}`;
|
|
|
7726
8084
|
reason,
|
|
7727
8085
|
baseUrl: ctx.baseUrl
|
|
7728
8086
|
});
|
|
7729
|
-
return `Requested deletion of the **entire nest**. A nest owner or admin will delete it or
|
|
8087
|
+
return `Requested deletion of the **entire nest**. A nest owner or admin will delete it or reject the request.
|
|
7730
8088
|
Request id: \`${request2.id}\``;
|
|
7731
8089
|
}
|
|
7732
8090
|
if (typeof args.folder === "string" && args.folder.trim()) {
|
|
@@ -7744,7 +8102,7 @@ Request id: \`${request2.id}\``;
|
|
|
7744
8102
|
reason,
|
|
7745
8103
|
baseUrl: ctx.baseUrl
|
|
7746
8104
|
});
|
|
7747
|
-
return `Requested deletion of folder **${folder}** (${inside.length} document(s) inside). A nest owner or admin will delete it or
|
|
8105
|
+
return `Requested deletion of folder **${folder}** (${inside.length} document(s) inside). A nest owner or admin will delete it or reject the request.
|
|
7748
8106
|
Request id: \`${request2.id}\``;
|
|
7749
8107
|
}
|
|
7750
8108
|
const node = await resolveTargetNode(storage, args);
|
|
@@ -7760,19 +8118,19 @@ Request id: \`${request2.id}\``;
|
|
|
7760
8118
|
reason,
|
|
7761
8119
|
baseUrl: ctx.baseUrl
|
|
7762
8120
|
});
|
|
7763
|
-
return `Requested deletion of **${node.frontmatter.title}**. A nest owner or admin will delete it or
|
|
8121
|
+
return `Requested deletion of **${node.frontmatter.title}**. A nest owner or admin will delete it or reject the request \u2014 nothing is removed yet.
|
|
7764
8122
|
Request id: \`${request.id}\``;
|
|
7765
8123
|
}
|
|
7766
8124
|
case "context_deletion_queue": {
|
|
7767
8125
|
const status = args.status === "declined" ? "declined" : "pending";
|
|
7768
8126
|
const requests = await listDeletionRequests({ nestId, status });
|
|
7769
8127
|
if (requests.length === 0) {
|
|
7770
|
-
return status === "pending" ? "No deletion requests waiting. All caught up!" : "No
|
|
8128
|
+
return status === "pending" ? "No deletion requests waiting. All caught up!" : "No rejected deletion requests.";
|
|
7771
8129
|
}
|
|
7772
8130
|
const list = requests.map((r, i) => {
|
|
7773
8131
|
const kind = r.targetType === "document" ? "" : `${r.targetType}: `;
|
|
7774
8132
|
const verdict = r.status === "declined" ? `
|
|
7775
|
-
|
|
8133
|
+
Rejected by ${r.resolvedBy}: "${r.resolutionNote}"` : "";
|
|
7776
8134
|
return `${i + 1}. **${kind}${r.title || r.nodeId}**
|
|
7777
8135
|
Asked by ${r.requestedBy}: "${r.reason}"
|
|
7778
8136
|
Request id: \`${r.id}\`${verdict}`;
|
|
@@ -7800,7 +8158,7 @@ ${list}`;
|
|
|
7800
8158
|
if (action === "decline") {
|
|
7801
8159
|
const note = typeof args.note === "string" ? args.note.trim() : "";
|
|
7802
8160
|
if (!note) {
|
|
7803
|
-
return "note is required when
|
|
8161
|
+
return "note is required when rejecting \u2014 the requester is told why it stays.";
|
|
7804
8162
|
}
|
|
7805
8163
|
await declineDeletion({
|
|
7806
8164
|
nestId,
|
|
@@ -7809,7 +8167,7 @@ ${list}`;
|
|
|
7809
8167
|
note,
|
|
7810
8168
|
baseUrl: ctx.baseUrl
|
|
7811
8169
|
});
|
|
7812
|
-
return `
|
|
8170
|
+
return `Rejected the deletion request for **${label}**. It stays, and ${request.requestedBy} has been told why.`;
|
|
7813
8171
|
}
|
|
7814
8172
|
if (request.targetType === "nest") {
|
|
7815
8173
|
await deleteNest(nestId);
|
|
@@ -7880,13 +8238,17 @@ Submit one with \`context_submit_review\`.`;
|
|
|
7880
8238
|
const node = docs.find(
|
|
7881
8239
|
(n) => n.frontmatter.title.toLowerCase() === args.title.toLowerCase()
|
|
7882
8240
|
);
|
|
7883
|
-
if (!node) return `Node not found: ${args.title}
|
|
8241
|
+
if (!node) return toolError(`Node not found: ${args.title}`, "DOCUMENT_NOT_FOUND");
|
|
7884
8242
|
const submitCheck = await canUserEdit(ctx.nestId, node.id, userEmail);
|
|
7885
8243
|
if (!submitCheck.allowed) {
|
|
7886
|
-
return
|
|
8244
|
+
return toolError(
|
|
8245
|
+
`You don't have permission to submit "${args.title}" for review: ${submitCheck.reason}`,
|
|
8246
|
+
"UNAUTHORIZED_ACTION"
|
|
8247
|
+
);
|
|
7887
8248
|
}
|
|
7888
8249
|
const currentVersion = await getCurrentVersion(ctx.nestId, node.id);
|
|
7889
|
-
if (currentVersion === 0)
|
|
8250
|
+
if (currentVersion === 0)
|
|
8251
|
+
return toolError(`No versions found for "${args.title}"`, "VERSION_NOT_FOUND");
|
|
7890
8252
|
try {
|
|
7891
8253
|
const request = await submitForReview({
|
|
7892
8254
|
nestId: ctx.nestId,
|
|
@@ -7905,9 +8267,33 @@ Submit one with \`context_submit_review\`.`;
|
|
|
7905
8267
|
|
|
7906
8268
|
**Stewards who can review:**
|
|
7907
8269
|
${resolved.map((r) => `- ${r.steward.userEmail} (${r.source})`).join("\n")}` : "";
|
|
7908
|
-
return
|
|
8270
|
+
return {
|
|
8271
|
+
text: `Submitted "${args.title}" v${currentVersion} for review (${request.priority} priority).${stewardList}`,
|
|
8272
|
+
// No catalog shape to conform to — `context_submit_review` is a
|
|
8273
|
+
// governance-namespace operation and that namespace is declared but
|
|
8274
|
+
// unimplemented in the engine (`api/index.ts`, NAMESPACES). So this
|
|
8275
|
+
// is the review state the handler already computed, and nothing
|
|
8276
|
+
// more: no `version`/`checkpoint` at the top level, because this
|
|
8277
|
+
// operation writes no new version and seals no checkpoint — it
|
|
8278
|
+
// points a review request at the version that already existed.
|
|
8279
|
+
// Built from the same gated values the prose above is built from
|
|
8280
|
+
// (`request`, `resolved`), never re-read past the permission check.
|
|
8281
|
+
data: {
|
|
8282
|
+
id: node.id,
|
|
8283
|
+
submitted: true,
|
|
8284
|
+
review: {
|
|
8285
|
+
id: request.id,
|
|
8286
|
+
version: request.version,
|
|
8287
|
+
status: request.status,
|
|
8288
|
+
priority: request.priority,
|
|
8289
|
+
requested_by: request.requestedBy,
|
|
8290
|
+
requested_at: request.requestedAt
|
|
8291
|
+
},
|
|
8292
|
+
reviewers: resolved.map((r) => r.steward.userEmail)
|
|
8293
|
+
}
|
|
8294
|
+
};
|
|
7909
8295
|
} catch (err) {
|
|
7910
|
-
return `Failed to submit: ${err.message}
|
|
8296
|
+
return toolError(`Failed to submit: ${err.message}`, "INTERNAL");
|
|
7911
8297
|
}
|
|
7912
8298
|
}
|
|
7913
8299
|
case "context_approve": {
|
|
@@ -7951,23 +8337,46 @@ Reason: ${args.note}`;
|
|
|
7951
8337
|
}
|
|
7952
8338
|
}
|
|
7953
8339
|
case "context_versions": {
|
|
7954
|
-
const
|
|
7955
|
-
|
|
7956
|
-
|
|
7957
|
-
);
|
|
7958
|
-
if (!node) return `Node not found: ${args.title}`;
|
|
8340
|
+
const node = await resolveTargetNode(storage, args);
|
|
8341
|
+
if (typeof node === "string") return toolError(node, "DOCUMENT_NOT_FOUND");
|
|
8342
|
+
const label = args.id ? node.frontmatter.title : args.title;
|
|
7959
8343
|
const allVersions = await getVersions(ctx.nestId, node.id);
|
|
7960
8344
|
const approved = await getApprovedVersion(ctx.nestId, node.id);
|
|
7961
8345
|
if (allVersions.length === 0) {
|
|
7962
|
-
return
|
|
8346
|
+
return {
|
|
8347
|
+
text: `No version history for "${label}".`,
|
|
8348
|
+
data: { id: node.id, approved_version: approved ?? null, versions: [] }
|
|
8349
|
+
};
|
|
7963
8350
|
}
|
|
7964
8351
|
const list = allVersions.map(
|
|
7965
8352
|
(v) => `- **v${v.version}** ${v.status}${v.version === approved ? " (AI-active)" : ""} \u2014 by ${v.editedBy} at ${v.editedAt}${v.changeNote ? ` "${v.changeNote}"` : ""}`
|
|
7966
8353
|
).join("\n");
|
|
7967
|
-
return
|
|
8354
|
+
return {
|
|
8355
|
+
text: `# Version History: ${label}
|
|
7968
8356
|
**Current:** v${allVersions[0]?.version || 0} | **AI-active:** ${approved ? `v${approved}` : "none"}
|
|
7969
8357
|
|
|
7970
|
-
${list}
|
|
8358
|
+
${list}`,
|
|
8359
|
+
// Five catalog fields have no equivalent here and are omitted rather
|
|
8360
|
+
// than faked: `keyframe_interval` and `keyframe` (content lives on
|
|
8361
|
+
// disk — there is no keyframe+diff model), `chain_hash` (integrity is
|
|
8362
|
+
// server-side, not a per-version chain), `content_hash` (the column
|
|
8363
|
+
// exists but is conflict-detection state this tool has never served,
|
|
8364
|
+
// and it is ungated), and `published_at` (a version carries a
|
|
8365
|
+
// lifecycle status, not a publish timestamp). `status` and
|
|
8366
|
+
// `approved_version` are the community equivalent — the same two
|
|
8367
|
+
// facts the prose above reports.
|
|
8368
|
+
data: {
|
|
8369
|
+
id: node.id,
|
|
8370
|
+
approved_version: approved ?? null,
|
|
8371
|
+
versions: allVersions.map((v) => ({
|
|
8372
|
+
version: v.version,
|
|
8373
|
+
status: v.status,
|
|
8374
|
+
edited_by: v.editedBy,
|
|
8375
|
+
edited_at: v.editedAt,
|
|
8376
|
+
note: v.changeNote
|
|
8377
|
+
}))
|
|
8378
|
+
}
|
|
8379
|
+
};
|
|
7971
8380
|
}
|
|
7972
8381
|
case "context_assign_steward": {
|
|
7973
8382
|
const scope = args.scope;
|
|
@@ -8025,8 +8434,8 @@ ${list}`;
|
|
|
8025
8434
|
if (!target) return "target is required (a node id or folder prefix).";
|
|
8026
8435
|
if (!["read", "write"].includes(role)) return "role must be read or write.";
|
|
8027
8436
|
try {
|
|
8028
|
-
const { createGrant: createGrant2 } = await import("./grants-service-
|
|
8029
|
-
const db = (await import("./client-
|
|
8437
|
+
const { createGrant: createGrant2 } = await import("./grants-service-I535QVTW.js");
|
|
8438
|
+
const db = (await import("./client-OBS5HOTA.js")).getDb();
|
|
8030
8439
|
const { normalizeEmail: normalizeEmail2 } = await import("./email-R7DFS6E5.js");
|
|
8031
8440
|
const e = normalizeEmail2(String(args.email || ""));
|
|
8032
8441
|
if (!e) return "email is required.";
|
|
@@ -8119,7 +8528,7 @@ function createMcpServerForNest(nestId, userId, userEmail, baseUrl) {
|
|
|
8119
8528
|
}
|
|
8120
8529
|
server.tool(tool.name, tool.description, shape, async (args) => {
|
|
8121
8530
|
const engine = await engineCache.get(nestId);
|
|
8122
|
-
const
|
|
8531
|
+
const out = await handleToolCall(tool.name, args, {
|
|
8123
8532
|
storage: engine.storage,
|
|
8124
8533
|
queryEngine: engine.query,
|
|
8125
8534
|
versionManager: engine.versions,
|
|
@@ -8128,7 +8537,7 @@ function createMcpServerForNest(nestId, userId, userEmail, baseUrl) {
|
|
|
8128
8537
|
userEmail,
|
|
8129
8538
|
baseUrl
|
|
8130
8539
|
});
|
|
8131
|
-
return
|
|
8540
|
+
return toCallToolResult(out);
|
|
8132
8541
|
});
|
|
8133
8542
|
}
|
|
8134
8543
|
return server;
|
|
@@ -9487,7 +9896,7 @@ function createServerMcp(userId, userEmail, nestScope, baseUrl) {
|
|
|
9487
9896
|
shape[key] = field;
|
|
9488
9897
|
}
|
|
9489
9898
|
server.tool(tool.name, tool.description, shape, async (args) => {
|
|
9490
|
-
const
|
|
9899
|
+
const out = await handleServerToolCall(
|
|
9491
9900
|
tool.name,
|
|
9492
9901
|
args,
|
|
9493
9902
|
userId,
|
|
@@ -9495,7 +9904,7 @@ function createServerMcp(userId, userEmail, nestScope, baseUrl) {
|
|
|
9495
9904
|
nestScope,
|
|
9496
9905
|
baseUrl
|
|
9497
9906
|
);
|
|
9498
|
-
return
|
|
9907
|
+
return toCallToolResult(out);
|
|
9499
9908
|
});
|
|
9500
9909
|
}
|
|
9501
9910
|
return server;
|
|
@@ -10424,7 +10833,14 @@ governanceNodeRoutes.get("/:nodeId{.+}/versions", async (c) => {
|
|
|
10424
10833
|
return c.json({
|
|
10425
10834
|
versions: enriched,
|
|
10426
10835
|
approvedVersion: approved,
|
|
10427
|
-
currentVersion: allVersions[0]?.version || 0
|
|
10836
|
+
currentVersion: allVersions[0]?.version || 0,
|
|
10837
|
+
// Verdicts on out-of-band edits. A rejection commits no content, so it has
|
|
10838
|
+
// no version row to hang off — it lives in the engine's chain-event log and
|
|
10839
|
+
// rides alongside the version list rather than inside it.
|
|
10840
|
+
externalEditVerdicts: await listExternalEditVerdicts(
|
|
10841
|
+
nestId,
|
|
10842
|
+
nodeId
|
|
10843
|
+
)
|
|
10428
10844
|
});
|
|
10429
10845
|
});
|
|
10430
10846
|
governanceNodeRoutes.get("/:nodeId{.+}/versions/:version{[0-9]+}", async (c) => {
|
|
@@ -10606,7 +11022,9 @@ governanceNodeRoutes.post(
|
|
|
10606
11022
|
return c.json({
|
|
10607
11023
|
approved: true,
|
|
10608
11024
|
version: result.versionEntry.version,
|
|
10609
|
-
chainEvent: result.chainEvent.event_id
|
|
11025
|
+
chainEvent: result.chainEvent.event_id,
|
|
11026
|
+
// As on reject: false means the verdict never reached the audit log.
|
|
11027
|
+
historyRecorded: result.historyRecorded
|
|
10610
11028
|
});
|
|
10611
11029
|
} catch (err) {
|
|
10612
11030
|
console.error(
|
|
@@ -10641,7 +11059,10 @@ governanceNodeRoutes.post(
|
|
|
10641
11059
|
});
|
|
10642
11060
|
return c.json({
|
|
10643
11061
|
rejected: true,
|
|
10644
|
-
chainEvent: result.chainEvent.event_id
|
|
11062
|
+
chainEvent: result.chainEvent.event_id,
|
|
11063
|
+
// False means the edit was rejected but the audit entry did not land —
|
|
11064
|
+
// the caller is not shown a clean success (server log carries why).
|
|
11065
|
+
historyRecorded: result.historyRecorded
|
|
10645
11066
|
});
|
|
10646
11067
|
} catch (err) {
|
|
10647
11068
|
return c.json({ error: err.message }, 400);
|
|
@@ -10886,6 +11307,13 @@ function createApp() {
|
|
|
10886
11307
|
"*",
|
|
10887
11308
|
corsOrigins === "*" ? cors({ origin: "*" }) : cors({ origin: corsOrigins, credentials: true })
|
|
10888
11309
|
);
|
|
11310
|
+
const frameAncestors = `frame-ancestors ${config.FRAME_ANCESTORS}`;
|
|
11311
|
+
app.use("*", async (c, next) => {
|
|
11312
|
+
await next();
|
|
11313
|
+
const existing = c.res.headers.get("Content-Security-Policy");
|
|
11314
|
+
const merged = mergeFrameAncestors(existing, frameAncestors);
|
|
11315
|
+
if (merged !== existing) c.header("Content-Security-Policy", merged);
|
|
11316
|
+
});
|
|
10889
11317
|
const ASSET_UPLOAD_PATH = /^\/nests\/[^/]+\/assets\/?$/;
|
|
10890
11318
|
app.use("*", async (c, next) => {
|
|
10891
11319
|
const lenStr = c.req.header("Content-Length");
|
|
@@ -11666,6 +12094,38 @@ function createApp() {
|
|
|
11666
12094
|
users: usersRow.c
|
|
11667
12095
|
});
|
|
11668
12096
|
});
|
|
12097
|
+
app.use("/search", flexAuthMiddleware);
|
|
12098
|
+
app.get("/search", async (c) => {
|
|
12099
|
+
const q = (c.req.query("q") || "").trim();
|
|
12100
|
+
if (!q) throw new ValidationError("q query parameter is required");
|
|
12101
|
+
const limit = Math.min(Math.max(Number(c.req.query("limit")) || 20, 1), 50);
|
|
12102
|
+
const userId = c.get("userId");
|
|
12103
|
+
const userEmail = await resolveCallerEmail(userId);
|
|
12104
|
+
const perNest = await Promise.all(
|
|
12105
|
+
(await listVisibleNests(userId)).map(async (nest) => {
|
|
12106
|
+
try {
|
|
12107
|
+
const { storage } = await engineCache.get(nest.id);
|
|
12108
|
+
const matches = matchDocuments(await storage.discoverDocuments(), q);
|
|
12109
|
+
if (matches.length === 0) return [];
|
|
12110
|
+
const accessible = await filterAccessible(nest.id, userId, userEmail, matches);
|
|
12111
|
+
return accessible.map((n) => ({
|
|
12112
|
+
...toSearchHit(n),
|
|
12113
|
+
nest_id: nest.id,
|
|
12114
|
+
nest_name: nest.name ?? nest.id
|
|
12115
|
+
}));
|
|
12116
|
+
} catch (e) {
|
|
12117
|
+
console.error("[search] skipped nest", nest.id, e);
|
|
12118
|
+
return [];
|
|
12119
|
+
}
|
|
12120
|
+
})
|
|
12121
|
+
);
|
|
12122
|
+
const hits = perNest.flat();
|
|
12123
|
+
return c.json({
|
|
12124
|
+
query: q,
|
|
12125
|
+
count: hits.length,
|
|
12126
|
+
nodes: hits.slice(0, limit)
|
|
12127
|
+
});
|
|
12128
|
+
});
|
|
11669
12129
|
app.use("/runs/*", flexAuthMiddleware);
|
|
11670
12130
|
app.route("/runs", runDetailRoutes);
|
|
11671
12131
|
app.route("/hooks", hookFireRoutes);
|
|
@@ -11877,6 +12337,7 @@ function createApp() {
|
|
|
11877
12337
|
nestsApp.route("/:nestId/assets", assetRoutes);
|
|
11878
12338
|
nestsApp.route("/:nestId/mcp", mcpRoutes);
|
|
11879
12339
|
app.route("/nests", nestsApp);
|
|
12340
|
+
app.all("/.well-known/*", (c) => c.json({ error: "Not found" }, 404));
|
|
11880
12341
|
app.use(
|
|
11881
12342
|
"/assets/*",
|
|
11882
12343
|
serveStatic({
|
|
@@ -11884,14 +12345,20 @@ function createApp() {
|
|
|
11884
12345
|
onFound: (_p, c) => c.header("Cache-Control", "public, max-age=31536000, immutable")
|
|
11885
12346
|
})
|
|
11886
12347
|
);
|
|
12348
|
+
const isDocumentNavigation = (c) => c.req.header("Sec-Fetch-Mode") === "navigate" || (c.req.header("Accept") || "").includes("text/html");
|
|
11887
12349
|
app.get(
|
|
11888
12350
|
"*",
|
|
12351
|
+
async (c, next) => {
|
|
12352
|
+
if (isDocumentNavigation(c)) return next();
|
|
12353
|
+
return c.json({ error: "Not found" }, 404);
|
|
12354
|
+
},
|
|
11889
12355
|
serveStatic({
|
|
11890
12356
|
root: UI_DIR_REL,
|
|
11891
12357
|
path: "index.html",
|
|
11892
12358
|
onFound: (_p, c) => c.header("Cache-Control", "no-cache")
|
|
11893
12359
|
})
|
|
11894
12360
|
);
|
|
12361
|
+
app.notFound((c) => c.json({ error: "Not found" }, 404));
|
|
11895
12362
|
app.onError((err, c) => {
|
|
11896
12363
|
if (err instanceof AppError) {
|
|
11897
12364
|
return c.json({ error: err.message }, err.statusCode);
|
|
@@ -12327,6 +12794,7 @@ async function main() {
|
|
|
12327
12794
|
POST /nests/:id/query Selector query
|
|
12328
12795
|
POST /nests/:id/context One-call context retrieval
|
|
12329
12796
|
GET /nests/:id/search?q= Full-text search
|
|
12797
|
+
GET /search?q= Search every nest you can see
|
|
12330
12798
|
POST /nests/:id/publish Bulk publish
|
|
12331
12799
|
POST /nests/:id/mcp MCP endpoint
|
|
12332
12800
|
GET /health Health check
|