lib/project-knowledge.js
Rama main · réplica de lectura del repositorio original.
var path = require("path");
var crypto = require("crypto");
var mateSync = require("./mate-knowledge-sync");
var knowledgeFiles = require("./knowledge-file-store");
var markdown = require("./knowledge-markdown");
var draftStore = require("./knowledge-draft-store");
var documentIndexModule = require("./knowledge-document-index");
var knowledgeDbModule = require("./knowledge-db-service");
var knowledgeImport = require("./knowledge-import");
function attachKnowledge(ctx) {
var cwd = ctx.cwd;
var isMate = ctx.isMate;
var sendTo = ctx.sendTo;
var matesModule = ctx.matesModule;
var getProjectOwnerId = ctx.getProjectOwnerId;
var metadataKey = crypto.createHash("sha256").update(path.resolve(cwd)).digest("hex").slice(0, 24);
var metadataRoot = path.join(ctx.knowledgeBaseDir || path.join(cwd, ".clay"), "knowledge-documents", metadataKey);
var files = knowledgeFiles.createKnowledgeFileStore(cwd, { metaDir: metadataRoot });
var drafts = draftStore.createKnowledgeDraftStore(ctx.knowledgeBaseDir || path.join(cwd, ".clay"), path.resolve(cwd));
var indexer = documentIndexModule.createKnowledgeDocumentIndex({ list: listKnowledgeFiles, read: readEntry, maxDocumentBytes: knowledgeFiles.MAX_CONTENT_BYTES });
var dbScope = isMate ? mateSync.scopeForMateDir(cwd) : null;
var dbImporter = dbScope ? knowledgeImport.createKnowledgeImporter({ scopeId: dbScope.scopeId, baseDir: ctx.knowledgeBaseDir }) : null;
var databases = isMate ? knowledgeDbModule.createKnowledgeDbService({
projectDir: cwd,
authorizeTextReference: function (id) { return files.list().some(function (file) { return file.id === id && file.itemType === "text"; }); },
projectSummary: dbImporter ? function (summary) {
return dbImporter.importRecord({ importKey: "knowledge-database:" + summary.databaseId, sourceHash: String(summary.revision) + ":" + String(summary.archived),
kind: "knowledge-database-summary", name: summary.name, content: summary.content,
source: { namespace: "knowledge-database", databaseId: summary.databaseId, relPath: "knowledge/.clay-db/databases.db" }, actor: summary.actor });
} : null,
}) : null;
var databaseReady = databases && databases.hasStore() ? databases.initialize() : null;
if (databaseReady) databaseReady.catch(function (error) { console.error("[knowledge-db] Failed to initialize:", error.message); });
function canWrite(ws) {
if (typeof ctx.canManageKnowledge === "function") return ctx.canManageKnowledge(ws) === true;
var ownerId = getProjectOwnerId();
return !ownerId || !!(ws && ws._clayUser && ws._clayUser.id === ownerId);
}
function actor(ws) {
var user = (ws && ws._clayUser) || null;
return { type: user ? "user" : "system", userId: user ? user.id : null, displayName: user ? (user.displayName || user.username) : null };
}
function mirrorActor(who, name) {
if (!isMate) return { ok: true, names: [] };
var summary = mateSync.syncMateSource({ mateDir: cwd, fileName: name, baseDir: ctx.knowledgeBaseDir, actor: who });
return { ok: !!summary && summary.failed === 0, names: [name], error: summary && summary.errors[0] ? summary.errors[0].message : "Knowledge index synchronization failed." };
}
function mirror(ws, name) { return mirrorActor(actor(ws), name); }
function mirrorManyActor(who, names) {
var failures = [];
for (var i = 0; i < names.length; i++) {
var mirrored = mirrorActor(who, names[i]);
if (!mirrored.ok) failures.push(mirrored.error);
}
return { ok: failures.length === 0, names: names.slice(), error: failures[0] || null };
}
function mirrorMany(ws, names) { return mirrorManyActor(actor(ws), names); }
function controlMirror(who, names) {
try { var state = mirrorManyActor(who, names); return { indexSyncPending: !state.ok, syncNames: names, syncError: state.ok ? null : state.error }; }
catch (error) { return { indexSyncPending: true, syncNames: names, syncError: error.message }; }
}
function addSyncState(payload, sync) {
if (sync.ok) return payload;
return Object.assign(payload, { indexSyncPending: true, syncNames: sync.names, syncError: sync.error });
}
function listKnowledgeFiles() {
var listed = files.list();
for (var i = 0; i < listed.length; i++) listed[i].common = false;
if (!isMate) return listed;
var mateCtx = matesModule.buildMateCtx(getProjectOwnerId());
var thisMateId = path.basename(cwd);
for (var p = 0; p < listed.length; p++) listed[p].promoted = matesModule.isPromoted(mateCtx, thisMateId, listed[p].name);
var common = matesModule.getCommonKnowledgeForMate(mateCtx, thisMateId);
for (var c = 0; c < common.length; c++) {
if (common[c].ownMateId !== thisMateId) listed.push(Object.assign({}, common[c], { id: "shared:" + common[c].ownMateId + ":" + common[c].name,
itemType: /\.jsonl$/i.test(common[c].name) ? "db" : "text", writable: false, revision: null }));
}
return listed;
}
function listKnowledgeItems(ws) {
var textItems = listKnowledgeFiles();
if (!databases || !databases.hasStore()) return textItems;
function combine(result) { return textItems.concat(result.databases.map(function (item) { return Object.assign({}, item, { writable: canWrite(ws), common: false }); })); }
if (databases.isReady()) return combine(databases.listCached({}));
return databaseReady.then(function () { return combine(databases.listCached({})); });
}
function response(ws, msg, payload) {
sendTo(ws, Object.assign({ requestId: msg.requestId || null }, payload));
}
function failure(ws, msg, error, extra) {
response(ws, msg, Object.assign({ type: "knowledge_error", operation: msg.type, name: msg.name || msg.from || null,
code: error.code || "FAILED", error: error.message || "Knowledge operation failed.", currentRevision: error.currentRevision || null }, extra || {}));
}
function requireWrite(ws, msg) {
if (canWrite(ws)) return true;
failure(ws, msg, Object.assign(new Error("Only the Mate owner can change Knowledge documents."), { code: "FORBIDDEN" }));
return false;
}
function readEntry(file) {
if (file.common && file.ownMateId && isMate) {
var mateCtx = matesModule.buildMateCtx(getProjectOwnerId());
return matesModule.readCommonKnowledgeFile(mateCtx, file.ownMateId, file.name);
}
return files.read(file.name).content;
}
function documentIndex() {
return indexer.reconcile();
}
function findDocument(index, name, id) {
var matches;
if (id) { matches = index.filter(function (item) { return item.id === id; }); return matches.length === 1 ? matches[0] : null; }
matches = index.filter(function (item) { return item.name === name; });
return matches.length === 1 ? matches[0] : null;
}
function linksFor(index, target) {
var outgoing = []; var backlinks = [];
for (var i = 0; i < target.parsed.links.length; i++) {
var link = target.parsed.links[i]; var resolved = markdown.resolveLink(target.name, link, index, target.id);
var headingValid = !link.heading || !!(resolved && resolved.parsed.headings.some(function (heading) { return heading.slug === markdown.slug(link.heading); }));
outgoing.push({ kind: link.kind, raw: link.raw, label: link.alias || link.target || link.heading, target: link.target, heading: link.heading,
headingValid: headingValid, documentId: resolved && resolved.id || null, documentKey: resolved && resolved.key || null,
name: resolved && resolved.name || null, external: link.external === true, self: !link.target && !!link.heading, unresolved: !resolved && !link.external });
}
for (var d = 0; d < index.length; d++) {
if (index[d] === target) continue;
for (var l = 0; l < index[d].parsed.links.length; l++) {
var resolvedTarget = markdown.resolveLink(index[d].name, index[d].parsed.links[l], index, index[d].id);
if (resolvedTarget && resolvedTarget.id === target.id) {
backlinks.push({ documentId: index[d].id, documentKey: index[d].key, name: index[d].name, heading: index[d].parsed.links[l].heading,
snippet: markdown.snippet(index[d].content, index[d].parsed.links[l].start) }); break;
}
}
}
return { outgoing: outgoing, backlinks: backlinks };
}
function relativeLink(from, to, withExtension) {
var value = path.posix.relative(path.posix.dirname(from), to) || path.posix.basename(to);
if (withExtension === false) value = value.replace(/\.md$/i, "");
return value;
}
function rewriteMoveLinks(index, target, nextName) {
var changes = [];
for (var i = 0; i < index.length; i++) {
var source = index[i];
if (source.common || source.itemType === "db" || !source.writable) continue;
var replacements = [];
for (var l = 0; l < source.parsed.links.length; l++) {
var link = source.parsed.links[l]; var resolved = markdown.resolveLink(source.name, link, index, source.id);
if (!resolved || source.id !== target.id && resolved.id !== target.id) continue;
var sourceAfter = source.id === target.id ? nextName : source.name;
var targetAfter = resolved.id === target.id ? nextName : resolved.name;
var nextTarget = relativeLink(sourceAfter, targetAfter, link.kind === "markdown" || link.kind === "image");
var suffix = link.heading ? "#" + link.heading : ""; var replacement;
if (link.kind === "wiki" || link.kind === "embed") replacement = (link.kind === "embed" ? "!" : "") + "[[" + nextTarget + suffix + (link.alias ? "|" + link.alias : "") + "]]";
else replacement = (link.kind === "image" ? "!" : "") + "[" + link.alias + "](" + nextTarget + suffix + ")";
replacements.push({ start: link.start, end: link.end, value: replacement });
}
if (!replacements.length) continue;
var content = source.content;
for (var r = replacements.length - 1; r >= 0; r--) content = content.slice(0, replacements[r].start) + replacements[r].value + content.slice(replacements[r].end);
changes.push({ id: source.id, name: source.name, revision: source.revision, content: content, count: replacements.length });
}
return changes;
}
function searchFiles(query, offset, limit) {
var start = Math.max(0, Number(offset) || 0); var count = Math.max(1, Math.min(100, Number(limit) || 50)); var direct = indexer.search(query, start, count); var needle = String(query || "").trim().toLowerCase();
if (!databases || !databases.hasStore()) return direct;
function combineSearch(result) {
var databaseMatches = result.databases.filter(function (item) { return needle && (item.name.toLowerCase().indexOf(needle) !== -1 || item.schema.some(function (field) { return field.name.toLowerCase().indexOf(needle) !== -1 || field.type.indexOf(needle) !== -1; })); }).map(function (item) { return Object.assign({}, item, { itemType: "db", writable: true, common: false, snippet: "Database · " + item.recordCount + " records" }); });
if (!databaseMatches.length) return direct;
var files = direct.files.slice(); var textTotal = direct.total; var databaseOffset = Math.max(0, start - textTotal); var room = count - files.length;
if (room > 0) files = files.concat(databaseMatches.slice(databaseOffset, databaseOffset + room));
return { files: files, total: textTotal + databaseMatches.length, offset: start, limit: count, complete: direct.complete, errors: direct.errors };
}
if (databases.isReady()) return combineSearch(databases.listCached({}));
return databaseReady.then(function () { return combineSearch(databases.listCached({})); });
}
function renameDocument(msg, who) {
var beforeMoveState = documentIndex(); var beforeMove = beforeMoveState.documents; var moveTarget = findDocument(beforeMove, msg.from, msg.documentId); var linkChanges = moveTarget ? rewriteMoveLinks(beforeMove, moveTarget, msg.name) : [];
var renamed = files.rename(msg.from, msg.name, msg.expectedRevision || null); var linkUpdateFailures = []; var updatedLinks = 0;
var updatedDocuments = []; var mirrorNames = [msg.from, renamed.name];
for (var u = 0; u < linkChanges.length; u++) {
var change = linkChanges[u]; var writeName = change.id === renamed.id ? renamed.name : change.name; var expected = change.id === renamed.id ? renamed.revision : change.revision;
try { var rewritten = files.save(writeName, change.content, expected); updatedLinks += change.count; updatedDocuments.push(rewritten); mirrorNames.push(rewritten.name); }
catch (linkError) { linkUpdateFailures.push({ name: writeName, error: linkError.message }); }
}
renamed = files.read(renamed.name);
var uniqueMirrorNames = mirrorNames.filter(function (name, at) { return mirrorNames.indexOf(name) === at; }); var renameSync = mirrorManyActor(who, uniqueMirrorNames);
return addSyncState({ type: "knowledge_renamed", id: renamed.id, from: msg.from, name: renamed.name, revision: renamed.revision,
content: renamed.content, updatedLinks: updatedLinks, linkUpdateFailures: linkUpdateFailures,
updatedDocuments: updatedDocuments.map(function (item) { return { id: item.id, name: item.name, content: item.content, revision: item.revision }; }) }, renameSync);
}
function handleKnowledgeMessage(ws, msg) {
if (msg.type === "knowledge_list") {
try { var listed = listKnowledgeItems(ws); if (listed && typeof listed.then === "function") listed.then(function (items) { response(ws, msg, { type: "knowledge_list", files: items, drafts: drafts.list(ws && ws._clayUser && ws._clayUser.id || "system") }); }).catch(function (error) { failure(ws, msg, error); }); else response(ws, msg, { type: "knowledge_list", files: listed, drafts: drafts.list(ws && ws._clayUser && ws._clayUser.id || "system") }); } catch (error) { failure(ws, msg, error); }
return true;
}
if (msg.type === "knowledge_search") {
try { var searched = searchFiles(msg.query, msg.offset, msg.limit); if (searched && typeof searched.then === "function") searched.then(function (result) { response(ws, msg, Object.assign({ type: "knowledge_search_results", query: String(msg.query || "") }, result)); }).catch(function (error) { failure(ws, msg, error); }); else response(ws, msg, Object.assign({ type: "knowledge_search_results", query: String(msg.query || "") }, searched)); } catch (error) { failure(ws, msg, error); }
return true;
}
if (msg.type === "knowledge_read") {
try {
if (msg.common && msg.ownMateId && isMate) {
if (!knowledgeFiles.validName(msg.name)) throw new Error("Invalid knowledge file name.");
var mateCtx = matesModule.buildMateCtx(getProjectOwnerId());
var sharedContent = matesModule.readCommonKnowledgeFile(mateCtx, msg.ownMateId, msg.name);
var sharedParsed = markdown.parse(sharedContent); var sharedIndexState = documentIndex();
var sharedIndexed = findDocument(sharedIndexState.documents, msg.name, msg.documentId); var sharedRelations = sharedIndexed ? linksFor(sharedIndexState.documents, sharedIndexed) : { outgoing: [], backlinks: [] };
response(ws, msg, { type: "knowledge_content", name: msg.name, content: sharedContent, common: true, ownMateId: msg.ownMateId, writable: false, revision: null,
outline: sharedParsed.headings, outgoing: sharedRelations.outgoing, backlinks: sharedRelations.backlinks,
properties: sharedParsed.properties, tags: sharedParsed.tags, propertyErrors: sharedParsed.propertyErrors,
indexComplete: sharedIndexState.complete, indexErrors: sharedIndexState.errors });
} else {
var item = files.read(msg.name);
var indexState = documentIndex(); var indexed = findDocument(indexState.documents, item.name, msg.documentId || item.id); var relations = indexed ? linksFor(indexState.documents, indexed) : { outgoing: [], backlinks: [] };
var parsed = indexed ? indexed.parsed : markdown.parse(item.content);
var userId = ws && ws._clayUser && ws._clayUser.id || "system"; var recovered = drafts.read(userId, item.id);
response(ws, msg, { type: "knowledge_content", id: item.id, name: item.name, content: item.content, revision: item.revision, itemType: item.itemType,
writable: item.itemType === "text" && canWrite(ws), outline: parsed.headings, outgoing: relations.outgoing, backlinks: relations.backlinks,
properties: parsed.properties, tags: parsed.tags, propertyErrors: parsed.propertyErrors, recoveredDraft: recovered,
indexComplete: indexState.complete, indexErrors: indexState.errors });
}
} catch (error) { failure(ws, msg, error); }
return true;
}
if (msg.type === "knowledge_save") {
if (!requireWrite(ws, msg)) return true;
try {
var saved = files.save(msg.name, msg.content, msg.expectedRevision || null);
var savedSync = mirror(ws, saved.name);
if (msg.documentId) drafts.remove(ws._clayUser && ws._clayUser.id || "system", msg.documentId, msg.draftVersion);
response(ws, msg, addSyncState({ type: "knowledge_saved", id: saved.id, name: saved.name, revision: saved.revision }, savedSync));
sendTo(ws, { type: "knowledge_list", files: listKnowledgeFiles() });
} catch (error) { failure(ws, msg, error); }
return true;
}
if (msg.type === "knowledge_rename") {
if (!requireWrite(ws, msg)) return true;
try {
response(ws, msg, renameDocument(msg, actor(ws)));
sendTo(ws, { type: "knowledge_list", files: listKnowledgeFiles() });
} catch (error) { failure(ws, msg, error); }
return true;
}
if (msg.type === "knowledge_delete") {
if (!requireWrite(ws, msg)) return true;
try {
var removed = files.remove(msg.name, msg.expectedRevision || null);
var deleteSync = mirror(ws, removed.name);
response(ws, msg, addSyncState({ type: "knowledge_deleted", name: removed.name }, deleteSync));
sendTo(ws, { type: "knowledge_list", files: listKnowledgeFiles() });
} catch (error) { failure(ws, msg, error); }
return true;
}
if (msg.type === "knowledge_sync") {
if (!requireWrite(ws, msg)) return true;
try {
var syncNames = Array.isArray(msg.names) ? msg.names.slice(0, 4) : [msg.name];
if (!syncNames.length) throw new Error("A Knowledge source is required.");
for (var n = 0; n < syncNames.length; n++) if (!knowledgeFiles.validName(syncNames[n])) throw new Error("Invalid knowledge file name.");
var retrySync = mirrorMany(ws, syncNames);
if (!retrySync.ok) throw Object.assign(new Error(retrySync.error), { code: "SYNC_FAILED" });
response(ws, msg, { type: "knowledge_synced", names: syncNames });
} catch (error) { failure(ws, msg, error, { diskCommitted: true, names: msg.names || [msg.name] }); }
return true;
}
if (msg.type === "knowledge_draft_save") {
if (!requireWrite(ws, msg)) return true;
try {
var savedDraft = drafts.save(ws._clayUser && ws._clayUser.id || "system", msg.documentId, { name: msg.name, content: msg.content, baseRevision: msg.baseRevision,
cursor: msg.cursor, clientVersion: msg.clientVersion, newDocument: msg.newDocument });
response(ws, msg, { type: "knowledge_draft_saved", documentId: msg.documentId, savedAt: savedDraft.savedAt, clientVersion: savedDraft.clientVersion });
} catch (error) { failure(ws, msg, error); }
return true;
}
if (msg.type === "knowledge_draft_delete") {
if (!requireWrite(ws, msg)) return true;
try { var removedDraft = drafts.remove(ws._clayUser && ws._clayUser.id || "system", msg.documentId, msg.throughVersion); response(ws, msg, { type: "knowledge_draft_deleted", documentId: msg.documentId, removed: removedDraft }); }
catch (error) { failure(ws, msg, error); }
return true;
}
if (msg.type === "knowledge_discard_draft") {
if (!requireWrite(ws, msg)) return true;
try {
var disk = files.read(msg.name); if (msg.expectedRevision && disk.revision !== msg.expectedRevision) throw Object.assign(new Error("The disk version changed again."), { code: "STALE", currentRevision: disk.revision });
var removed = drafts.remove(ws._clayUser && ws._clayUser.id || "system", msg.documentId, msg.throughVersion);
response(ws, msg, { type: "knowledge_draft_discarded", documentId: msg.documentId, removed: removed, id: disk.id, name: disk.name, content: disk.content, revision: disk.revision });
} catch (error) { failure(ws, msg, error); }
return true;
}
if (msg.type === "knowledge_history") {
try { response(ws, msg, { type: "knowledge_history", documentId: msg.documentId, versions: files.history(msg.documentId) }); }
catch (error) { failure(ws, msg, error); }
return true;
}
if (msg.type === "knowledge_history_read") {
try { response(ws, msg, Object.assign({ type: "knowledge_history_version", documentId: msg.documentId }, files.readHistory(msg.documentId, msg.version))); }
catch (error) { failure(ws, msg, error); }
return true;
}
if (msg.type === "knowledge_deleted_list") {
if (!databases || !databases.hasStore()) { try { response(ws, msg, { type: "knowledge_deleted_list", files: files.deleted() }); } catch (error) { failure(ws, msg, error); } return true; }
databases.list({ archived: true }).then(function (result) { var archivedDatabases = result.databases.map(function (item) { return Object.assign({}, item, { itemType: "db", writable: canWrite(ws) }); }); response(ws, msg, { type: "knowledge_deleted_list", files: files.deleted().concat(archivedDatabases) }); }).catch(function (error) { failure(ws, msg, error); });
return true;
}
if (msg.type === "knowledge_restore") {
if (!requireWrite(ws, msg)) return true;
try {
var restored = files.restore(msg.documentId, msg.version, msg.name, msg.expectedRevision || null); var restoreSync = mirror(ws, restored.name);
response(ws, msg, addSyncState({ type: "knowledge_restored", id: restored.id, name: restored.name, revision: restored.revision }, restoreSync));
sendTo(ws, { type: "knowledge_list", files: listKnowledgeFiles() });
} catch (error) { failure(ws, msg, error); }
return true;
}
if (msg.type === "knowledge_promote" || msg.type === "knowledge_depromote") {
if (!isMate || !requireWrite(ws, msg)) return true;
try {
if (!knowledgeFiles.validName(msg.name)) throw new Error("Invalid knowledge file name.");
var promoteCtx = matesModule.buildMateCtx(getProjectOwnerId());
var mateId = path.basename(cwd);
if (msg.type === "knowledge_promote") {
var mate = matesModule.getMate(promoteCtx, mateId);
matesModule.promoteKnowledge(promoteCtx, mateId, mate && mate.name, msg.name);
} else matesModule.depromoteKnowledge(promoteCtx, mateId, msg.name);
response(ws, msg, { type: msg.type === "knowledge_promote" ? "knowledge_promoted" : "knowledge_depromoted", name: msg.name });
sendTo(ws, { type: "knowledge_list", files: listKnowledgeFiles() });
} catch (error) { failure(ws, msg, error); }
return true;
}
var dbOperations = {
knowledge_db_create: { method: "create", reply: "knowledge_db_created" }, knowledge_db_read: { method: "read", reply: "knowledge_db_content", read: true },
knowledge_db_rename: { method: "rename", reply: "knowledge_db_renamed" }, knowledge_db_archive: { method: "archive", reply: "knowledge_db_archived" },
knowledge_db_recover: { method: "recover", reply: "knowledge_db_recovered" }, knowledge_db_schema_save: { method: "updateSchema", reply: "knowledge_db_schema_saved" },
knowledge_db_view_save: { method: "saveView", reply: "knowledge_db_view_saved" }, knowledge_db_record_create: { method: "createRecord", reply: "knowledge_db_record_created" },
knowledge_db_record_read: { method: "readRecord", reply: "knowledge_db_record_content", read: true }, knowledge_db_record_update: { method: "updateRecord", reply: "knowledge_db_record_updated" },
knowledge_db_record_archive: { method: "archiveRecord", reply: "knowledge_db_record_archived" }, knowledge_db_record_recover: { method: "recoverRecord", reply: "knowledge_db_record_recovered" },
knowledge_db_query: { method: "query", reply: "knowledge_db_query_results", read: true }, knowledge_db_sync: { method: "retryProjection", reply: "knowledge_db_synced" },
};
if (dbOperations[msg.type]) {
if (!isMate) { failure(ws, msg, new Error("Knowledge databases are available only inside a Mate project.")); return true; }
var operation = dbOperations[msg.type]; if (!operation.read && !requireWrite(ws, msg)) return true;
var args = Object.assign({}, msg, { actor: actor(ws) }); delete args.type; delete args.requestId;
Promise.resolve(databases[operation.method](args)).then(function (result) { response(ws, msg, Object.assign({ type: operation.reply }, result)); }).catch(function (error) { failure(ws, msg, error, { databaseId: msg.databaseId || null, recordId: msg.recordId || null }); });
return true;
}
return false;
}
var productOperations = require('./knowledge-product-operations').createKnowledgeProductOperations({ files: files, mirror: controlMirror, rename: renameDocument });
return { productOperations: productOperations, handleKnowledgeMessage: handleKnowledgeMessage, listKnowledgeFiles: listKnowledgeFiles, databases: databases };
}
module.exports = { attachKnowledge: attachKnowledge };