@promptowl/contextnest-community 1.17.0 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONFIGURATION.md +1 -1
- package/README.md +6 -2
- package/dist/{chunk-YF4OFT2V.js → chunk-3GMGLYTZ.js} +36 -5
- package/dist/{chunk-UXJSO54R.js → chunk-BXUVIAMP.js} +59 -14
- package/dist/{chunk-SNFGU5Q7.js → chunk-ENDHMQPD.js} +602 -327
- package/dist/chunk-F66VCBLA.js +136 -0
- package/dist/{chunk-4YGJSUJC.js → chunk-FBDEESNJ.js} +30 -22
- package/dist/{chunk-HWDPCPYT.js → chunk-GSYMJ3A4.js} +2 -2
- package/dist/{chunk-JMJLNEXE.js → chunk-HYSTFMFG.js} +43 -2
- package/dist/chunk-TNAZJYL5.js +379 -0
- package/dist/{chunk-SLTQACJW.js → chunk-YB3LKF7U.js} +3 -1
- package/dist/{chunk-GUNJTORH.js → chunk-YVMSM7LS.js} +14 -1
- package/dist/{client-OBS5HOTA.js → client-SW5I6ZBK.js} +2 -2
- package/dist/engine-V4DY4PYN.js +14 -0
- package/dist/external-edit-service-3NK5DGLA.js +33 -0
- package/dist/{grants-service-4NXPBYWD.js → grants-service-CADI6LIB.js} +4 -4
- package/dist/index.js +580 -513
- package/dist/{keys-73STFJJB.js → keys-CTOGAG3W.js} +5 -1
- package/dist/{migrations.postgres-CYKO5CFV.js → migrations.postgres-5VI4RDTS.js} +41 -4
- package/dist/{review-service-3CCKDO5N.js → review-service-LK4M2C43.js} +8 -8
- package/dist/{stewardship-service-OHTTZFND.js → stewardship-service-JAFSU5GN.js} +9 -5
- package/dist/{version-service-EFZGGL3Y.js → version-service-G7SFFKFO.js} +7 -5
- package/dist/web3/assets/index-C26nM5Kn.css +1 -0
- package/dist/web3/assets/index-C4oalc2K.js +1065 -0
- package/dist/web3/index.html +2 -2
- package/package.json +3 -3
- package/dist/chunk-XRK6SQSC.js +0 -58
- package/dist/web3/assets/index-D1299AIf.css +0 -1
- package/dist/web3/assets/index-W6tEA3RK.js +0 -1046
package/dist/index.js
CHANGED
|
@@ -41,7 +41,27 @@ import {
|
|
|
41
41
|
submitForReview,
|
|
42
42
|
withNodeWriteLock,
|
|
43
43
|
withdrawDeletionRequest
|
|
44
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-FBDEESNJ.js";
|
|
45
|
+
import {
|
|
46
|
+
canCreateInNest,
|
|
47
|
+
canManageStewards,
|
|
48
|
+
canUserAccess,
|
|
49
|
+
canUserApprove,
|
|
50
|
+
canUserEdit,
|
|
51
|
+
createStewardRecord,
|
|
52
|
+
getCollaboratorRole,
|
|
53
|
+
getStewardRolesForUser,
|
|
54
|
+
getStewardsForNest,
|
|
55
|
+
getStewardsForUser,
|
|
56
|
+
listStewards,
|
|
57
|
+
removeSteward,
|
|
58
|
+
resolveGrantRoles,
|
|
59
|
+
resolveStewardsForNode,
|
|
60
|
+
resolveStewardsWithFallback,
|
|
61
|
+
resolveUserRoles,
|
|
62
|
+
syncFromConfig,
|
|
63
|
+
updateSteward
|
|
64
|
+
} from "./chunk-3GMGLYTZ.js";
|
|
45
65
|
import {
|
|
46
66
|
createGrant,
|
|
47
67
|
deleteGrant,
|
|
@@ -50,15 +70,16 @@ import {
|
|
|
50
70
|
listGrants,
|
|
51
71
|
listUserGrants,
|
|
52
72
|
resolveNodeGrant
|
|
53
|
-
} from "./chunk-
|
|
73
|
+
} from "./chunk-GSYMJ3A4.js";
|
|
54
74
|
import {
|
|
55
75
|
generateApiKey,
|
|
56
76
|
getKeyPrefix,
|
|
57
77
|
hashApiKey,
|
|
58
78
|
hashPassword,
|
|
59
79
|
parseBearerToken,
|
|
80
|
+
verifyKeyRequest,
|
|
60
81
|
verifyPassword
|
|
61
|
-
} from "./chunk-
|
|
82
|
+
} from "./chunk-F66VCBLA.js";
|
|
62
83
|
import {
|
|
63
84
|
checkConflict,
|
|
64
85
|
createVersion,
|
|
@@ -66,36 +87,32 @@ import {
|
|
|
66
87
|
getApprovedVersions,
|
|
67
88
|
getCurrentVersion,
|
|
68
89
|
getDisplayStatus,
|
|
90
|
+
getDisplayStatuses,
|
|
69
91
|
getTrackedNodeIds,
|
|
70
92
|
getVersions,
|
|
71
93
|
listMyDrafts,
|
|
72
94
|
setApprovedVersion,
|
|
73
95
|
upsertVersion
|
|
74
|
-
} from "./chunk-
|
|
96
|
+
} from "./chunk-BXUVIAMP.js";
|
|
75
97
|
import {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
removeSteward,
|
|
88
|
-
resolveGrantRoles,
|
|
89
|
-
resolveStewardsForNode,
|
|
90
|
-
resolveStewardsWithFallback,
|
|
91
|
-
resolveUserRoles,
|
|
92
|
-
syncFromConfig,
|
|
93
|
-
updateSteward
|
|
94
|
-
} from "./chunk-YF4OFT2V.js";
|
|
98
|
+
approveExternalEdit,
|
|
99
|
+
getExternalEditDetail,
|
|
100
|
+
getPendingChange,
|
|
101
|
+
listExternalEditVerdicts,
|
|
102
|
+
listNestExternalEdits,
|
|
103
|
+
loadLatestApprovedNode,
|
|
104
|
+
rejectExternalEdit,
|
|
105
|
+
scanDocumentForDrift,
|
|
106
|
+
scanNestForDrift,
|
|
107
|
+
startDriftScanner
|
|
108
|
+
} from "./chunk-TNAZJYL5.js";
|
|
95
109
|
import {
|
|
96
110
|
addMember,
|
|
111
|
+
assertSafeNodeId,
|
|
97
112
|
buildTitleMap,
|
|
98
113
|
canApproveWith,
|
|
114
|
+
canEditWith,
|
|
115
|
+
collabPermToRole,
|
|
99
116
|
createNest,
|
|
100
117
|
createTeam,
|
|
101
118
|
deleteNest,
|
|
@@ -104,14 +121,17 @@ import {
|
|
|
104
121
|
docLink,
|
|
105
122
|
engineApi,
|
|
106
123
|
engineCache,
|
|
124
|
+
ensureNodeIndex,
|
|
107
125
|
findTeamByExternalId,
|
|
108
126
|
folderForNode,
|
|
127
|
+
folderOfId,
|
|
109
128
|
getAccessConfig,
|
|
110
129
|
getCurrentLicense,
|
|
111
130
|
getNest,
|
|
112
131
|
getSuspensionReason,
|
|
113
132
|
getTeam,
|
|
114
133
|
importNest,
|
|
134
|
+
indexedFolderCounts,
|
|
115
135
|
insertOrIgnore,
|
|
116
136
|
installLicenseKey,
|
|
117
137
|
isConfigSuperAdmin,
|
|
@@ -136,11 +156,15 @@ import {
|
|
|
136
156
|
nestPrimeTags,
|
|
137
157
|
nestReviewsPrimeOnly,
|
|
138
158
|
nestStorageRoot,
|
|
159
|
+
nodesWithSuggestions,
|
|
139
160
|
normalizeTag,
|
|
140
161
|
nowExpr,
|
|
141
162
|
opContext,
|
|
142
163
|
permissionLevel,
|
|
143
164
|
persistSetting,
|
|
165
|
+
primaryRole,
|
|
166
|
+
readIndexedNodes,
|
|
167
|
+
rebuildNodeIndex,
|
|
144
168
|
removeMember,
|
|
145
169
|
removeNodeFromTagIndex,
|
|
146
170
|
renameNest,
|
|
@@ -150,6 +174,8 @@ import {
|
|
|
150
174
|
resolveNestAccess,
|
|
151
175
|
resolveNestPath,
|
|
152
176
|
resolveNestPermission,
|
|
177
|
+
resolveNodeUrl,
|
|
178
|
+
resolveTeamRolesForUser,
|
|
153
179
|
sendEmailToRecipient,
|
|
154
180
|
setAllowSelfApprove,
|
|
155
181
|
setPrimeOnlyReview,
|
|
@@ -165,7 +191,7 @@ import {
|
|
|
165
191
|
updateMemberRole,
|
|
166
192
|
validateLicense,
|
|
167
193
|
verifySmtp
|
|
168
|
-
} from "./chunk-
|
|
194
|
+
} from "./chunk-ENDHMQPD.js";
|
|
169
195
|
import {
|
|
170
196
|
AppError,
|
|
171
197
|
ConflictError,
|
|
@@ -173,19 +199,22 @@ import {
|
|
|
173
199
|
LockedError,
|
|
174
200
|
NotFoundError,
|
|
175
201
|
UnauthorizedError,
|
|
176
|
-
ValidationError
|
|
177
|
-
|
|
202
|
+
ValidationError,
|
|
203
|
+
VaultBusyError,
|
|
204
|
+
isVaultLockTimeout
|
|
205
|
+
} from "./chunk-YVMSM7LS.js";
|
|
178
206
|
import {
|
|
179
207
|
config,
|
|
180
208
|
getDb,
|
|
181
209
|
initDb,
|
|
182
210
|
isEmailListish,
|
|
183
211
|
isEmailish
|
|
184
|
-
} from "./chunk-
|
|
212
|
+
} from "./chunk-HYSTFMFG.js";
|
|
185
213
|
import {
|
|
186
214
|
ANON_EMAIL,
|
|
187
|
-
ANON_USER_ID
|
|
188
|
-
|
|
215
|
+
ANON_USER_ID,
|
|
216
|
+
MAX_FOLDER_DEPTH
|
|
217
|
+
} from "./chunk-YB3LKF7U.js";
|
|
189
218
|
import {
|
|
190
219
|
normalizeEmail
|
|
191
220
|
} from "./chunk-FRQJWGN3.js";
|
|
@@ -1079,19 +1108,11 @@ authRoutes.post("/keys", authMiddleware, async (c) => {
|
|
|
1079
1108
|
);
|
|
1080
1109
|
const db = getDb();
|
|
1081
1110
|
const userId = c.get("userId");
|
|
1082
|
-
const
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
);
|
|
1086
|
-
if (
|
|
1087
|
-
return c.json(
|
|
1088
|
-
{
|
|
1089
|
-
error: "An API key already exists for this user. Rotate it instead.",
|
|
1090
|
-
key_prefix: existing.key_prefix
|
|
1091
|
-
},
|
|
1092
|
-
409
|
|
1093
|
-
);
|
|
1094
|
-
}
|
|
1111
|
+
const check = await verifyKeyRequest(db, userId, {
|
|
1112
|
+
action: "generate",
|
|
1113
|
+
label: body.label
|
|
1114
|
+
});
|
|
1115
|
+
if (!check.ok) return c.json({ error: check.error }, check.status);
|
|
1095
1116
|
const apiKey = generateApiKey();
|
|
1096
1117
|
const keyId = uuid2();
|
|
1097
1118
|
await db.run(
|
|
@@ -1102,11 +1123,11 @@ authRoutes.post("/keys", authMiddleware, async (c) => {
|
|
|
1102
1123
|
hashApiKey(apiKey),
|
|
1103
1124
|
getKeyPrefix(apiKey),
|
|
1104
1125
|
body.nest_id || null,
|
|
1105
|
-
|
|
1126
|
+
check.label
|
|
1106
1127
|
]
|
|
1107
1128
|
);
|
|
1108
1129
|
return c.json(
|
|
1109
|
-
{ api_key: apiKey, key_prefix: getKeyPrefix(apiKey) },
|
|
1130
|
+
{ id: keyId, api_key: apiKey, key_prefix: getKeyPrefix(apiKey), label: check.label },
|
|
1110
1131
|
201
|
|
1111
1132
|
);
|
|
1112
1133
|
});
|
|
@@ -1114,15 +1135,22 @@ authRoutes.post("/keys/rotate", authMiddleware, async (c) => {
|
|
|
1114
1135
|
const body = await c.req.json().catch(() => ({}));
|
|
1115
1136
|
const db = getDb();
|
|
1116
1137
|
const userId = c.get("userId");
|
|
1138
|
+
const check = await verifyKeyRequest(db, userId, {
|
|
1139
|
+
action: "update",
|
|
1140
|
+
label: body.label,
|
|
1141
|
+
keyId: body.key_id
|
|
1142
|
+
});
|
|
1143
|
+
if (!check.ok) return c.json({ error: check.error }, check.status);
|
|
1144
|
+
const prior = check.key;
|
|
1117
1145
|
const apiKey = generateApiKey();
|
|
1118
1146
|
const keyId = uuid2();
|
|
1119
|
-
const
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1147
|
+
const nestScope = "nest_id" in body ? body.nest_id || null : prior.nest_id;
|
|
1148
|
+
const replaced = await db.transaction(async (tx) => {
|
|
1149
|
+
const removed = await tx.run(
|
|
1150
|
+
"DELETE FROM api_keys WHERE id = ? AND user_id = ?",
|
|
1151
|
+
[prior.id, userId]
|
|
1152
|
+
);
|
|
1153
|
+
if (removed.changes === 0) return false;
|
|
1126
1154
|
await tx.run(
|
|
1127
1155
|
"INSERT INTO api_keys (id, user_id, key_hash, key_prefix, nest_id, label) VALUES (?, ?, ?, ?, ?, ?)",
|
|
1128
1156
|
[
|
|
@@ -1131,29 +1159,47 @@ authRoutes.post("/keys/rotate", authMiddleware, async (c) => {
|
|
|
1131
1159
|
hashApiKey(apiKey),
|
|
1132
1160
|
getKeyPrefix(apiKey),
|
|
1133
1161
|
nestScope,
|
|
1134
|
-
|
|
1162
|
+
check.label ?? prior.label
|
|
1135
1163
|
]
|
|
1136
1164
|
);
|
|
1165
|
+
return true;
|
|
1166
|
+
});
|
|
1167
|
+
if (!replaced) {
|
|
1168
|
+
return c.json(
|
|
1169
|
+
{ error: "That key no longer exists \u2014 reload and try again." },
|
|
1170
|
+
404
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
return c.json({
|
|
1174
|
+
id: keyId,
|
|
1175
|
+
api_key: apiKey,
|
|
1176
|
+
key_prefix: getKeyPrefix(apiKey),
|
|
1177
|
+
label: check.label ?? prior.label
|
|
1137
1178
|
});
|
|
1138
|
-
return c.json({ api_key: apiKey, key_prefix: getKeyPrefix(apiKey) });
|
|
1139
1179
|
});
|
|
1140
1180
|
authRoutes.get("/keys", authMiddleware, async (c) => {
|
|
1141
1181
|
const db = getDb();
|
|
1182
|
+
const userId = c.get("userId");
|
|
1183
|
+
const check = await verifyKeyRequest(db, userId, { action: "fetch" });
|
|
1184
|
+
if (!check.ok) return c.json({ error: check.error }, check.status);
|
|
1142
1185
|
const keys = await db.all(
|
|
1143
|
-
"SELECT id, key_prefix, nest_id, label, created_at, last_used_at FROM api_keys WHERE user_id = ?",
|
|
1144
|
-
[
|
|
1186
|
+
"SELECT id, key_prefix, nest_id, label, created_at, last_used_at FROM api_keys WHERE user_id = ? ORDER BY created_at ASC, id ASC",
|
|
1187
|
+
[userId]
|
|
1145
1188
|
);
|
|
1146
1189
|
return c.json({ keys });
|
|
1147
1190
|
});
|
|
1148
1191
|
authRoutes.delete("/keys/:keyId", authMiddleware, async (c) => {
|
|
1149
1192
|
const db = getDb();
|
|
1150
|
-
const
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1193
|
+
const userId = c.get("userId");
|
|
1194
|
+
const check = await verifyKeyRequest(db, userId, {
|
|
1195
|
+
action: "update",
|
|
1196
|
+
keyId: c.req.param("keyId")
|
|
1197
|
+
});
|
|
1198
|
+
if (!check.ok) return c.json({ error: check.error }, check.status);
|
|
1199
|
+
await db.run("DELETE FROM api_keys WHERE id = ? AND user_id = ?", [
|
|
1200
|
+
check.key.id,
|
|
1201
|
+
userId
|
|
1202
|
+
]);
|
|
1157
1203
|
return c.json({ deleted: true });
|
|
1158
1204
|
});
|
|
1159
1205
|
authRoutes.post("/device", async (c) => {
|
|
@@ -1610,19 +1656,18 @@ import { Hono as Hono3 } from "hono";
|
|
|
1610
1656
|
|
|
1611
1657
|
// src/nodes/service.ts
|
|
1612
1658
|
import {
|
|
1613
|
-
parseDocument
|
|
1659
|
+
parseDocument,
|
|
1614
1660
|
normalizeStatus,
|
|
1615
1661
|
isRejected,
|
|
1616
|
-
|
|
1662
|
+
filterDocuments,
|
|
1663
|
+
DocumentNotFoundError,
|
|
1664
|
+
NODE_TYPES
|
|
1617
1665
|
} from "@promptowl/contextnest-engine";
|
|
1618
1666
|
import { rename, mkdir } from "fs/promises";
|
|
1619
|
-
import { dirname, join
|
|
1667
|
+
import { dirname, join } from "path";
|
|
1620
1668
|
|
|
1621
1669
|
// src/governance/safe-publish.ts
|
|
1622
|
-
import {
|
|
1623
|
-
publishDocument,
|
|
1624
|
-
serializeDocument
|
|
1625
|
-
} from "@promptowl/contextnest-engine";
|
|
1670
|
+
import { serializeDocument } from "@promptowl/contextnest-engine";
|
|
1626
1671
|
function serializeNodeSafe(node) {
|
|
1627
1672
|
return serializeDocument({
|
|
1628
1673
|
...node,
|
|
@@ -1644,6 +1689,9 @@ function stripUndefinedDeep(value) {
|
|
|
1644
1689
|
return value;
|
|
1645
1690
|
}
|
|
1646
1691
|
|
|
1692
|
+
// src/nodes/service.ts
|
|
1693
|
+
import { inputJsonSchema } from "@promptowl/contextnest-engine/api";
|
|
1694
|
+
|
|
1647
1695
|
// src/governance/prime-service.ts
|
|
1648
1696
|
async function isPrimeDocument(nestId, tags, metadata) {
|
|
1649
1697
|
const flag = documentPrimeFlag(metadata);
|
|
@@ -1661,313 +1709,6 @@ function documentPrimeFlag(metadata) {
|
|
|
1661
1709
|
return void 0;
|
|
1662
1710
|
}
|
|
1663
1711
|
|
|
1664
|
-
// src/governance/external-edit-service.ts
|
|
1665
|
-
import { readFile } from "fs/promises";
|
|
1666
|
-
import { join } from "path";
|
|
1667
|
-
import {
|
|
1668
|
-
detectDrift,
|
|
1669
|
-
stageSuggestion,
|
|
1670
|
-
ChainEventLog,
|
|
1671
|
-
approveSuggestion,
|
|
1672
|
-
rejectSuggestion,
|
|
1673
|
-
listSuggestions,
|
|
1674
|
-
readSuggestion,
|
|
1675
|
-
parseDocument,
|
|
1676
|
-
VersionManager,
|
|
1677
|
-
computeContentHash,
|
|
1678
|
-
getChecksumContent
|
|
1679
|
-
} from "@promptowl/contextnest-engine";
|
|
1680
|
-
var communityRbac = {
|
|
1681
|
-
isCzar: () => false,
|
|
1682
|
-
canIngest: () => true,
|
|
1683
|
-
isDocOwner: () => true
|
|
1684
|
-
};
|
|
1685
|
-
function docPath(nestId, documentId) {
|
|
1686
|
-
return join(resolveNestPath(nestId), `${documentId}.md`);
|
|
1687
|
-
}
|
|
1688
|
-
async function readRaw(nestId, documentId) {
|
|
1689
|
-
try {
|
|
1690
|
-
return await readFile(docPath(nestId, documentId), "utf-8");
|
|
1691
|
-
} catch {
|
|
1692
|
-
return null;
|
|
1693
|
-
}
|
|
1694
|
-
}
|
|
1695
|
-
async function loadChainHead(storage, documentId) {
|
|
1696
|
-
const history = await storage.readHistory(documentId);
|
|
1697
|
-
if (!history || history.versions.length === 0) return null;
|
|
1698
|
-
const latest = history.versions[history.versions.length - 1];
|
|
1699
|
-
try {
|
|
1700
|
-
const content = await new VersionManager(storage).reconstructVersion(
|
|
1701
|
-
documentId,
|
|
1702
|
-
latest.version
|
|
1703
|
-
);
|
|
1704
|
-
return { version: latest.version, content };
|
|
1705
|
-
} catch {
|
|
1706
|
-
return null;
|
|
1707
|
-
}
|
|
1708
|
-
}
|
|
1709
|
-
async function loadLatestApprovedNode(nestId, documentId) {
|
|
1710
|
-
const { storage } = await engineCache.get(nestId);
|
|
1711
|
-
const head = await loadChainHead(storage, documentId);
|
|
1712
|
-
if (!head) return null;
|
|
1713
|
-
return parseDocument(docPath(nestId, documentId), head.content, documentId);
|
|
1714
|
-
}
|
|
1715
|
-
async function bodyMatchesLatestVersion(storage, documentId, liveBodyHash) {
|
|
1716
|
-
const head = await loadChainHead(storage, documentId);
|
|
1717
|
-
if (!head) return false;
|
|
1718
|
-
const headBodyHash = computeContentHash(getChecksumContent(head.content));
|
|
1719
|
-
return headBodyHash === liveBodyHash;
|
|
1720
|
-
}
|
|
1721
|
-
async function scanDocumentForDrift(nestId, documentId, actor = "system:scanner") {
|
|
1722
|
-
const res = await scanDocumentForDriftInternal(nestId, documentId, actor);
|
|
1723
|
-
return res?.meta ?? null;
|
|
1724
|
-
}
|
|
1725
|
-
async function scanDocumentForDriftInternal(nestId, documentId, actor) {
|
|
1726
|
-
const { storage } = await engineCache.get(nestId);
|
|
1727
|
-
const node = await storage.readDocument(documentId).catch(() => null);
|
|
1728
|
-
if (!node) return null;
|
|
1729
|
-
const raw = await readRaw(nestId, documentId);
|
|
1730
|
-
if (raw == null) return null;
|
|
1731
|
-
const drift = detectDrift(raw, node.frontmatter.checksum);
|
|
1732
|
-
if (!drift.drifted) return null;
|
|
1733
|
-
if (await bodyMatchesLatestVersion(storage, documentId, drift.actualHash)) {
|
|
1734
|
-
return null;
|
|
1735
|
-
}
|
|
1736
|
-
const approved = await loadChainHead(storage, documentId);
|
|
1737
|
-
if (!approved) return null;
|
|
1738
|
-
const existing = await listSuggestions(storage, documentId);
|
|
1739
|
-
const dup = existing.find((s) => s.proposed_hash === drift.actualHash);
|
|
1740
|
-
if (dup) return { meta: dup, created: false };
|
|
1741
|
-
const result = await stageSuggestion({
|
|
1742
|
-
storage,
|
|
1743
|
-
documentId,
|
|
1744
|
-
approvedRawContent: approved.content,
|
|
1745
|
-
proposedRawContent: raw,
|
|
1746
|
-
source: "out-of-band-edit",
|
|
1747
|
-
actor,
|
|
1748
|
-
docTier: "standard"
|
|
1749
|
-
});
|
|
1750
|
-
return { meta: result.meta, created: true };
|
|
1751
|
-
}
|
|
1752
|
-
async function scanNestForDrift(nestId, actor = "system:scanner") {
|
|
1753
|
-
const { storage, invalidateDiscovery } = await engineCache.get(nestId);
|
|
1754
|
-
invalidateDiscovery();
|
|
1755
|
-
const docs = await storage.discoverDocuments();
|
|
1756
|
-
const results = await Promise.all(
|
|
1757
|
-
docs.map((doc) => scanDocumentForDriftInternal(nestId, doc.id, actor))
|
|
1758
|
-
);
|
|
1759
|
-
const staged = results.filter((r) => r?.created).length;
|
|
1760
|
-
return { scanned: docs.length, staged };
|
|
1761
|
-
}
|
|
1762
|
-
async function getPendingChange(nestId, documentId) {
|
|
1763
|
-
const { storage } = await engineCache.get(nestId);
|
|
1764
|
-
const list = await listSuggestions(storage, documentId);
|
|
1765
|
-
for (let i = list.length - 1; i >= 0; i--) {
|
|
1766
|
-
const meta = list[i];
|
|
1767
|
-
if (await bodyMatchesLatestVersion(storage, documentId, meta.proposed_hash)) {
|
|
1768
|
-
continue;
|
|
1769
|
-
}
|
|
1770
|
-
return {
|
|
1771
|
-
suggestion_id: meta.suggestion_id,
|
|
1772
|
-
detected_at: meta.detected_at,
|
|
1773
|
-
source: meta.source,
|
|
1774
|
-
proposed_hash: meta.proposed_hash
|
|
1775
|
-
};
|
|
1776
|
-
}
|
|
1777
|
-
return null;
|
|
1778
|
-
}
|
|
1779
|
-
async function listNestExternalEdits(nestId) {
|
|
1780
|
-
const { storage } = await engineCache.get(nestId);
|
|
1781
|
-
const docs = await storage.discoverDocuments();
|
|
1782
|
-
const lists = await Promise.all(
|
|
1783
|
-
docs.map(async (doc) => {
|
|
1784
|
-
const metas = await listSuggestions(storage, doc.id);
|
|
1785
|
-
const fresh = [];
|
|
1786
|
-
for (const meta of metas) {
|
|
1787
|
-
if (await bodyMatchesLatestVersion(storage, doc.id, meta.proposed_hash)) {
|
|
1788
|
-
continue;
|
|
1789
|
-
}
|
|
1790
|
-
fresh.push(meta);
|
|
1791
|
-
}
|
|
1792
|
-
return fresh;
|
|
1793
|
-
})
|
|
1794
|
-
);
|
|
1795
|
-
const entries = lists.flat().map((meta) => ({
|
|
1796
|
-
suggestion_id: meta.suggestion_id,
|
|
1797
|
-
nest_id: nestId,
|
|
1798
|
-
document_id: meta.document_id,
|
|
1799
|
-
source: meta.source,
|
|
1800
|
-
detected_at: meta.detected_at,
|
|
1801
|
-
actor: meta.actor,
|
|
1802
|
-
target_hash: meta.target_hash,
|
|
1803
|
-
proposed_hash: meta.proposed_hash,
|
|
1804
|
-
note: meta.note
|
|
1805
|
-
}));
|
|
1806
|
-
return entries.sort((a, b) => b.detected_at.localeCompare(a.detected_at));
|
|
1807
|
-
}
|
|
1808
|
-
async function getExternalEditDetail(nestId, documentId, suggestionId) {
|
|
1809
|
-
const { storage } = await engineCache.get(nestId);
|
|
1810
|
-
const found = await readSuggestion(storage, documentId, suggestionId);
|
|
1811
|
-
if (!found) return null;
|
|
1812
|
-
return {
|
|
1813
|
-
suggestion_id: found.meta.suggestion_id,
|
|
1814
|
-
nest_id: nestId,
|
|
1815
|
-
document_id: found.meta.document_id,
|
|
1816
|
-
source: found.meta.source,
|
|
1817
|
-
detected_at: found.meta.detected_at,
|
|
1818
|
-
actor: found.meta.actor,
|
|
1819
|
-
target_hash: found.meta.target_hash,
|
|
1820
|
-
proposed_hash: found.meta.proposed_hash,
|
|
1821
|
-
note: found.meta.note,
|
|
1822
|
-
patch: found.patch
|
|
1823
|
-
};
|
|
1824
|
-
}
|
|
1825
|
-
async function recordVerdict(nestId, event) {
|
|
1826
|
-
try {
|
|
1827
|
-
const { storage } = await engineCache.get(nestId);
|
|
1828
|
-
await new ChainEventLog(storage).append(event);
|
|
1829
|
-
return true;
|
|
1830
|
-
} catch (err) {
|
|
1831
|
-
console.error(
|
|
1832
|
-
`[external-edit] failed to record verdict ${event.event_id} for nest ${nestId}:`,
|
|
1833
|
-
err
|
|
1834
|
-
);
|
|
1835
|
-
return false;
|
|
1836
|
-
}
|
|
1837
|
-
}
|
|
1838
|
-
async function listExternalEditVerdicts(nestId, documentId) {
|
|
1839
|
-
try {
|
|
1840
|
-
const { storage } = await engineCache.get(nestId);
|
|
1841
|
-
const events = await new ChainEventLog(storage).readByDocument(documentId);
|
|
1842
|
-
return events.flatMap((e) => {
|
|
1843
|
-
const meta = e.action_metadata || {};
|
|
1844
|
-
if (typeof meta.suggestion_id !== "string") return [];
|
|
1845
|
-
const reason = meta.rejection_reason;
|
|
1846
|
-
const comment = meta.approval_comment;
|
|
1847
|
-
return [
|
|
1848
|
-
{
|
|
1849
|
-
suggestion_id: meta.suggestion_id,
|
|
1850
|
-
status: typeof reason === "string" ? "rejected" : "approved",
|
|
1851
|
-
actor: e.actor,
|
|
1852
|
-
at: e.timestamp,
|
|
1853
|
-
note: typeof reason === "string" ? reason : typeof comment === "string" ? comment : void 0,
|
|
1854
|
-
source: typeof meta.source === "string" ? meta.source : void 0
|
|
1855
|
-
}
|
|
1856
|
-
];
|
|
1857
|
-
});
|
|
1858
|
-
} catch (err) {
|
|
1859
|
-
console.error(
|
|
1860
|
-
`[external-edit] failed to read verdicts for ${nestId}/${documentId}:`,
|
|
1861
|
-
err
|
|
1862
|
-
);
|
|
1863
|
-
return [];
|
|
1864
|
-
}
|
|
1865
|
-
}
|
|
1866
|
-
async function mirrorVersion(input) {
|
|
1867
|
-
const { storage } = await engineCache.get(input.nestId);
|
|
1868
|
-
const node = await storage.readDocument(input.documentId);
|
|
1869
|
-
const { upsertVersion: upsertVersion2, setApprovedVersion: setApprovedVersion2 } = await import("./version-service-EFZGGL3Y.js");
|
|
1870
|
-
await upsertVersion2({
|
|
1871
|
-
nestId: input.nestId,
|
|
1872
|
-
nodeId: input.documentId,
|
|
1873
|
-
version: input.version,
|
|
1874
|
-
content: node.body || "",
|
|
1875
|
-
author: input.actor,
|
|
1876
|
-
status: "published",
|
|
1877
|
-
tags: node.frontmatter.tags || [],
|
|
1878
|
-
changeNote: input.note
|
|
1879
|
-
});
|
|
1880
|
-
await setApprovedVersion2(
|
|
1881
|
-
input.nestId,
|
|
1882
|
-
input.documentId,
|
|
1883
|
-
input.version,
|
|
1884
|
-
input.actor
|
|
1885
|
-
);
|
|
1886
|
-
}
|
|
1887
|
-
async function approveExternalEdit(input) {
|
|
1888
|
-
const { storage } = await engineCache.get(input.nestId);
|
|
1889
|
-
let result;
|
|
1890
|
-
try {
|
|
1891
|
-
result = await approveSuggestion({
|
|
1892
|
-
storage,
|
|
1893
|
-
rbac: communityRbac,
|
|
1894
|
-
documentId: input.documentId,
|
|
1895
|
-
suggestionId: input.suggestionId,
|
|
1896
|
-
actor: input.actor,
|
|
1897
|
-
zone: "default",
|
|
1898
|
-
comment: input.comment
|
|
1899
|
-
});
|
|
1900
|
-
} catch (err) {
|
|
1901
|
-
console.error(
|
|
1902
|
-
`[external-edit] approveSuggestion failed for ${input.nestId}/${input.documentId} suggestion=${input.suggestionId}:`,
|
|
1903
|
-
err
|
|
1904
|
-
);
|
|
1905
|
-
throw err;
|
|
1906
|
-
}
|
|
1907
|
-
try {
|
|
1908
|
-
await mirrorVersion({
|
|
1909
|
-
nestId: input.nestId,
|
|
1910
|
-
documentId: input.documentId,
|
|
1911
|
-
version: result.versionEntry.version,
|
|
1912
|
-
actor: input.actor,
|
|
1913
|
-
note: input.comment ? `External edit approved: ${input.comment}` : "External edit approved"
|
|
1914
|
-
});
|
|
1915
|
-
} catch (err) {
|
|
1916
|
-
console.error(
|
|
1917
|
-
`[external-edit] failed to mirror approved version into node_versions for ${input.nestId}/${input.documentId}:`,
|
|
1918
|
-
err
|
|
1919
|
-
);
|
|
1920
|
-
}
|
|
1921
|
-
const historyRecorded = await recordVerdict(input.nestId, result.chainEvent);
|
|
1922
|
-
return { ...result, historyRecorded };
|
|
1923
|
-
}
|
|
1924
|
-
async function rejectExternalEdit(input) {
|
|
1925
|
-
const { storage } = await engineCache.get(input.nestId);
|
|
1926
|
-
const result = await rejectSuggestion({
|
|
1927
|
-
storage,
|
|
1928
|
-
rbac: communityRbac,
|
|
1929
|
-
documentId: input.documentId,
|
|
1930
|
-
suggestionId: input.suggestionId,
|
|
1931
|
-
actor: input.actor,
|
|
1932
|
-
zone: "default",
|
|
1933
|
-
reason: input.reason
|
|
1934
|
-
});
|
|
1935
|
-
const historyRecorded = await recordVerdict(input.nestId, result.chainEvent);
|
|
1936
|
-
try {
|
|
1937
|
-
const approved = await loadChainHead(storage, input.documentId);
|
|
1938
|
-
if (approved) {
|
|
1939
|
-
await storage.writeDocument(input.documentId, approved.content);
|
|
1940
|
-
}
|
|
1941
|
-
} catch (err) {
|
|
1942
|
-
console.error(
|
|
1943
|
-
`[external-edit] revert-on-reject failed for ${input.nestId}/${input.documentId}:`,
|
|
1944
|
-
err
|
|
1945
|
-
);
|
|
1946
|
-
}
|
|
1947
|
-
return { ...result, historyRecorded };
|
|
1948
|
-
}
|
|
1949
|
-
var scannerTimer = null;
|
|
1950
|
-
async function scanAllNests() {
|
|
1951
|
-
const db = getDb();
|
|
1952
|
-
const rows = await db.all("SELECT id FROM nests");
|
|
1953
|
-
await Promise.all(
|
|
1954
|
-
rows.map(
|
|
1955
|
-
({ id }) => scanNestForDrift(id).catch(
|
|
1956
|
-
(err) => console.error(`[external-edit] scan failed for nest ${id}:`, err)
|
|
1957
|
-
)
|
|
1958
|
-
)
|
|
1959
|
-
);
|
|
1960
|
-
}
|
|
1961
|
-
function startDriftScanner(intervalMs = 3e4) {
|
|
1962
|
-
if (scannerTimer) return;
|
|
1963
|
-
scannerTimer = setInterval(() => {
|
|
1964
|
-
scanAllNests().catch(
|
|
1965
|
-
(err) => console.error("[external-edit] scanner tick failed:", err)
|
|
1966
|
-
);
|
|
1967
|
-
}, intervalMs);
|
|
1968
|
-
scannerTimer.unref?.();
|
|
1969
|
-
}
|
|
1970
|
-
|
|
1971
1712
|
// src/nodes/service.ts
|
|
1972
1713
|
async function userIdFromEmail(email) {
|
|
1973
1714
|
const db = getDb();
|
|
@@ -1986,17 +1727,42 @@ function assertTitleLength(title) {
|
|
|
1986
1727
|
);
|
|
1987
1728
|
}
|
|
1988
1729
|
}
|
|
1989
|
-
function
|
|
1990
|
-
if (
|
|
1730
|
+
function assertTitleReadable(title) {
|
|
1731
|
+
if (!/[\p{L}\p{N}]/u.test(title)) {
|
|
1991
1732
|
throw new ValidationError(
|
|
1992
|
-
"the
|
|
1733
|
+
"the title needs at least one letter or number \u2014 it is what search and wiki links read back"
|
|
1993
1734
|
);
|
|
1994
1735
|
}
|
|
1995
1736
|
}
|
|
1996
|
-
var slugify = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1737
|
+
var slugify = (s) => s.normalize("NFKD").replace(/\p{M}+/gu, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1738
|
+
async function generatedSlug(storage, prefix) {
|
|
1739
|
+
for (let n = 1; n <= 1e3; n++) {
|
|
1740
|
+
const slug = `document-${n}`;
|
|
1741
|
+
try {
|
|
1742
|
+
await storage.readDocument(`${prefix}${slug}`);
|
|
1743
|
+
} catch (err) {
|
|
1744
|
+
if (err instanceof DocumentNotFoundError) return slug;
|
|
1745
|
+
throw err;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
throw new ValidationError(
|
|
1749
|
+
"too many generated document ids in this folder \u2014 give the document a title with Latin letters or digits, or file it elsewhere"
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1997
1752
|
function isIdUnderFolder(id, folder) {
|
|
1998
1753
|
return id.startsWith(`nodes/${folder}/`) || id.startsWith(`${folder}/`);
|
|
1999
1754
|
}
|
|
1755
|
+
function assertSafeFolder(folder) {
|
|
1756
|
+
if (folder.split(/[/\\]/).some((seg) => seg === "..")) {
|
|
1757
|
+
throw new ValidationError(
|
|
1758
|
+
`Invalid folder "${folder}": path traversal ("..") is not allowed.`
|
|
1759
|
+
);
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
async function storagePrefix(nestId) {
|
|
1763
|
+
const { storage } = await engineCache.get(nestId);
|
|
1764
|
+
return await storage.detectLayout() === "structured" ? "nodes" : "";
|
|
1765
|
+
}
|
|
2000
1766
|
function folderToSegments(folder) {
|
|
2001
1767
|
const segments = folder.split("/").map(slugify).filter(Boolean);
|
|
2002
1768
|
if (folder.trim() && segments.length === 0) {
|
|
@@ -2005,8 +1771,10 @@ function folderToSegments(folder) {
|
|
|
2005
1771
|
);
|
|
2006
1772
|
}
|
|
2007
1773
|
if (segments[0] === "nodes") segments.shift();
|
|
2008
|
-
if (segments.length >
|
|
2009
|
-
throw new ValidationError(
|
|
1774
|
+
if (segments.length > MAX_FOLDER_DEPTH) {
|
|
1775
|
+
throw new ValidationError(
|
|
1776
|
+
`folder may nest at most ${MAX_FOLDER_DEPTH} levels deep`
|
|
1777
|
+
);
|
|
2010
1778
|
}
|
|
2011
1779
|
if (segments.some((seg) => seg.length > 100)) {
|
|
2012
1780
|
throw new ValidationError(
|
|
@@ -2037,12 +1805,16 @@ function toSafeError(err, context, message) {
|
|
|
2037
1805
|
"This document is rejected. Change its status before saving."
|
|
2038
1806
|
);
|
|
2039
1807
|
}
|
|
1808
|
+
if (isVaultLockTimeout(err)) {
|
|
1809
|
+
console.error(`${context}:`, err);
|
|
1810
|
+
return new VaultBusyError();
|
|
1811
|
+
}
|
|
2040
1812
|
console.error(`${context}:`, err);
|
|
2041
1813
|
return new AppError(500, message);
|
|
2042
1814
|
}
|
|
2043
1815
|
function bodyOnly(nodeId, raw) {
|
|
2044
1816
|
try {
|
|
2045
|
-
return
|
|
1817
|
+
return parseDocument(`${nodeId}.md`, raw, nodeId).body ?? "";
|
|
2046
1818
|
} catch {
|
|
2047
1819
|
return raw;
|
|
2048
1820
|
}
|
|
@@ -2051,6 +1823,12 @@ var RUNNABLE_NODE_TYPES = ["agent", "skill"];
|
|
|
2051
1823
|
function isRunnableType(type) {
|
|
2052
1824
|
return RUNNABLE_NODE_TYPES.includes(type || "");
|
|
2053
1825
|
}
|
|
1826
|
+
var ACCEPTED_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
1827
|
+
...NODE_TYPES,
|
|
1828
|
+
...RUNNABLE_NODE_TYPES,
|
|
1829
|
+
"artifact",
|
|
1830
|
+
"table"
|
|
1831
|
+
]);
|
|
2054
1832
|
function toNodeResponse(node) {
|
|
2055
1833
|
const fm = node.frontmatter;
|
|
2056
1834
|
const title = fm.title === void 0 || fm.title === null ? "" : String(fm.title);
|
|
@@ -2127,6 +1905,15 @@ async function resolveAuthorship(nestId, nodes) {
|
|
|
2127
1905
|
}
|
|
2128
1906
|
return result;
|
|
2129
1907
|
}
|
|
1908
|
+
var folderScopeSupported = null;
|
|
1909
|
+
function engineScopesFolders() {
|
|
1910
|
+
if (folderScopeSupported === null) {
|
|
1911
|
+
const op = engineApi.getOperation("context_list");
|
|
1912
|
+
const properties = op ? inputJsonSchema(op).properties ?? {} : {};
|
|
1913
|
+
folderScopeSupported = "folder" in properties;
|
|
1914
|
+
}
|
|
1915
|
+
return folderScopeSupported;
|
|
1916
|
+
}
|
|
2130
1917
|
async function getNodeForCaller(nestId, selector, userId, userEmail, opts = {}) {
|
|
2131
1918
|
if (selector.id && !await canReadNode(nestId, selector.id, userId, userEmail)) {
|
|
2132
1919
|
throw new ForbiddenError(
|
|
@@ -2168,29 +1955,52 @@ async function getNodeForCaller(nestId, selector, userId, userEmail, opts = {})
|
|
|
2168
1955
|
};
|
|
2169
1956
|
}
|
|
2170
1957
|
async function listNodesForCaller(nestId, userId, filters = {}) {
|
|
1958
|
+
if (filters.folder !== void 0) assertSafeFolder(filters.folder);
|
|
2171
1959
|
const { versions: versionManager } = await engineCache.get(nestId);
|
|
2172
1960
|
const userEmail = await resolveCallerEmail(userId);
|
|
2173
|
-
const
|
|
2174
|
-
|
|
2175
|
-
{
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
1961
|
+
const listFilters = {
|
|
1962
|
+
...filters.type ? { type: filters.type } : {},
|
|
1963
|
+
...filters.tag ? { tag: filters.tag } : {},
|
|
1964
|
+
...filters.status ? { status: filters.status } : {}
|
|
1965
|
+
};
|
|
1966
|
+
const frontmatterFilters = { ...listFilters, includeRetired: true };
|
|
1967
|
+
let documents;
|
|
1968
|
+
if (filters.withContent) {
|
|
1969
|
+
const scopeInEngine = filters.folder !== void 0 && engineScopesFolders();
|
|
1970
|
+
const scopePrefix = scopeInEngine ? await storagePrefix(nestId) : "";
|
|
1971
|
+
({ documents } = await engineApi.run(
|
|
1972
|
+
"context_list",
|
|
1973
|
+
{
|
|
1974
|
+
...listFilters,
|
|
1975
|
+
include_retired: true,
|
|
1976
|
+
...scopeInEngine ? {
|
|
1977
|
+
folder: [scopePrefix, filters.folder].filter(Boolean).join("/"),
|
|
1978
|
+
recursive: false
|
|
1979
|
+
} : {},
|
|
1980
|
+
full: true
|
|
1981
|
+
},
|
|
1982
|
+
await opContext(nestId, userEmail)
|
|
1983
|
+
));
|
|
1984
|
+
} else {
|
|
1985
|
+
await ensureNodeIndex(nestId);
|
|
1986
|
+
const indexed = await readIndexedNodes(
|
|
1987
|
+
nestId,
|
|
1988
|
+
filters.folder !== void 0 ? { folder: filters.folder } : {}
|
|
1989
|
+
);
|
|
1990
|
+
documents = filterDocuments(indexed, {
|
|
1991
|
+
...frontmatterFilters,
|
|
1992
|
+
includeRetired: true
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1995
|
+
const scoped = filters.folder === void 0 ? documents : documents.filter((d) => folderOfId(d.id) === filters.folder);
|
|
1996
|
+
const nodes = scoped.map((d) => ({
|
|
2189
1997
|
id: d.id,
|
|
2190
1998
|
filePath: "",
|
|
2191
1999
|
rawContent: "",
|
|
2192
2000
|
frontmatter: d.frontmatter,
|
|
2193
|
-
|
|
2001
|
+
// Empty on the indexed path: no listing renders a body, and the
|
|
2002
|
+
// approved-only branch below serves its own from the version store.
|
|
2003
|
+
body: d.body ?? ""
|
|
2194
2004
|
}));
|
|
2195
2005
|
const publicReader = await isPublicReader(nestId, userId);
|
|
2196
2006
|
const approvedOnly = publicReader || filters.approvedOnly === true;
|
|
@@ -2207,6 +2017,13 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
|
|
|
2207
2017
|
accessible = accessible.filter((d) => approvedVersions.has(d.id));
|
|
2208
2018
|
}
|
|
2209
2019
|
if (filters.limit) accessible = accessible.slice(0, filters.limit);
|
|
2020
|
+
const [displayStatuses, driftedDocs] = approvedVersions ? [null, null] : await Promise.all([
|
|
2021
|
+
getDisplayStatuses(nestId),
|
|
2022
|
+
// From the DB, not a vault walk. The disk truth behind it is
|
|
2023
|
+
// reconciled by the index rebuild and kept current by the stage /
|
|
2024
|
+
// approve / reject paths.
|
|
2025
|
+
nodesWithSuggestions(nestId)
|
|
2026
|
+
]);
|
|
2210
2027
|
const authorship = publicReader ? null : await resolveAuthorship(
|
|
2211
2028
|
nestId,
|
|
2212
2029
|
accessible.map((d) => ({
|
|
@@ -2249,22 +2066,125 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
|
|
|
2249
2066
|
return r;
|
|
2250
2067
|
}
|
|
2251
2068
|
let pending = null;
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2069
|
+
if (driftedDocs?.has(doc.id)) {
|
|
2070
|
+
try {
|
|
2071
|
+
pending = await getPendingChange(nestId, doc.id);
|
|
2072
|
+
} catch {
|
|
2073
|
+
pending = null;
|
|
2074
|
+
}
|
|
2256
2075
|
}
|
|
2257
2076
|
if (pending) {
|
|
2258
2077
|
r.pendingChange = pending;
|
|
2259
2078
|
r.status = "external_edit_pending";
|
|
2260
2079
|
} else {
|
|
2261
|
-
r.status =
|
|
2080
|
+
r.status = displayStatuses?.get(r.id) ?? "draft";
|
|
2262
2081
|
}
|
|
2263
2082
|
return r;
|
|
2264
2083
|
})
|
|
2265
2084
|
);
|
|
2266
2085
|
return enriched;
|
|
2267
2086
|
}
|
|
2087
|
+
function withSubtreeTotals(direct) {
|
|
2088
|
+
return direct.map(({ path, count }) => ({
|
|
2089
|
+
path,
|
|
2090
|
+
count,
|
|
2091
|
+
total: direct.reduce(
|
|
2092
|
+
(sum, f) => f.path === path || f.path.startsWith(`${path}/`) ? sum + f.count : sum,
|
|
2093
|
+
0
|
|
2094
|
+
)
|
|
2095
|
+
})).sort((a, b) => a.path.localeCompare(b.path));
|
|
2096
|
+
}
|
|
2097
|
+
async function listNestFolders(nestId, opts = {}) {
|
|
2098
|
+
if (opts.folder !== void 0) assertSafeFolder(opts.folder);
|
|
2099
|
+
await ensureNodeIndex(nestId);
|
|
2100
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2101
|
+
for (const { path, count } of await indexedFolderCounts(nestId)) {
|
|
2102
|
+
counts.set(path, count);
|
|
2103
|
+
let parent = path;
|
|
2104
|
+
while (parent.includes("/")) {
|
|
2105
|
+
parent = parent.slice(0, parent.lastIndexOf("/"));
|
|
2106
|
+
if (!counts.has(parent)) counts.set(parent, 0);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
const direct = [...counts].map(([path, count]) => ({ path, count }));
|
|
2110
|
+
const scope = opts.folder ?? "";
|
|
2111
|
+
const inScope = direct.filter((f) => {
|
|
2112
|
+
if (scope && !f.path.startsWith(`${scope}/`)) return false;
|
|
2113
|
+
if (opts.recursive === false) {
|
|
2114
|
+
const rel = scope ? f.path.slice(scope.length + 1) : f.path;
|
|
2115
|
+
return !rel.includes("/");
|
|
2116
|
+
}
|
|
2117
|
+
return true;
|
|
2118
|
+
});
|
|
2119
|
+
return withSubtreeTotals(inScope);
|
|
2120
|
+
}
|
|
2121
|
+
var RECENT_LIMIT = 3;
|
|
2122
|
+
async function nestCensus(nestId, userId) {
|
|
2123
|
+
const userEmail = await resolveCallerEmail(userId);
|
|
2124
|
+
await ensureNodeIndex(nestId);
|
|
2125
|
+
const documents = await readIndexedNodes(nestId);
|
|
2126
|
+
const visible = await filterAccessible(nestId, userId, userEmail, documents);
|
|
2127
|
+
const tags = {};
|
|
2128
|
+
const types = {};
|
|
2129
|
+
const direct = /* @__PURE__ */ new Map();
|
|
2130
|
+
const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1e3;
|
|
2131
|
+
let editedLast7d = 0;
|
|
2132
|
+
for (const d of visible) {
|
|
2133
|
+
const fm = d.frontmatter;
|
|
2134
|
+
types[fm.type || "document"] = (types[fm.type || "document"] || 0) + 1;
|
|
2135
|
+
for (const t of fm.tags || []) tags[String(t)] = (tags[String(t)] || 0) + 1;
|
|
2136
|
+
const folder = folderOfId(d.id);
|
|
2137
|
+
if (folder) direct.set(folder, (direct.get(folder) || 0) + 1);
|
|
2138
|
+
const edited = Date.parse(String(fm.updated_at || ""));
|
|
2139
|
+
if (!Number.isNaN(edited) && edited >= cutoff) editedLast7d++;
|
|
2140
|
+
}
|
|
2141
|
+
const paths = /* @__PURE__ */ new Set();
|
|
2142
|
+
for (const folder of direct.keys()) {
|
|
2143
|
+
const segs = folder.split("/");
|
|
2144
|
+
for (let i = 1; i <= segs.length; i++) paths.add(segs.slice(0, i).join("/"));
|
|
2145
|
+
}
|
|
2146
|
+
const folders = withSubtreeTotals(
|
|
2147
|
+
[...paths].map((path) => ({ path, count: direct.get(path) ?? 0 }))
|
|
2148
|
+
);
|
|
2149
|
+
const statusMap = await getDisplayStatuses(nestId);
|
|
2150
|
+
const statuses = {};
|
|
2151
|
+
for (const d of visible) {
|
|
2152
|
+
const st = statusMap.get(d.id) ?? "draft";
|
|
2153
|
+
statuses[st] = (statuses[st] || 0) + 1;
|
|
2154
|
+
}
|
|
2155
|
+
const authorship = await resolveAuthorship(
|
|
2156
|
+
nestId,
|
|
2157
|
+
visible.map((d) => ({
|
|
2158
|
+
id: d.id,
|
|
2159
|
+
frontmatterAuthor: d.frontmatter.author ?? null
|
|
2160
|
+
}))
|
|
2161
|
+
);
|
|
2162
|
+
const authors = [
|
|
2163
|
+
...new Set(
|
|
2164
|
+
[...authorship.values()].map((a) => a.author).filter((a) => !!a)
|
|
2165
|
+
)
|
|
2166
|
+
].sort((a, b) => a.localeCompare(b));
|
|
2167
|
+
const recent = visible.filter((d) => d.frontmatter.updated_at).sort(
|
|
2168
|
+
(a, b) => String(b.frontmatter.updated_at).localeCompare(
|
|
2169
|
+
String(a.frontmatter.updated_at)
|
|
2170
|
+
)
|
|
2171
|
+
).slice(0, RECENT_LIMIT).map((d) => ({
|
|
2172
|
+
id: d.id,
|
|
2173
|
+
title: String(d.frontmatter.title ?? ""),
|
|
2174
|
+
type: d.frontmatter.type || "document",
|
|
2175
|
+
updated_at: d.frontmatter.updated_at
|
|
2176
|
+
}));
|
|
2177
|
+
return {
|
|
2178
|
+
total: visible.length,
|
|
2179
|
+
folders,
|
|
2180
|
+
tags,
|
|
2181
|
+
types,
|
|
2182
|
+
statuses,
|
|
2183
|
+
authors,
|
|
2184
|
+
editedLast7d,
|
|
2185
|
+
recent
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2268
2188
|
async function listNodesForCallerByEmail(nestId, userEmail, filters = {}) {
|
|
2269
2189
|
return listNodesForCaller(nestId, await userIdFromEmail(userEmail), filters);
|
|
2270
2190
|
}
|
|
@@ -2290,14 +2210,27 @@ async function autoPublishReason(nestId, userEmail, doc) {
|
|
|
2290
2210
|
return await actorAutoPublishes(nestId, userEmail) ? "owner/admin self-approve" : null;
|
|
2291
2211
|
}
|
|
2292
2212
|
async function createNode(nestId, input, userEmail) {
|
|
2213
|
+
const generatedId = !input.id && !slugify(input.title);
|
|
2214
|
+
for (let attempt = 0; ; attempt++) {
|
|
2215
|
+
try {
|
|
2216
|
+
return await createNodeOnce(nestId, input, userEmail);
|
|
2217
|
+
} catch (err) {
|
|
2218
|
+
if (!generatedId || attempt >= 3 || !(err instanceof ConflictError)) {
|
|
2219
|
+
throw err;
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
async function createNodeOnce(nestId, input, userEmail) {
|
|
2293
2225
|
const { storage, versions: versionManager } = await engineCache.get(nestId);
|
|
2294
2226
|
assertTitleLength(input.title);
|
|
2295
|
-
|
|
2227
|
+
assertTitleReadable(input.title);
|
|
2296
2228
|
const slug = slugify(input.title).slice(0, 100).replace(/-+$/, "");
|
|
2297
2229
|
let id = input.id;
|
|
2298
2230
|
if (!id) {
|
|
2299
2231
|
const folderSegments = folderToSegments(input.folder ?? "");
|
|
2300
|
-
|
|
2232
|
+
const prefix = folderSegments.length > 0 ? `nodes/${folderSegments.join("/")}/` : "nodes/";
|
|
2233
|
+
id = `${prefix}${slug || await generatedSlug(storage, prefix)}`;
|
|
2301
2234
|
}
|
|
2302
2235
|
const tags = (input.tags || []).map(normalizeTag2);
|
|
2303
2236
|
const requestedType = input.type || "document";
|
|
@@ -2307,6 +2240,11 @@ async function createNode(nestId, input, userEmail) {
|
|
|
2307
2240
|
if (requestedType === "table" && !config.TYPE_TABLE_ENABLED) {
|
|
2308
2241
|
throw new ValidationError("Table nodes are disabled on this server");
|
|
2309
2242
|
}
|
|
2243
|
+
if (!ACCEPTED_NODE_TYPES.has(requestedType)) {
|
|
2244
|
+
throw new ValidationError(
|
|
2245
|
+
`unknown node type "${requestedType}"; allowed: ${[...ACCEPTED_NODE_TYPES].sort().join(", ")}`
|
|
2246
|
+
);
|
|
2247
|
+
}
|
|
2310
2248
|
if (input.schedule !== void 0 && !isRunnableType(input.type)) {
|
|
2311
2249
|
throw new ValidationError(
|
|
2312
2250
|
`schedule is only valid for runnable node types (${RUNNABLE_NODE_TYPES.join(
|
|
@@ -2558,7 +2496,7 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
|
2558
2496
|
if (patch.title) {
|
|
2559
2497
|
if (patch.title !== node.frontmatter.title) {
|
|
2560
2498
|
assertTitleLength(patch.title);
|
|
2561
|
-
|
|
2499
|
+
assertTitleReadable(patch.title);
|
|
2562
2500
|
}
|
|
2563
2501
|
node = {
|
|
2564
2502
|
...node,
|
|
@@ -2706,6 +2644,8 @@ var NODE_KEYED_TABLES = [
|
|
|
2706
2644
|
["annotation_threads", "node_id"],
|
|
2707
2645
|
["comments", "node_id"],
|
|
2708
2646
|
["node_tag_index", "node_id"],
|
|
2647
|
+
["node_index", "node_id"],
|
|
2648
|
+
["node_suggestions", "node_id"],
|
|
2709
2649
|
["watchers", "node_id"],
|
|
2710
2650
|
["schedules", "agent_node"],
|
|
2711
2651
|
// A live capability token bound to one agent node — deleting the agent must
|
|
@@ -2784,8 +2724,8 @@ async function moveNode(nestId, nodeId, folder, userEmail) {
|
|
|
2784
2724
|
await storage.writeDocument(newId, serializeNodeSafe(moved));
|
|
2785
2725
|
try {
|
|
2786
2726
|
const nestPath = resolveNestPath(nestId);
|
|
2787
|
-
const oldChain =
|
|
2788
|
-
const newChain =
|
|
2727
|
+
const oldChain = join(nestPath, dirname(nodeId), ".versions", leaf);
|
|
2728
|
+
const newChain = join(nestPath, dirname(newId), ".versions", leaf);
|
|
2789
2729
|
if (oldChain !== newChain) {
|
|
2790
2730
|
await mkdir(dirname(newChain), { recursive: true });
|
|
2791
2731
|
await rename(oldChain, newChain);
|
|
@@ -2818,6 +2758,11 @@ async function moveNode(nestId, nodeId, folder, userEmail) {
|
|
|
2818
2758
|
await storage.deleteDocument(oldDerived);
|
|
2819
2759
|
} catch {
|
|
2820
2760
|
}
|
|
2761
|
+
try {
|
|
2762
|
+
await storage.regenerateIndex();
|
|
2763
|
+
} catch (err) {
|
|
2764
|
+
console.error("[move] regenerateIndex failed", nestId, nodeId, newId, err);
|
|
2765
|
+
}
|
|
2821
2766
|
await trackEvent("node.move", { nestId, nodeId, newId });
|
|
2822
2767
|
return { node: moved, previousId: nodeId };
|
|
2823
2768
|
});
|
|
@@ -2888,7 +2833,7 @@ async function deleteFolder(nestId, folder, userEmail) {
|
|
|
2888
2833
|
|
|
2889
2834
|
// src/nests/unsynced-service.ts
|
|
2890
2835
|
import { readdirSync, readFileSync, statSync } from "fs";
|
|
2891
|
-
import { join as
|
|
2836
|
+
import { join as join2, relative, resolve } from "path";
|
|
2892
2837
|
import { rm } from "fs/promises";
|
|
2893
2838
|
var RESERVED = /* @__PURE__ */ new Set(["nests"]);
|
|
2894
2839
|
function isNestStorageRoot(absPath) {
|
|
@@ -2905,7 +2850,7 @@ function scanMarkdown(dir) {
|
|
|
2905
2850
|
}
|
|
2906
2851
|
for (const e of entries) {
|
|
2907
2852
|
if (e.name.startsWith(".") || e.name === "node_modules") continue;
|
|
2908
|
-
const full =
|
|
2853
|
+
const full = join2(dir, e.name);
|
|
2909
2854
|
if (e.isDirectory()) {
|
|
2910
2855
|
const sub = scanMarkdown(full);
|
|
2911
2856
|
count += sub.count;
|
|
@@ -2955,7 +2900,7 @@ function collectLeafFolders(rel, abs) {
|
|
|
2955
2900
|
if (!e.isDirectory()) continue;
|
|
2956
2901
|
if (e.name.startsWith(".") || e.name === "node_modules") continue;
|
|
2957
2902
|
out.push(
|
|
2958
|
-
...collectLeafFolders(`${rel}/${e.name}`,
|
|
2903
|
+
...collectLeafFolders(`${rel}/${e.name}`, join2(abs, e.name))
|
|
2959
2904
|
);
|
|
2960
2905
|
}
|
|
2961
2906
|
return out;
|
|
@@ -2973,7 +2918,7 @@ function listUnsyncedFolders() {
|
|
|
2973
2918
|
if (!e.isDirectory()) continue;
|
|
2974
2919
|
if (e.name.startsWith(".")) continue;
|
|
2975
2920
|
if (RESERVED.has(e.name)) continue;
|
|
2976
|
-
const abs =
|
|
2921
|
+
const abs = join2(root, e.name);
|
|
2977
2922
|
if (isNestStorageRoot(abs)) continue;
|
|
2978
2923
|
out.push(...collectLeafFolders(e.name, abs));
|
|
2979
2924
|
}
|
|
@@ -3010,7 +2955,7 @@ function collectMarkdownFiles(dir, root) {
|
|
|
3010
2955
|
}
|
|
3011
2956
|
for (const e of entries) {
|
|
3012
2957
|
if (e.name.startsWith(".") || e.name === "node_modules") continue;
|
|
3013
|
-
const full =
|
|
2958
|
+
const full = join2(dir, e.name);
|
|
3014
2959
|
if (e.isDirectory()) {
|
|
3015
2960
|
out.push(...collectMarkdownFiles(full, root));
|
|
3016
2961
|
} else if (e.isFile() && e.name.toLowerCase().endsWith(".md")) {
|
|
@@ -3028,7 +2973,7 @@ async function syncUnsyncedFolder(userId, folderName, callerEmail) {
|
|
|
3028
2973
|
`[unsynced] sync requested folder="${folderName}" user="${callerEmail}"`
|
|
3029
2974
|
);
|
|
3030
2975
|
assertSafeFolderName(folderName);
|
|
3031
|
-
const src =
|
|
2976
|
+
const src = join2(config.DATA_ROOT, folderName);
|
|
3032
2977
|
if (isNestStorageRoot(src)) {
|
|
3033
2978
|
throw new ValidationError("Folder is not eligible for sync");
|
|
3034
2979
|
}
|
|
@@ -3084,7 +3029,7 @@ async function syncUnsyncedFolder(userId, folderName, callerEmail) {
|
|
|
3084
3029
|
async function deleteUnsyncedFolder(folderName) {
|
|
3085
3030
|
console.log(`[unsynced] delete requested folder="${folderName}"`);
|
|
3086
3031
|
assertSafeFolderName(folderName);
|
|
3087
|
-
const src =
|
|
3032
|
+
const src = join2(config.DATA_ROOT, folderName);
|
|
3088
3033
|
if (isNestStorageRoot(src)) {
|
|
3089
3034
|
throw new ValidationError("Folder is not eligible for delete");
|
|
3090
3035
|
}
|
|
@@ -3180,9 +3125,11 @@ nestRoutes.get("/", async (c) => {
|
|
|
3180
3125
|
}
|
|
3181
3126
|
const out = await Promise.all(deduped.map(annotate));
|
|
3182
3127
|
const docRows = await db.all(
|
|
3183
|
-
|
|
3128
|
+
`SELECT nest_id, COUNT(DISTINCT node_id) AS c, MAX(created_at) AS last_activity
|
|
3129
|
+
FROM node_versions GROUP BY nest_id`
|
|
3184
3130
|
);
|
|
3185
3131
|
const docByNest = new Map(docRows.map((r) => [r.nest_id, Number(r.c)]));
|
|
3132
|
+
const activityByNest = new Map(docRows.map((r) => [r.nest_id, r.last_activity]));
|
|
3186
3133
|
const collabRows = await db.all(
|
|
3187
3134
|
"SELECT nest_id, COUNT(*) AS c FROM nest_collaborators GROUP BY nest_id"
|
|
3188
3135
|
);
|
|
@@ -3236,6 +3183,7 @@ nestRoutes.get("/", async (c) => {
|
|
|
3236
3183
|
collaborator_emails: []
|
|
3237
3184
|
} : {
|
|
3238
3185
|
...n,
|
|
3186
|
+
last_activity: activityByNest.get(n.id) ?? null,
|
|
3239
3187
|
document_count: docByNest.get(n.id) ?? 0,
|
|
3240
3188
|
collaborator_count: collabByNest.get(n.id) ?? 0,
|
|
3241
3189
|
steward_count: stewardByNest.get(n.id) ?? 0,
|
|
@@ -3499,7 +3447,7 @@ async function addCollaborator(params) {
|
|
|
3499
3447
|
if (existing) {
|
|
3500
3448
|
userId = existing.id;
|
|
3501
3449
|
} else {
|
|
3502
|
-
const { hashPassword: hashPassword2 } = await import("./keys-
|
|
3450
|
+
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
3503
3451
|
userId = uuid3();
|
|
3504
3452
|
await db.run(
|
|
3505
3453
|
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
@@ -3691,7 +3639,7 @@ async function resolveGrantee(email) {
|
|
|
3691
3639
|
const e = normalizeEmail(email);
|
|
3692
3640
|
const existing = await db.get("SELECT id FROM users WHERE LOWER(email) = ?", [e]);
|
|
3693
3641
|
if (existing) return existing.id;
|
|
3694
|
-
const { hashPassword: hashPassword2 } = await import("./keys-
|
|
3642
|
+
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
3695
3643
|
const id = uuid4();
|
|
3696
3644
|
await db.run(
|
|
3697
3645
|
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
@@ -3699,6 +3647,41 @@ async function resolveGrantee(email) {
|
|
|
3699
3647
|
);
|
|
3700
3648
|
return id;
|
|
3701
3649
|
}
|
|
3650
|
+
async function assertGrantAddsAccess(params) {
|
|
3651
|
+
const { nestId, email, userId, targetType, target, role } = params;
|
|
3652
|
+
const roles = [];
|
|
3653
|
+
if (userId) {
|
|
3654
|
+
const access = await resolveNestAccess(nestId, userId);
|
|
3655
|
+
if (access.ownerId === userId) roles.push("owner");
|
|
3656
|
+
const collab = collabPermToRole(access.collaboratorPermission);
|
|
3657
|
+
if (collab) roles.push(collab);
|
|
3658
|
+
for (const teamRole of await resolveTeamRolesForUser(nestId, userId)) {
|
|
3659
|
+
roles.push(teamRole);
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
if (targetType === "document") {
|
|
3663
|
+
const stewards = await resolveStewardsForNode(nestId, target);
|
|
3664
|
+
for (const s of stewards) {
|
|
3665
|
+
if (s.steward.userEmail.toLowerCase() === email.toLowerCase()) {
|
|
3666
|
+
roles.push(s.steward.role);
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
}
|
|
3670
|
+
const covered = role === "write" ? canEditWith(roles) : roles.length > 0;
|
|
3671
|
+
if (covered) {
|
|
3672
|
+
const held = primaryRole(roles) ?? "existing";
|
|
3673
|
+
throw new ValidationError(
|
|
3674
|
+
`${email} already has ${held} access here, so this ${targetType} is already ${role === "write" ? "editable by" : "visible to"} them.`
|
|
3675
|
+
);
|
|
3676
|
+
}
|
|
3677
|
+
if (!userId) return;
|
|
3678
|
+
const existing = await resolveNodeGrant(nestId, userId, target);
|
|
3679
|
+
if (existing && (role === "read" || existing === "write")) {
|
|
3680
|
+
throw new ValidationError(
|
|
3681
|
+
`${email} has already been shared this ${targetType} (${existing === "write" ? "can edit" : "can view"}).`
|
|
3682
|
+
);
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3702
3685
|
grantRoutes.post("/", async (c) => {
|
|
3703
3686
|
const nestId = c.req.param("nestId");
|
|
3704
3687
|
await requireManage(c, nestId);
|
|
@@ -3714,7 +3697,19 @@ grantRoutes.post("/", async (c) => {
|
|
|
3714
3697
|
if (!["read", "write"].includes(role)) {
|
|
3715
3698
|
throw new ValidationError("role must be read | write");
|
|
3716
3699
|
}
|
|
3717
|
-
const
|
|
3700
|
+
const email = normalizeEmail(body.email);
|
|
3701
|
+
const known = await getDb().get("SELECT id FROM users WHERE LOWER(email) = ?", [
|
|
3702
|
+
email
|
|
3703
|
+
]);
|
|
3704
|
+
await assertGrantAddsAccess({
|
|
3705
|
+
nestId,
|
|
3706
|
+
email,
|
|
3707
|
+
userId: known?.id ?? null,
|
|
3708
|
+
targetType,
|
|
3709
|
+
target,
|
|
3710
|
+
role
|
|
3711
|
+
});
|
|
3712
|
+
const userId = known?.id ?? await resolveGrantee(body.email);
|
|
3718
3713
|
const grant = await createGrant({
|
|
3719
3714
|
nestId,
|
|
3720
3715
|
targetType,
|
|
@@ -4122,7 +4117,7 @@ function toSearchHit(node, query) {
|
|
|
4122
4117
|
import { Hono as Hono8 } from "hono";
|
|
4123
4118
|
|
|
4124
4119
|
// src/nodes/markdown-export.ts
|
|
4125
|
-
import { parseDocument as
|
|
4120
|
+
import { parseDocument as parseDocument2 } from "@promptowl/contextnest-engine";
|
|
4126
4121
|
function nodeToMarkdown(node) {
|
|
4127
4122
|
const tags = node.tags ?? [];
|
|
4128
4123
|
return [
|
|
@@ -4158,7 +4153,7 @@ function rawContentType(nodeType) {
|
|
|
4158
4153
|
function frontmatterBlockOf(raw) {
|
|
4159
4154
|
if (!raw || !raw.startsWith("---")) return "";
|
|
4160
4155
|
try {
|
|
4161
|
-
const body =
|
|
4156
|
+
const body = parseDocument2("frontmatter-split.md", raw, "frontmatter-split").body ?? "";
|
|
4162
4157
|
if (raw.endsWith(body)) return raw.slice(0, raw.length - body.length);
|
|
4163
4158
|
} catch {
|
|
4164
4159
|
}
|
|
@@ -4204,7 +4199,14 @@ nodeRoutes.get("/", async (c) => {
|
|
|
4204
4199
|
// Lets an owner preview what making the nest public would expose.
|
|
4205
4200
|
...q.approved_only === "1" || q.approved_only === "true" ? { approvedOnly: true } : {},
|
|
4206
4201
|
// Ignore junk rather than 400 — a bad ?limit should not fail a listing.
|
|
4207
|
-
...limit !== void 0 && Number.isFinite(limit) && limit > 0 ? { limit } : {}
|
|
4202
|
+
...limit !== void 0 && Number.isFinite(limit) && limit > 0 ? { limit } : {},
|
|
4203
|
+
// Present (even as "") = one folder level only, for the lazily-expanded
|
|
4204
|
+
// document tree. Absent = the whole nest, as every existing caller expects.
|
|
4205
|
+
...q.folder !== void 0 ? { folder: q.folder } : {},
|
|
4206
|
+
// Bodies are off by default — the list is served from the DB index, which
|
|
4207
|
+
// holds metadata only. Opt back in and the listing crawls the vault for
|
|
4208
|
+
// them; kept for any client that was reading `content` off this route.
|
|
4209
|
+
...q.content === "1" || q.content === "true" ? { withContent: true } : {}
|
|
4208
4210
|
});
|
|
4209
4211
|
return c.json({ count: nodes.length, nodes });
|
|
4210
4212
|
});
|
|
@@ -4246,7 +4248,7 @@ nodeRoutes.post("/", async (c) => {
|
|
|
4246
4248
|
nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
4247
4249
|
const nestId = c.req.param("nestId");
|
|
4248
4250
|
const nodeId = c.req.param("nodeId");
|
|
4249
|
-
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-
|
|
4251
|
+
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-JAFSU5GN.js");
|
|
4250
4252
|
const { stewards, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback2(
|
|
4251
4253
|
nestId,
|
|
4252
4254
|
nodeId
|
|
@@ -4267,7 +4269,7 @@ nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
|
4267
4269
|
nodeRoutes.get("/:nodeId{.+}/reviews", async (c) => {
|
|
4268
4270
|
const nestId = c.req.param("nestId");
|
|
4269
4271
|
const nodeId = c.req.param("nodeId");
|
|
4270
|
-
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-
|
|
4272
|
+
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-LK4M2C43.js");
|
|
4271
4273
|
const history = await getReviewHistory2(nestId, nodeId);
|
|
4272
4274
|
return c.json({ reviews: history });
|
|
4273
4275
|
});
|
|
@@ -4906,7 +4908,6 @@ annotationRoutes.get("/:nodeId{.+}/hosted", async (c) => {
|
|
|
4906
4908
|
|
|
4907
4909
|
// src/nodes/query-routes.ts
|
|
4908
4910
|
import { Hono as Hono10 } from "hono";
|
|
4909
|
-
import { serializeDocument as serializeDocument3 } from "@promptowl/contextnest-engine";
|
|
4910
4911
|
|
|
4911
4912
|
// src/tables/service.ts
|
|
4912
4913
|
function splitCsvLine(line) {
|
|
@@ -5327,7 +5328,7 @@ async function resolveExportBody(nestId, nodeId, workingBody) {
|
|
|
5327
5328
|
|
|
5328
5329
|
// src/nests/bundle-service.ts
|
|
5329
5330
|
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
5330
|
-
import { join as
|
|
5331
|
+
import { join as join3, sep } from "path";
|
|
5331
5332
|
import JSZip from "jszip";
|
|
5332
5333
|
import { serializeDocument as serializeDocument2 } from "@promptowl/contextnest-engine";
|
|
5333
5334
|
function walkFiles(root) {
|
|
@@ -5340,7 +5341,7 @@ function walkFiles(root) {
|
|
|
5340
5341
|
}
|
|
5341
5342
|
for (const rel of entries) {
|
|
5342
5343
|
try {
|
|
5343
|
-
if (statSync2(
|
|
5344
|
+
if (statSync2(join3(root, rel)).isFile()) out.push(rel.split(sep).join("/"));
|
|
5344
5345
|
} catch {
|
|
5345
5346
|
}
|
|
5346
5347
|
}
|
|
@@ -5381,7 +5382,7 @@ async function exportNestBundle(nestId, includeDrafts) {
|
|
|
5381
5382
|
const segs = rel.split("/");
|
|
5382
5383
|
const keep = segs.includes(".versions") || rel === ".context/config.yaml" || rel === "CONTEXT.md";
|
|
5383
5384
|
if (!keep) continue;
|
|
5384
|
-
zip.file(rel, readFileSync2(
|
|
5385
|
+
zip.file(rel, readFileSync2(join3(nestPath, rel)));
|
|
5385
5386
|
}
|
|
5386
5387
|
trackEvent("nest.export.bundle", { nestId, documents: count, includeDrafts });
|
|
5387
5388
|
return zip.generateAsync({ type: "arraybuffer" });
|
|
@@ -5552,7 +5553,9 @@ queryRoutes.get("/changes", async (c) => {
|
|
|
5552
5553
|
const includeContent = ["1", "true"].includes(
|
|
5553
5554
|
c.req.query("include_content") || ""
|
|
5554
5555
|
);
|
|
5555
|
-
const all = await listNodesForCaller(nestId, userId
|
|
5556
|
+
const all = await listNodesForCaller(nestId, userId, {
|
|
5557
|
+
...includeContent ? { withContent: true } : {}
|
|
5558
|
+
});
|
|
5556
5559
|
const db = getDb();
|
|
5557
5560
|
const publicReader = await isPublicReader(nestId, userId);
|
|
5558
5561
|
const approvedRows = await db.all(
|
|
@@ -5619,7 +5622,10 @@ queryRoutes.get("/runnables", async (c) => {
|
|
|
5619
5622
|
const nestId = c.req.param("nestId");
|
|
5620
5623
|
const userId = c.get("userId");
|
|
5621
5624
|
const all = await listNodesForCaller(nestId, userId, {
|
|
5622
|
-
type: RUNNABLE_NODE_TYPES
|
|
5625
|
+
type: RUNNABLE_NODE_TYPES,
|
|
5626
|
+
// The prompt/skill body IS the payload here — an orchestrator pulls it to
|
|
5627
|
+
// run the thing, so this surface pays for the file reads.
|
|
5628
|
+
withContent: true
|
|
5623
5629
|
});
|
|
5624
5630
|
const runnables = all.map((n) => ({
|
|
5625
5631
|
id: n.id,
|
|
@@ -5661,11 +5667,15 @@ queryRoutes.get("/graph", async (c) => {
|
|
|
5661
5667
|
function approxTokens(text) {
|
|
5662
5668
|
return Math.ceil(text.length / 4);
|
|
5663
5669
|
}
|
|
5664
|
-
function formatContextBlock(node) {
|
|
5670
|
+
function formatContextBlock(node, url) {
|
|
5665
5671
|
const tags = (node.frontmatter.tags || []).join(" ");
|
|
5666
|
-
const
|
|
5672
|
+
const meta = [
|
|
5673
|
+
tags ? `_tags: ${tags}_` : null,
|
|
5674
|
+
url ? `_source: ${url}_` : null
|
|
5675
|
+
].filter(Boolean);
|
|
5676
|
+
const header = meta.length ? `# ${node.frontmatter.title}
|
|
5667
5677
|
|
|
5668
|
-
|
|
5678
|
+
${meta.join("\n")}
|
|
5669
5679
|
|
|
5670
5680
|
` : `# ${node.frontmatter.title}
|
|
5671
5681
|
|
|
@@ -5678,6 +5688,7 @@ queryRoutes.post("/context", async (c) => {
|
|
|
5678
5688
|
throw new ValidationError("prompt or selector is required");
|
|
5679
5689
|
}
|
|
5680
5690
|
const nestId = c.req.param("nestId");
|
|
5691
|
+
const baseUrl = requestBaseUrl(c.req.url);
|
|
5681
5692
|
const { query: queryEngine, storage } = await engineCache.get(nestId);
|
|
5682
5693
|
const maxTokens = Math.max(50, body.max_tokens ?? 4e3);
|
|
5683
5694
|
const hops = body.hops ?? 2;
|
|
@@ -5786,20 +5797,21 @@ queryRoutes.post("/context", async (c) => {
|
|
|
5786
5797
|
const readable = await Promise.all(
|
|
5787
5798
|
accessible.map(async (doc) => ({
|
|
5788
5799
|
...doc,
|
|
5789
|
-
body: await resolveReadableBody(nestId, doc.id, userId, doc.body || "")
|
|
5800
|
+
body: await resolveReadableBody(nestId, doc.id, userId, doc.body || ""),
|
|
5801
|
+
url: resolveNodeUrl(doc, { nestId, baseUrl })
|
|
5790
5802
|
}))
|
|
5791
5803
|
);
|
|
5792
5804
|
const included = [];
|
|
5793
5805
|
let tokenCount = 0;
|
|
5794
5806
|
for (const doc of readable) {
|
|
5795
|
-
const block = formatContextBlock(doc);
|
|
5807
|
+
const block = formatContextBlock(doc, doc.url);
|
|
5796
5808
|
const blockTokens = approxTokens(block);
|
|
5797
5809
|
if (tokenCount + blockTokens > maxTokens && included.length > 0) break;
|
|
5798
5810
|
included.push(doc);
|
|
5799
5811
|
tokenCount += blockTokens;
|
|
5800
5812
|
}
|
|
5801
5813
|
const truncatedByBudget = accessible.length - included.length;
|
|
5802
|
-
const contextMarkdown = included.map(formatContextBlock).join("\n\n---\n\n");
|
|
5814
|
+
const contextMarkdown = included.map((n) => formatContextBlock(n, n.url)).join("\n\n---\n\n");
|
|
5803
5815
|
trackEvent("context.resolve", {
|
|
5804
5816
|
nestId,
|
|
5805
5817
|
selector_from_prompt: !!compileDetail,
|
|
@@ -5812,6 +5824,7 @@ queryRoutes.post("/context", async (c) => {
|
|
|
5812
5824
|
nodes: included.map((n) => ({
|
|
5813
5825
|
id: n.id,
|
|
5814
5826
|
title: n.frontmatter.title,
|
|
5827
|
+
url: n.url,
|
|
5815
5828
|
tags: n.frontmatter.tags || [],
|
|
5816
5829
|
type: n.frontmatter.type || "document"
|
|
5817
5830
|
})),
|
|
@@ -5914,6 +5927,25 @@ queryRoutes.get("/overview", async (c) => {
|
|
|
5914
5927
|
}))
|
|
5915
5928
|
});
|
|
5916
5929
|
});
|
|
5930
|
+
queryRoutes.get("/census", async (c) => {
|
|
5931
|
+
const census = await nestCensus(c.req.param("nestId"), c.get("userId"));
|
|
5932
|
+
return c.json({ census });
|
|
5933
|
+
});
|
|
5934
|
+
queryRoutes.get("/folders", async (c) => {
|
|
5935
|
+
const q = c.req.query();
|
|
5936
|
+
const folders = await listNestFolders(c.req.param("nestId"), {
|
|
5937
|
+
...q.folder !== void 0 ? { folder: q.folder } : {},
|
|
5938
|
+
...q.recursive === "0" || q.recursive === "false" ? { recursive: false } : {}
|
|
5939
|
+
});
|
|
5940
|
+
return c.json({ folders });
|
|
5941
|
+
});
|
|
5942
|
+
queryRoutes.post("/reindex", async (c) => {
|
|
5943
|
+
if (permissionLevel(c.get("nestPermission")) < permissionLevel("write")) {
|
|
5944
|
+
throw new ForbiddenError("Reindexing a nest requires write access");
|
|
5945
|
+
}
|
|
5946
|
+
const indexed = await rebuildNodeIndex(c.req.param("nestId"));
|
|
5947
|
+
return c.json({ indexed });
|
|
5948
|
+
});
|
|
5917
5949
|
queryRoutes.get("/comment-counts", async (c) => {
|
|
5918
5950
|
const counts = await countThreadsByNode(c.req.param("nestId"));
|
|
5919
5951
|
return c.json({ counts });
|
|
@@ -6006,48 +6038,51 @@ queryRoutes.post("/publish", async (c) => {
|
|
|
6006
6038
|
}
|
|
6007
6039
|
}
|
|
6008
6040
|
const nestId = c.req.param("nestId");
|
|
6009
|
-
const
|
|
6041
|
+
const userEmail = await resolveCallerEmail(c.get("userId"));
|
|
6010
6042
|
const created = [];
|
|
6043
|
+
const skipped = [];
|
|
6044
|
+
const failed = [];
|
|
6011
6045
|
if (body.context_md) {
|
|
6046
|
+
const { storage } = await engineCache.get(nestId);
|
|
6012
6047
|
await storage.writeContextMd(body.context_md);
|
|
6048
|
+
await storage.regenerateIndex();
|
|
6013
6049
|
}
|
|
6014
6050
|
for (const doc of body.documents || []) {
|
|
6015
6051
|
if (!doc.title || !doc.content) continue;
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6052
|
+
try {
|
|
6053
|
+
const result = await createNode(
|
|
6054
|
+
nestId,
|
|
6055
|
+
{
|
|
6056
|
+
title: doc.title,
|
|
6057
|
+
content: doc.content,
|
|
6058
|
+
type: doc.type,
|
|
6059
|
+
tags: doc.tags,
|
|
6060
|
+
scope: doc.scope
|
|
6061
|
+
},
|
|
6062
|
+
userEmail
|
|
6063
|
+
);
|
|
6064
|
+
created.push(result.node.id);
|
|
6065
|
+
} catch (err) {
|
|
6066
|
+
if (err instanceof ConflictError) {
|
|
6067
|
+
skipped.push(doc.title);
|
|
6068
|
+
continue;
|
|
6069
|
+
}
|
|
6070
|
+
if (isVaultLockTimeout(err) || err instanceof VaultBusyError) throw err;
|
|
6071
|
+
if (!(err instanceof AppError)) console.error("bulk publish row failed", err);
|
|
6072
|
+
failed.push({
|
|
6024
6073
|
title: doc.title,
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
version: 1,
|
|
6029
|
-
created_at: now,
|
|
6030
|
-
updated_at: now,
|
|
6031
|
-
metadata: {
|
|
6032
|
-
owners: ["*"],
|
|
6033
|
-
scope: doc.scope || "team"
|
|
6034
|
-
}
|
|
6035
|
-
},
|
|
6036
|
-
body: doc.content,
|
|
6037
|
-
rawContent: ""
|
|
6038
|
-
};
|
|
6039
|
-
const serialized = serializeDocument3(node);
|
|
6040
|
-
await storage.writeDocument(id, serialized);
|
|
6041
|
-
await syncNodeTags(nestId, id, tags);
|
|
6042
|
-
created.push(id);
|
|
6074
|
+
error: err instanceof AppError ? err.message : "Could not create this document"
|
|
6075
|
+
});
|
|
6076
|
+
}
|
|
6043
6077
|
}
|
|
6044
|
-
if (created.length > 0 || body.context_md) await storage.regenerateIndex();
|
|
6045
6078
|
trackEvent("nest.publish", { nestId, count: created.length });
|
|
6046
6079
|
return c.json(
|
|
6047
6080
|
{
|
|
6048
6081
|
published: created.length,
|
|
6049
6082
|
context_md_updated: !!body.context_md,
|
|
6050
|
-
node_ids: created
|
|
6083
|
+
node_ids: created,
|
|
6084
|
+
skipped,
|
|
6085
|
+
failed
|
|
6051
6086
|
},
|
|
6052
6087
|
201
|
|
6053
6088
|
);
|
|
@@ -6058,30 +6093,6 @@ import { Hono as Hono12 } from "hono";
|
|
|
6058
6093
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6059
6094
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
6060
6095
|
|
|
6061
|
-
// src/shared/node-id.ts
|
|
6062
|
-
function assertSafeNodeId(rawId) {
|
|
6063
|
-
let id = rawId;
|
|
6064
|
-
try {
|
|
6065
|
-
id = decodeURIComponent(rawId);
|
|
6066
|
-
} catch {
|
|
6067
|
-
}
|
|
6068
|
-
if (!id || id.length > 512) {
|
|
6069
|
-
throw new ValidationError("invalid node id");
|
|
6070
|
-
}
|
|
6071
|
-
if (id.includes("\0") || id.includes("\\")) {
|
|
6072
|
-
throw new ValidationError("invalid node id");
|
|
6073
|
-
}
|
|
6074
|
-
if (id.startsWith("/")) {
|
|
6075
|
-
throw new ValidationError("invalid node id (absolute path)");
|
|
6076
|
-
}
|
|
6077
|
-
for (const seg of id.split("/")) {
|
|
6078
|
-
if (seg === "" || seg === "." || seg === "..") {
|
|
6079
|
-
throw new ValidationError("invalid node id (path traversal)");
|
|
6080
|
-
}
|
|
6081
|
-
}
|
|
6082
|
-
return id;
|
|
6083
|
-
}
|
|
6084
|
-
|
|
6085
6096
|
// src/telemetry/trace-log.ts
|
|
6086
6097
|
var PRUNE_EVERY = 500;
|
|
6087
6098
|
var insertsSincePrune = 0;
|
|
@@ -6385,6 +6396,30 @@ async function appendRunStep(runId, input) {
|
|
|
6385
6396
|
return { seq };
|
|
6386
6397
|
}
|
|
6387
6398
|
var RUN_STATUSES = ["running", "succeeded", "failed", "cancelled"];
|
|
6399
|
+
async function cancelDescendants(runId) {
|
|
6400
|
+
const db = getDb();
|
|
6401
|
+
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
6402
|
+
const trace = JSON.stringify({ error: "Parent run cancelled" });
|
|
6403
|
+
let frontier = [runId];
|
|
6404
|
+
let cancelled = 0;
|
|
6405
|
+
for (let depth = 0; frontier.length && depth < 64; depth++) {
|
|
6406
|
+
const ph = frontier.map(() => "?").join(",");
|
|
6407
|
+
const kids = await db.all(
|
|
6408
|
+
`SELECT id FROM runs WHERE parent_run_id IN (${ph}) AND status = 'running'`,
|
|
6409
|
+
frontier
|
|
6410
|
+
);
|
|
6411
|
+
if (!kids.length) break;
|
|
6412
|
+
frontier = kids.map((k) => k.id);
|
|
6413
|
+
const kph = frontier.map(() => "?").join(",");
|
|
6414
|
+
await db.run(
|
|
6415
|
+
`UPDATE runs SET status = 'cancelled', trace = COALESCE(trace, ?), finished_at = ?
|
|
6416
|
+
WHERE id IN (${kph}) AND status = 'running'`,
|
|
6417
|
+
[trace, at, ...frontier]
|
|
6418
|
+
);
|
|
6419
|
+
cancelled += frontier.length;
|
|
6420
|
+
}
|
|
6421
|
+
return cancelled;
|
|
6422
|
+
}
|
|
6388
6423
|
async function closeRun(runId, input) {
|
|
6389
6424
|
const status = input.status;
|
|
6390
6425
|
if (typeof status !== "string" || !RUN_STATUSES.includes(status)) {
|
|
@@ -6406,6 +6441,7 @@ async function closeRun(runId, input) {
|
|
|
6406
6441
|
if (!run) throw new NotFoundError("Run not found");
|
|
6407
6442
|
throw new ConflictError("Run already terminal \u2014 status can't be changed");
|
|
6408
6443
|
}
|
|
6444
|
+
if (status === "cancelled") await cancelDescendants(runId);
|
|
6409
6445
|
return { status };
|
|
6410
6446
|
}
|
|
6411
6447
|
async function getRunTrace(run, opts) {
|
|
@@ -7656,6 +7692,13 @@ async function handleToolCall(toolName, args, ctx) {
|
|
|
7656
7692
|
try {
|
|
7657
7693
|
return await runTool(toolName, args, ctx);
|
|
7658
7694
|
} catch (err) {
|
|
7695
|
+
if (isVaultLockTimeout(err) || err instanceof VaultBusyError) {
|
|
7696
|
+
status = 503;
|
|
7697
|
+
return toolError(
|
|
7698
|
+
"The vault is busy with another write. Retry in a few seconds.",
|
|
7699
|
+
"VAULT_BUSY"
|
|
7700
|
+
);
|
|
7701
|
+
}
|
|
7659
7702
|
status = 500;
|
|
7660
7703
|
throw err;
|
|
7661
7704
|
} finally {
|
|
@@ -7709,7 +7752,9 @@ ${context_md}`;
|
|
|
7709
7752
|
{},
|
|
7710
7753
|
await opContext(nestId, userEmail)
|
|
7711
7754
|
);
|
|
7712
|
-
const visible = await listNodesForCallerByEmail(nestId, userEmail, {
|
|
7755
|
+
const visible = await listNodesForCallerByEmail(nestId, userEmail, {
|
|
7756
|
+
withContent: true
|
|
7757
|
+
});
|
|
7713
7758
|
const types = {};
|
|
7714
7759
|
const tags = {};
|
|
7715
7760
|
for (const n of visible) {
|
|
@@ -8544,14 +8589,14 @@ ${list}`,
|
|
|
8544
8589
|
if (!target) return "target is required (a node id or folder prefix).";
|
|
8545
8590
|
if (!["read", "write"].includes(role)) return "role must be read or write.";
|
|
8546
8591
|
try {
|
|
8547
|
-
const { createGrant: createGrant2 } = await import("./grants-service-
|
|
8548
|
-
const db = (await import("./client-
|
|
8592
|
+
const { createGrant: createGrant2 } = await import("./grants-service-CADI6LIB.js");
|
|
8593
|
+
const db = (await import("./client-SW5I6ZBK.js")).getDb();
|
|
8549
8594
|
const { normalizeEmail: normalizeEmail2 } = await import("./email-R7DFS6E5.js");
|
|
8550
8595
|
const e = normalizeEmail2(String(args.email || ""));
|
|
8551
8596
|
if (!e) return "email is required.";
|
|
8552
8597
|
let row = await db.get("SELECT id FROM users WHERE LOWER(email) = ?", [e]);
|
|
8553
8598
|
if (!row) {
|
|
8554
|
-
const { hashPassword: hashPassword2 } = await import("./keys-
|
|
8599
|
+
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
8555
8600
|
const { v4: uuid16 } = await import("uuid");
|
|
8556
8601
|
const id = uuid16();
|
|
8557
8602
|
await db.run(
|
|
@@ -9014,8 +9059,9 @@ runDetailRoutes.patch("/:id", requireWorkflowPlane, async (c) => {
|
|
|
9014
9059
|
throw new ValidationError("status must be running | succeeded | failed | cancelled");
|
|
9015
9060
|
}
|
|
9016
9061
|
const terminal = status !== "running";
|
|
9017
|
-
await getDb().run(
|
|
9018
|
-
`UPDATE runs SET status = ?, trace = COALESCE(?, trace), finished_at = ?
|
|
9062
|
+
const res = await getDb().run(
|
|
9063
|
+
`UPDATE runs SET status = ?, trace = COALESCE(?, trace), finished_at = ?
|
|
9064
|
+
WHERE id = ? AND status = 'running'`,
|
|
9019
9065
|
[
|
|
9020
9066
|
status,
|
|
9021
9067
|
body.trace === void 0 ? null : JSON.stringify(body.trace),
|
|
@@ -9023,6 +9069,13 @@ runDetailRoutes.patch("/:id", requireWorkflowPlane, async (c) => {
|
|
|
9023
9069
|
run.id
|
|
9024
9070
|
]
|
|
9025
9071
|
);
|
|
9072
|
+
if (!res.changes) {
|
|
9073
|
+
const now = await getDb().get("SELECT status FROM runs WHERE id = ?", [
|
|
9074
|
+
run.id
|
|
9075
|
+
]);
|
|
9076
|
+
throw new ConflictError(`Run already terminal (${now?.status ?? "closed"})`);
|
|
9077
|
+
}
|
|
9078
|
+
if (status === "cancelled") await cancelDescendants(run.id);
|
|
9026
9079
|
if (status === "failed") {
|
|
9027
9080
|
const err = body.trace && typeof body.trace === "object" ? String(body.trace.error ?? "").slice(0, 200) : "";
|
|
9028
9081
|
void dispatchEvent({
|
|
@@ -9631,8 +9684,8 @@ definitionRoutes.delete("/:id", async (c) => {
|
|
|
9631
9684
|
// src/nests/asset-routes.ts
|
|
9632
9685
|
import { Hono as Hono18 } from "hono";
|
|
9633
9686
|
import { v4 as uuid13 } from "uuid";
|
|
9634
|
-
import { mkdir as mkdir2, readFile
|
|
9635
|
-
import { join as
|
|
9687
|
+
import { mkdir as mkdir2, readFile, writeFile } from "fs/promises";
|
|
9688
|
+
import { join as join4 } from "path";
|
|
9636
9689
|
var assetRoutes = new Hono18();
|
|
9637
9690
|
var CONTENT_TYPES = {
|
|
9638
9691
|
png: "image/png",
|
|
@@ -9680,9 +9733,9 @@ assetRoutes.post("/", async (c) => {
|
|
|
9680
9733
|
);
|
|
9681
9734
|
}
|
|
9682
9735
|
const name = `${uuid13()}.${ext === "jpeg" ? "jpg" : ext}`;
|
|
9683
|
-
const dir =
|
|
9736
|
+
const dir = join4(resolveNestPath(nestId), "assets");
|
|
9684
9737
|
await mkdir2(dir, { recursive: true });
|
|
9685
|
-
await writeFile(
|
|
9738
|
+
await writeFile(join4(dir, name), Buffer.from(await file.arrayBuffer()));
|
|
9686
9739
|
const url = `/nests/${nestId}/assets/${name}`;
|
|
9687
9740
|
return c.json(
|
|
9688
9741
|
{
|
|
@@ -9704,7 +9757,7 @@ assetRoutes.get("/:file", async (c) => {
|
|
|
9704
9757
|
}
|
|
9705
9758
|
let bytes;
|
|
9706
9759
|
try {
|
|
9707
|
-
bytes = await
|
|
9760
|
+
bytes = await readFile(join4(resolveNestPath(nestId), "assets", name));
|
|
9708
9761
|
} catch {
|
|
9709
9762
|
throw new NotFoundError("Asset not found");
|
|
9710
9763
|
}
|
|
@@ -10514,7 +10567,7 @@ function rowToComment(row) {
|
|
|
10514
10567
|
|
|
10515
10568
|
// src/governance/stewards-parser.ts
|
|
10516
10569
|
import { readFileSync as readFileSync3, existsSync } from "fs";
|
|
10517
|
-
import { join as
|
|
10570
|
+
import { join as join5 } from "path";
|
|
10518
10571
|
function parseStewardsYaml(content) {
|
|
10519
10572
|
const result = { version: 1 };
|
|
10520
10573
|
const lines = content.split("\n");
|
|
@@ -10580,9 +10633,9 @@ function parseEntry(str) {
|
|
|
10580
10633
|
function loadStewardsConfig(nestId) {
|
|
10581
10634
|
const nestPath = resolveNestPath(nestId);
|
|
10582
10635
|
const candidates = [
|
|
10583
|
-
|
|
10584
|
-
|
|
10585
|
-
|
|
10636
|
+
join5(nestPath, "stewards.yaml"),
|
|
10637
|
+
join5(nestPath, "stewards.yml"),
|
|
10638
|
+
join5(nestPath, ".context", "stewards.yaml")
|
|
10586
10639
|
];
|
|
10587
10640
|
for (const candidatePath of candidates) {
|
|
10588
10641
|
if (existsSync(candidatePath)) {
|
|
@@ -10943,7 +10996,7 @@ governanceNodeRoutes.get("/:nodeId{.+}/versions", async (c) => {
|
|
|
10943
10996
|
const approved = await getApprovedVersion(nestId, nodeId);
|
|
10944
10997
|
const { versions: versionManager } = await engineCache.get(nestId);
|
|
10945
10998
|
const resolutions = await getDb().all(
|
|
10946
|
-
`SELECT version, status, resolved_by
|
|
10999
|
+
`SELECT version, status, resolved_by, resolution_note
|
|
10947
11000
|
FROM review_requests
|
|
10948
11001
|
WHERE nest_id = ? AND node_id = ?
|
|
10949
11002
|
AND status IN ('approved', 'rejected')
|
|
@@ -10954,13 +11007,21 @@ governanceNodeRoutes.get("/:nodeId{.+}/versions", async (c) => {
|
|
|
10954
11007
|
const byVersion = /* @__PURE__ */ new Map();
|
|
10955
11008
|
for (const r of resolutions) {
|
|
10956
11009
|
if (!byVersion.has(r.version)) {
|
|
10957
|
-
byVersion.set(r.version, {
|
|
11010
|
+
byVersion.set(r.version, {
|
|
11011
|
+
status: r.status,
|
|
11012
|
+
resolvedBy: r.resolved_by,
|
|
11013
|
+
resolutionNote: r.resolution_note?.trim() || void 0
|
|
11014
|
+
});
|
|
10958
11015
|
}
|
|
10959
11016
|
}
|
|
10960
11017
|
const enriched = await Promise.all(
|
|
10961
11018
|
allVersions.map(async (v) => {
|
|
10962
11019
|
const r = byVersion.get(v.version);
|
|
10963
|
-
const resolution = r ? {
|
|
11020
|
+
const resolution = r ? {
|
|
11021
|
+
resolvedBy: r.resolvedBy,
|
|
11022
|
+
resolutionStatus: r.status,
|
|
11023
|
+
resolutionNote: r.resolutionNote
|
|
11024
|
+
} : {};
|
|
10964
11025
|
try {
|
|
10965
11026
|
const diff = await versionManager.getDiff(nodeId, v.version);
|
|
10966
11027
|
return { ...v, ...resolution, diff: diff ?? void 0 };
|
|
@@ -11338,19 +11399,19 @@ async function ensureAnonymousUser() {
|
|
|
11338
11399
|
// src/app.ts
|
|
11339
11400
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
11340
11401
|
import { fileURLToPath } from "url";
|
|
11341
|
-
import { dirname as dirname2, join as
|
|
11402
|
+
import { dirname as dirname2, join as join6, relative as relative2 } from "path";
|
|
11342
11403
|
import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
|
|
11343
11404
|
var HERE = dirname2(fileURLToPath(import.meta.url));
|
|
11344
11405
|
var SERVICE_VERSION = (() => {
|
|
11345
11406
|
try {
|
|
11346
|
-
return JSON.parse(readFileSync4(
|
|
11407
|
+
return JSON.parse(readFileSync4(join6(HERE, "..", "package.json"), "utf8")).version;
|
|
11347
11408
|
} catch {
|
|
11348
11409
|
return "unknown";
|
|
11349
11410
|
}
|
|
11350
11411
|
})();
|
|
11351
11412
|
var UI_DIR_CANDIDATES = [
|
|
11352
|
-
|
|
11353
|
-
|
|
11413
|
+
join6(HERE, "web3"),
|
|
11414
|
+
join6(process.cwd(), "dist", "web3")
|
|
11354
11415
|
];
|
|
11355
11416
|
var UI_DIR_ABS = UI_DIR_CANDIDATES.find((p) => existsSync2(p)) || UI_DIR_CANDIDATES[0];
|
|
11356
11417
|
var UI_DIR_REL = relative2(process.cwd(), UI_DIR_ABS) || ".";
|
|
@@ -12508,6 +12569,12 @@ function createApp() {
|
|
|
12508
12569
|
);
|
|
12509
12570
|
app.notFound((c) => c.json({ error: "Not found" }, 404));
|
|
12510
12571
|
app.onError((err, c) => {
|
|
12572
|
+
if (isVaultLockTimeout(err) || err instanceof VaultBusyError) {
|
|
12573
|
+
const busy = err instanceof VaultBusyError ? err : new VaultBusyError();
|
|
12574
|
+
return c.json({ error: busy.message }, 503, {
|
|
12575
|
+
"Retry-After": String(busy.retryAfterSeconds)
|
|
12576
|
+
});
|
|
12577
|
+
}
|
|
12511
12578
|
if (err instanceof AppError) {
|
|
12512
12579
|
return c.json({ error: err.message }, err.statusCode);
|
|
12513
12580
|
}
|