@bbigbang/core 0.1.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/dist/config.js +380 -0
- package/dist/execution/executionDispatcher.js +3810 -0
- package/dist/main.js +90 -0
- package/dist/nodeEventHistory.js +206 -0
- package/dist/scheduler/dreamLogic.js +50 -0
- package/dist/scheduler/dreamScheduler.js +65 -0
- package/dist/services/agentFileAccessService.js +1913 -0
- package/dist/services/agentRuntimeCleanupBroker.js +62 -0
- package/dist/services/agentSkillsBroker.js +118 -0
- package/dist/services/agentSkillsService.js +83 -0
- package/dist/services/agentWorkspaceBroker.js +937 -0
- package/dist/services/agentWorkspaceService.js +70 -0
- package/dist/services/appVersion.js +14 -0
- package/dist/services/auth.js +586 -0
- package/dist/services/claudeControlBroker.js +154 -0
- package/dist/services/claudeTranscriptBroker.js +100 -0
- package/dist/services/claudeTranscriptService.js +359 -0
- package/dist/services/codexAppServerBroker.js +155 -0
- package/dist/services/codexTranscriptBroker.js +98 -0
- package/dist/services/codexTranscriptService.js +961 -0
- package/dist/services/droidMissionBroker.js +124 -0
- package/dist/services/droidMissionImporter.js +630 -0
- package/dist/services/droidModelOptions.js +165 -0
- package/dist/services/hubServerRegistrationService.js +268 -0
- package/dist/services/libraryManifest.js +43 -0
- package/dist/services/libraryScaffold.js +26 -0
- package/dist/services/libraryService.js +2263 -0
- package/dist/services/memoryService.js +386 -0
- package/dist/services/missionEvidence.js +377 -0
- package/dist/services/missionService.js +2361 -0
- package/dist/services/missionTrace.js +158 -0
- package/dist/services/nativeMissionBriefParser.js +120 -0
- package/dist/services/nativeMissionOrchestrator.js +2045 -0
- package/dist/services/nativeMissionReportGenerator.js +227 -0
- package/dist/services/nativeMissionValidationRunner.js +452 -0
- package/dist/services/nativeMissionWorkerBroker.js +190 -0
- package/dist/services/nodeRegistry.js +34 -0
- package/dist/services/nodeStateReconciler.js +97 -0
- package/dist/services/panelMediaScanner.js +119 -0
- package/dist/services/persistentRuntimeJsonlClient.js +153 -0
- package/dist/services/platformAgentPolicy.js +180 -0
- package/dist/services/platformAgentService.js +2041 -0
- package/dist/services/projectAccessResolver.js +93 -0
- package/dist/services/projectService.js +392 -0
- package/dist/services/resourceSpaceService.js +140 -0
- package/dist/services/scenarioRuntimeService.js +1130 -0
- package/dist/services/suggestedPlannerService.js +868 -0
- package/dist/services/workbenchGitBroker.js +161 -0
- package/dist/services/workbenchGitService.js +69 -0
- package/dist/services/workbenchInspectBroker.js +65 -0
- package/dist/services/workbenchNodePathService.js +79 -0
- package/dist/services/workbenchRegistryService.js +240 -0
- package/dist/services/workbenchRootService.js +181 -0
- package/dist/services/workbenchTerminalBroker.js +378 -0
- package/dist/services/workspaceRunOwnership.js +60 -0
- package/dist/services/workspaceScaffold.js +105 -0
- package/dist/services/workspaceSessionRuntimeService.js +576 -0
- package/dist/services/workspaceSessionService.js +245 -0
- package/dist/services/workspaceToolActionRunner.js +1582 -0
- package/dist/services/workspaceToolErrors.js +10 -0
- package/dist/services/workspaceToolExecutionUtils.js +895 -0
- package/dist/services/workspaceToolLatestStateProjector.js +91 -0
- package/dist/services/workspaceToolManifest.js +572 -0
- package/dist/services/workspaceToolMutationQueue.js +43 -0
- package/dist/services/workspaceToolPanelProjection.js +460 -0
- package/dist/services/workspaceToolPromotion.js +255 -0
- package/dist/services/workspaceToolPromotionState.js +224 -0
- package/dist/services/workspaceToolPublishDiagnostics.js +189 -0
- package/dist/services/workspaceToolPublishIdentityResolver.js +146 -0
- package/dist/services/workspaceToolReadModel.js +378 -0
- package/dist/services/workspaceToolRunLedger.js +239 -0
- package/dist/services/workspaceToolService.js +3067 -0
- package/dist/services/workspaceToolSnapshotPanelSync.js +293 -0
- package/dist/services/workspaceToolTerminalLifecycle.js +283 -0
- package/dist/services/workspaceToolTypes.js +1 -0
- package/dist/services/workspaceToolUploadMaterializer.js +228 -0
- package/dist/web/actionCardRoutes.js +129 -0
- package/dist/web/actionCards.js +469 -0
- package/dist/web/activationContext.js +684 -0
- package/dist/web/agentChannelGuards.js +48 -0
- package/dist/web/agentMentionCooldowns.js +32 -0
- package/dist/web/agentReminders.js +1668 -0
- package/dist/web/agentRuntimePresence.js +197 -0
- package/dist/web/agentSelfState.js +494 -0
- package/dist/web/agentTaskLinks.js +26 -0
- package/dist/web/agentVisibility.js +79 -0
- package/dist/web/assets.js +95 -0
- package/dist/web/channelActivationPrompt.js +395 -0
- package/dist/web/channelMemoryNotes.js +127 -0
- package/dist/web/channelMentions.js +10 -0
- package/dist/web/channelMessageSequences.js +19 -0
- package/dist/web/channelSubscriptions.js +26 -0
- package/dist/web/clearedTaskRoots.js +10 -0
- package/dist/web/collaborationPromptGuidance.js +36 -0
- package/dist/web/collaborationSurfaceState.js +140 -0
- package/dist/web/contextBundleRanking.js +154 -0
- package/dist/web/contextBundleResolver.js +488 -0
- package/dist/web/conversationBuiltinSkillRoots.js +50 -0
- package/dist/web/conversationControls.js +232 -0
- package/dist/web/conversationHandoffs.js +612 -0
- package/dist/web/conversationManager.js +2511 -0
- package/dist/web/conversationSummaries.js +876 -0
- package/dist/web/conversationSurfaceKinds.js +17 -0
- package/dist/web/conversationTargets.js +173 -0
- package/dist/web/directActivationPrompt.js +122 -0
- package/dist/web/directReplyTargets.js +69 -0
- package/dist/web/directThreadResolver.js +129 -0
- package/dist/web/dmTaskHandoffPrompt.js +120 -0
- package/dist/web/dmTaskThreadStatusProjection.js +229 -0
- package/dist/web/ftsQuery.js +33 -0
- package/dist/web/internalAgentRouter.js +11341 -0
- package/dist/web/libraryCuratorScheduler.js +58 -0
- package/dist/web/libraryDocumentPromptGuidance.js +8 -0
- package/dist/web/messageCheckpoints.js +19 -0
- package/dist/web/nodeWsHandler.js +2495 -0
- package/dist/web/notificationRounds.js +1061 -0
- package/dist/web/panelActionMessages.js +108 -0
- package/dist/web/panelActivationPrompt.js +18 -0
- package/dist/web/panelAudit.js +273 -0
- package/dist/web/panelLifecycle.js +222 -0
- package/dist/web/panelMediaPolicy.js +43 -0
- package/dist/web/panelPathPolicy.js +63 -0
- package/dist/web/panelPreviews.js +175 -0
- package/dist/web/panelQueryHandles.js +2749 -0
- package/dist/web/panelRoutes.js +2147 -0
- package/dist/web/panels.js +904 -0
- package/dist/web/peerInboxAggregates.js +1247 -0
- package/dist/web/planApprovalState.js +92 -0
- package/dist/web/platformAgentScheduler.js +66 -0
- package/dist/web/proactiveOpportunities.js +452 -0
- package/dist/web/promptContextSections.js +242 -0
- package/dist/web/promptHistorySanitizer.js +26 -0
- package/dist/web/promptSlashCommands.js +158 -0
- package/dist/web/rollingConversationSummary.js +453 -0
- package/dist/web/routeHelpers.js +11 -0
- package/dist/web/routes/handoff.js +288 -0
- package/dist/web/routes/history.js +345 -0
- package/dist/web/routes/memory.js +258 -0
- package/dist/web/routes/selfState.js +171 -0
- package/dist/web/routes/workspace.js +154 -0
- package/dist/web/runSurfaceWatermarks.js +431 -0
- package/dist/web/runtimeCapabilities.js +48 -0
- package/dist/web/sameAgentHandoffs.js +494 -0
- package/dist/web/server.js +15567 -0
- package/dist/web/sharedCollaborationCapsules.js +163 -0
- package/dist/web/soloSessionRelay.js +42 -0
- package/dist/web/soloWsHandler.js +138 -0
- package/dist/web/suggestedPlannerScheduler.js +56 -0
- package/dist/web/surfaceActivationPolicy.js +108 -0
- package/dist/web/surfaceCollaborators.js +61 -0
- package/dist/web/surfaceSystemStatus.js +263 -0
- package/dist/web/targetParticipants.js +77 -0
- package/dist/web/taskEvents.js +49 -0
- package/dist/web/taskLifecycleMessages.js +165 -0
- package/dist/web/taskLoops.js +732 -0
- package/dist/web/taskMemoryNotes.js +224 -0
- package/dist/web/taskNumbers.js +16 -0
- package/dist/web/taskOwnerGuards.js +49 -0
- package/dist/web/taskParticipantResolver.js +42 -0
- package/dist/web/taskParticipants.js +97 -0
- package/dist/web/taskSourceDetails.js +20 -0
- package/dist/web/taskStateViews.js +210 -0
- package/dist/web/taskStatusTransitions.js +9 -0
- package/dist/web/taskThreadFollowups.js +599 -0
- package/dist/web/taskThreadRuntimeClosure.js +685 -0
- package/dist/web/taskUpdateDelivery.js +104 -0
- package/dist/web/threadReplyContentHeuristics.js +30 -0
- package/dist/web/threadRoots.js +61 -0
- package/dist/web/threadTaskBindings.js +365 -0
- package/dist/web/uiPanelPromptGuidance.js +27 -0
- package/dist/web/workspaceMemoryHints.js +143 -0
- package/dist/web/workspaceToolPromptGuidance.js +30 -0
- package/dist/web/wsHandler.js +397 -0
- package/dist/web/wsSink.js +116 -0
- package/package.json +54 -0
|
@@ -0,0 +1,2263 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { buildDefaultLibraryIndexContent, ensureLocalLibraryScaffold } from './libraryScaffold.js';
|
|
6
|
+
import { buildDefaultLibraryManifest, LIBRARY_MANIFEST_RELATIVE_PATH, parseLibraryManifest, } from './libraryManifest.js';
|
|
7
|
+
import { getPlatformAgentConversationTitle, getPlatformAgentDisabledToolKinds, getPlatformAgentLegacyConversationTitles, } from './platformAgentPolicy.js';
|
|
8
|
+
export const LIBRARY_CURATOR_AGENT_NAME = 'Library Curator';
|
|
9
|
+
export const DEFAULT_LIBRARY_SCHEDULE_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
10
|
+
const LIBRARY_ROOT_DIRECTORY_NAME = '.library-curator';
|
|
11
|
+
const PERSONAL_LIBRARY_DIRECTORY_NAME = 'personal';
|
|
12
|
+
const SHARED_LIBRARY_DIRECTORY_NAME = 'shared';
|
|
13
|
+
const CURATOR_WORKSPACE_DIRECTORY_NAME = 'workspaces';
|
|
14
|
+
const SHARED_LIBRARY_KEY = 'global';
|
|
15
|
+
const SHARED_LIBRARY_AGENT_NAME = 'Shared Library';
|
|
16
|
+
const LIBRARY_DIRECTORY_PLACEHOLDER = '.keep';
|
|
17
|
+
const RESERVED_LIBRARY_WORKSPACE_TOP_LEVEL_ENTRIES = ['MEMORY.md', 'notes', 'artifacts', 'projects'];
|
|
18
|
+
const CURATOR_DISABLED_TOOL_KINDS_JSON = JSON.stringify(getPlatformAgentDisabledToolKinds('library_curator'));
|
|
19
|
+
export function inferSavedItemSourceKind(contextUrl) {
|
|
20
|
+
if (contextUrl.includes('/thread/'))
|
|
21
|
+
return 'thread';
|
|
22
|
+
if (contextUrl.includes('/channel/'))
|
|
23
|
+
return 'channel';
|
|
24
|
+
return 'dm';
|
|
25
|
+
}
|
|
26
|
+
export function mapSavedItemRow(row) {
|
|
27
|
+
return {
|
|
28
|
+
id: row.id,
|
|
29
|
+
userId: row.userId,
|
|
30
|
+
messageId: row.messageId,
|
|
31
|
+
senderName: row.senderName,
|
|
32
|
+
content: row.content,
|
|
33
|
+
contextUrl: row.contextUrl,
|
|
34
|
+
sourceKind: row.sourceKind,
|
|
35
|
+
processStatus: row.processStatus,
|
|
36
|
+
processedAt: row.processedAt,
|
|
37
|
+
processedRunId: row.processedRunId,
|
|
38
|
+
sourceDeletedAt: row.sourceDeletedAt,
|
|
39
|
+
sourceDeletedByName: row.sourceDeletedByName,
|
|
40
|
+
createdAt: row.createdAt,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function mapConfigRow(row) {
|
|
44
|
+
let categories = [];
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(row.categoriesJson);
|
|
47
|
+
if (Array.isArray(parsed)) {
|
|
48
|
+
categories = parsed.filter((item) => typeof item === 'string' && item.trim().length > 0).map((item) => item.trim());
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
categories = [];
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
userId: row.userId,
|
|
56
|
+
curatorAgentId: row.curatorAgentId,
|
|
57
|
+
nodeId: row.nodeId,
|
|
58
|
+
libraryRootPath: row.libraryRootPath,
|
|
59
|
+
scheduleIntervalMs: row.scheduleIntervalMs,
|
|
60
|
+
categories,
|
|
61
|
+
lastRunAt: row.lastRunAt,
|
|
62
|
+
nextRunAt: row.nextRunAt,
|
|
63
|
+
enabled: row.enabled === 1,
|
|
64
|
+
createdAt: row.createdAt,
|
|
65
|
+
updatedAt: row.updatedAt,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function mapSharedConfigRow(row) {
|
|
69
|
+
return {
|
|
70
|
+
sharedKey: row.sharedKey,
|
|
71
|
+
libraryRootPath: row.libraryRootPath,
|
|
72
|
+
createdAt: row.createdAt,
|
|
73
|
+
updatedAt: row.updatedAt,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function buildPersonalLibraryRootPath(workspaceRoot, username) {
|
|
77
|
+
const safeUsername = username.replace(/[^a-zA-Z0-9._-]+/g, '_').trim() || 'user';
|
|
78
|
+
return path.join(workspaceRoot, LIBRARY_ROOT_DIRECTORY_NAME, PERSONAL_LIBRARY_DIRECTORY_NAME, safeUsername);
|
|
79
|
+
}
|
|
80
|
+
function buildCuratorWorkspacePath(workspaceRoot, username) {
|
|
81
|
+
const safeUsername = username.replace(/[^a-zA-Z0-9._-]+/g, '_').trim() || 'user';
|
|
82
|
+
return path.join(workspaceRoot, LIBRARY_ROOT_DIRECTORY_NAME, CURATOR_WORKSPACE_DIRECTORY_NAME, safeUsername);
|
|
83
|
+
}
|
|
84
|
+
function buildSharedLibraryRootPath(workspaceRoot) {
|
|
85
|
+
return path.join(workspaceRoot, LIBRARY_ROOT_DIRECTORY_NAME, SHARED_LIBRARY_DIRECTORY_NAME, SHARED_LIBRARY_KEY);
|
|
86
|
+
}
|
|
87
|
+
function buildLegacyHomeLibraryRootPath(username) {
|
|
88
|
+
const safeUsername = username.replace(/[^a-zA-Z0-9._-]+/g, '_').trim() || 'user';
|
|
89
|
+
return path.join(os.homedir(), 'bigbang-library', safeUsername);
|
|
90
|
+
}
|
|
91
|
+
function buildLegacyWorkspaceLibraryRootPath(workspaceRoot, username) {
|
|
92
|
+
const safeUsername = username.replace(/[^a-zA-Z0-9._-]+/g, '_').trim() || 'user';
|
|
93
|
+
return path.join(workspaceRoot, LIBRARY_ROOT_DIRECTORY_NAME, safeUsername);
|
|
94
|
+
}
|
|
95
|
+
function isLegacyPersonalLibraryRootPath(workspaceRoot, libraryRootPath, username) {
|
|
96
|
+
const resolved = path.resolve(libraryRootPath);
|
|
97
|
+
return resolved === path.resolve(buildLegacyHomeLibraryRootPath(username))
|
|
98
|
+
|| resolved === path.resolve(buildLegacyWorkspaceLibraryRootPath(workspaceRoot, username));
|
|
99
|
+
}
|
|
100
|
+
function migrateLocalLibraryRootIfNeeded(previousPath, nextPath) {
|
|
101
|
+
if (!previousPath.trim() || path.resolve(previousPath) === path.resolve(nextPath))
|
|
102
|
+
return;
|
|
103
|
+
try {
|
|
104
|
+
if (!fs.existsSync(previousPath) || fs.existsSync(nextPath))
|
|
105
|
+
return;
|
|
106
|
+
fs.mkdirSync(path.dirname(nextPath), { recursive: true });
|
|
107
|
+
fs.cpSync(previousPath, nextPath, { recursive: true });
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// Best-effort local migration; keep going with a fresh scaffold if it fails.
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function ensureLocalCuratorWorkspace(workspacePath) {
|
|
114
|
+
fs.mkdirSync(workspacePath, { recursive: true });
|
|
115
|
+
}
|
|
116
|
+
function migrateLocalCuratorWorkspaceArtifacts(libraryRootPath, workspacePath) {
|
|
117
|
+
ensureLocalCuratorWorkspace(workspacePath);
|
|
118
|
+
for (const entryName of RESERVED_LIBRARY_WORKSPACE_TOP_LEVEL_ENTRIES) {
|
|
119
|
+
const sourcePath = path.join(libraryRootPath, entryName);
|
|
120
|
+
const destinationPath = path.join(workspacePath, entryName);
|
|
121
|
+
try {
|
|
122
|
+
if (!fs.existsSync(sourcePath) || fs.existsSync(destinationPath))
|
|
123
|
+
continue;
|
|
124
|
+
moveLocalPathBestEffort(sourcePath, destinationPath);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Best-effort cleanup; mirror filtering prevents future exposure if this fails.
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function moveLocalPathBestEffort(sourcePath, destinationPath) {
|
|
132
|
+
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
133
|
+
try {
|
|
134
|
+
fs.renameSync(sourcePath, destinationPath);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
const maybeErr = error;
|
|
138
|
+
if (maybeErr?.code !== 'EXDEV')
|
|
139
|
+
throw error;
|
|
140
|
+
fs.cpSync(sourcePath, destinationPath, { recursive: true });
|
|
141
|
+
fs.rmSync(sourcePath, { recursive: true, force: true });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function buildCuratorEnvVars() {
|
|
145
|
+
const envVars = {};
|
|
146
|
+
const baseUrl = process.env.LIBRARY_CURATOR_ANTHROPIC_BASE_URL?.trim()
|
|
147
|
+
|| process.env.ANTHROPIC_BASE_URL?.trim();
|
|
148
|
+
const model = process.env.LIBRARY_CURATOR_ANTHROPIC_MODEL?.trim()
|
|
149
|
+
|| process.env.ANTHROPIC_MODEL?.trim()
|
|
150
|
+
|| 'deepseek-v4-pro';
|
|
151
|
+
const authToken = process.env.LIBRARY_CURATOR_ANTHROPIC_AUTH_TOKEN?.trim()
|
|
152
|
+
|| process.env.LIBRARY_CURATOR_ANTHROPIC_API_KEY?.trim()
|
|
153
|
+
|| process.env.ANTHROPIC_AUTH_TOKEN?.trim()
|
|
154
|
+
|| process.env.ANTHROPIC_API_KEY?.trim();
|
|
155
|
+
if (baseUrl)
|
|
156
|
+
envVars.ANTHROPIC_BASE_URL = baseUrl;
|
|
157
|
+
if (model)
|
|
158
|
+
envVars.ANTHROPIC_MODEL = model;
|
|
159
|
+
if (authToken)
|
|
160
|
+
envVars.ANTHROPIC_AUTH_TOKEN = authToken;
|
|
161
|
+
for (const key of [
|
|
162
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
163
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
164
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
165
|
+
'CLAUDE_CODE_SUBAGENT_MODEL',
|
|
166
|
+
'CLAUDE_CODE_EFFORT_LEVEL',
|
|
167
|
+
]) {
|
|
168
|
+
const value = process.env[key]?.trim();
|
|
169
|
+
if (value)
|
|
170
|
+
envVars[key] = value;
|
|
171
|
+
}
|
|
172
|
+
return envVars;
|
|
173
|
+
}
|
|
174
|
+
function pickLibraryNodeId(nodeRegistry) {
|
|
175
|
+
for (const node of nodeRegistry.listNodes()) {
|
|
176
|
+
const agentTypes = Array.isArray(node.agentTypes) ? node.agentTypes : [];
|
|
177
|
+
if (agentTypes.includes('claude_sdk'))
|
|
178
|
+
return node.nodeId;
|
|
179
|
+
}
|
|
180
|
+
const fallback = nodeRegistry.listNodes()[0];
|
|
181
|
+
return fallback?.nodeId ?? null;
|
|
182
|
+
}
|
|
183
|
+
function slugifyLibraryAgentName(name) {
|
|
184
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'library-curator';
|
|
185
|
+
}
|
|
186
|
+
function mapLibraryDocumentRow(row, scope) {
|
|
187
|
+
return {
|
|
188
|
+
id: String(row.id),
|
|
189
|
+
scope,
|
|
190
|
+
userId: row.userId ? String(row.userId) : null,
|
|
191
|
+
title: String(row.title),
|
|
192
|
+
category: String(row.category ?? ''),
|
|
193
|
+
topicPath: String(row.topicPath ?? ''),
|
|
194
|
+
relativePath: String(row.relativePath),
|
|
195
|
+
summary: String(row.summary ?? ''),
|
|
196
|
+
sourceSavedItemIds: parseStringArrayJson(String(row.sourceSavedItemIdsJson ?? '[]')),
|
|
197
|
+
createdAt: Number(row.createdAt),
|
|
198
|
+
updatedAt: Number(row.updatedAt),
|
|
199
|
+
lastRunId: row.lastRunId ? String(row.lastRunId) : null,
|
|
200
|
+
notionPageId: row.notionPageId ? String(row.notionPageId) : null,
|
|
201
|
+
notionPageUrl: row.notionPageUrl ? String(row.notionPageUrl) : null,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
export function getLibraryCuratorTerminalError(stopReason, error) {
|
|
205
|
+
const explicitError = error?.trim();
|
|
206
|
+
if (explicitError)
|
|
207
|
+
return explicitError;
|
|
208
|
+
const normalized = stopReason?.trim().toLowerCase();
|
|
209
|
+
if (!normalized || normalized === 'end_turn' || normalized === 'stop_sequence')
|
|
210
|
+
return undefined;
|
|
211
|
+
if (normalized.includes('cancel') || normalized.includes('abort') || normalized === 'error') {
|
|
212
|
+
return `Library curator run ended before completion: ${stopReason}`;
|
|
213
|
+
}
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
export class LibraryService {
|
|
217
|
+
db;
|
|
218
|
+
conversationManager;
|
|
219
|
+
nodeRegistry;
|
|
220
|
+
workspaceBroker;
|
|
221
|
+
workspaceRoot;
|
|
222
|
+
writebackChains = new Map();
|
|
223
|
+
constructor(db, conversationManager, nodeRegistry, workspaceBroker, workspaceRoot) {
|
|
224
|
+
this.db = db;
|
|
225
|
+
this.conversationManager = conversationManager;
|
|
226
|
+
this.nodeRegistry = nodeRegistry;
|
|
227
|
+
this.workspaceBroker = workspaceBroker;
|
|
228
|
+
this.workspaceRoot = workspaceRoot;
|
|
229
|
+
}
|
|
230
|
+
purgeStaleRunLibraryMirrors() {
|
|
231
|
+
this.db.prepare(`DELETE FROM library_run_mirrors
|
|
232
|
+
WHERE run_id IN (
|
|
233
|
+
SELECT m.run_id
|
|
234
|
+
FROM library_run_mirrors m
|
|
235
|
+
LEFT JOIN runs r ON r.run_id = m.run_id
|
|
236
|
+
WHERE r.run_id IS NULL
|
|
237
|
+
OR r.ended_at IS NOT NULL
|
|
238
|
+
)`).run();
|
|
239
|
+
}
|
|
240
|
+
getConfig(userId) {
|
|
241
|
+
const row = this.db.prepare(`SELECT user_id as userId, curator_agent_id as curatorAgentId, node_id as nodeId,
|
|
242
|
+
library_root_path as libraryRootPath, schedule_interval_ms as scheduleIntervalMs,
|
|
243
|
+
categories_json as categoriesJson, last_run_at as lastRunAt, next_run_at as nextRunAt,
|
|
244
|
+
enabled, created_at as createdAt, updated_at as updatedAt
|
|
245
|
+
FROM library_curator_configs WHERE user_id = ?`).get(userId);
|
|
246
|
+
return row ? mapConfigRow(row) : null;
|
|
247
|
+
}
|
|
248
|
+
getSharedConfig() {
|
|
249
|
+
const row = this.db.prepare(`SELECT shared_key as sharedKey, library_root_path as libraryRootPath,
|
|
250
|
+
created_at as createdAt, updated_at as updatedAt
|
|
251
|
+
FROM shared_library_config
|
|
252
|
+
WHERE shared_key = ?`).get(SHARED_LIBRARY_KEY);
|
|
253
|
+
return row ? mapSharedConfigRow(row) : null;
|
|
254
|
+
}
|
|
255
|
+
ensureSharedConfig() {
|
|
256
|
+
const existing = this.getSharedConfig();
|
|
257
|
+
if (existing) {
|
|
258
|
+
ensureLocalLibraryScaffold(existing.libraryRootPath);
|
|
259
|
+
return existing;
|
|
260
|
+
}
|
|
261
|
+
const now = Date.now();
|
|
262
|
+
const libraryRootPath = buildSharedLibraryRootPath(this.workspaceRoot);
|
|
263
|
+
ensureLocalLibraryScaffold(libraryRootPath);
|
|
264
|
+
this.db.prepare(`INSERT INTO shared_library_config(shared_key, library_root_path, created_at, updated_at)
|
|
265
|
+
VALUES(?, ?, ?, ?)`).run(SHARED_LIBRARY_KEY, libraryRootPath, now, now);
|
|
266
|
+
return this.getSharedConfig();
|
|
267
|
+
}
|
|
268
|
+
isPersonalLibraryEnabledForConversation(conversationId) {
|
|
269
|
+
const row = this.db.prepare(`SELECT personal_library_enabled as personalLibraryEnabled
|
|
270
|
+
FROM conversations
|
|
271
|
+
WHERE id = ?`).get(conversationId);
|
|
272
|
+
return row?.personalLibraryEnabled === 1;
|
|
273
|
+
}
|
|
274
|
+
enablePersonalLibraryForConversation(conversationId) {
|
|
275
|
+
this.db.prepare(`UPDATE conversations
|
|
276
|
+
SET personal_library_enabled = 1,
|
|
277
|
+
updated_at = ?
|
|
278
|
+
WHERE id = ?`).run(Date.now(), conversationId);
|
|
279
|
+
}
|
|
280
|
+
async ensureConfigForUser(params) {
|
|
281
|
+
const existing = this.getConfig(params.userId);
|
|
282
|
+
if (existing) {
|
|
283
|
+
const synced = await this.syncExistingConfigForUser({
|
|
284
|
+
...params,
|
|
285
|
+
existing,
|
|
286
|
+
});
|
|
287
|
+
return synced;
|
|
288
|
+
}
|
|
289
|
+
const now = Date.now();
|
|
290
|
+
const nodeId = pickLibraryNodeId(this.nodeRegistry);
|
|
291
|
+
const libraryRootPath = buildPersonalLibraryRootPath(this.workspaceRoot, params.username);
|
|
292
|
+
const workspacePath = buildCuratorWorkspacePath(this.workspaceRoot, params.username);
|
|
293
|
+
const curatorAgentId = randomUUID();
|
|
294
|
+
const agentType = 'claude_sdk';
|
|
295
|
+
const envVars = buildCuratorEnvVars();
|
|
296
|
+
const envVarsJson = Object.keys(envVars).length > 0 ? JSON.stringify(envVars) : null;
|
|
297
|
+
ensureLocalLibraryScaffold(libraryRootPath);
|
|
298
|
+
ensureLocalCuratorWorkspace(workspacePath);
|
|
299
|
+
this.db.prepare(`INSERT INTO agents(
|
|
300
|
+
agent_id, name, agent_type, agent_role, model, reasoning_effort, claude_permission_mode,
|
|
301
|
+
codex_mode, codex_service_tier, channel_id, system_prompt, description, memory,
|
|
302
|
+
env_vars, disabled_tool_kinds, node_id, workspace_path, project_path, skill_roots,
|
|
303
|
+
created_at, updated_at
|
|
304
|
+
) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(curatorAgentId, LIBRARY_CURATOR_AGENT_NAME, agentType, 'platform_agent', envVars.ANTHROPIC_MODEL ?? 'deepseek-v4-pro', null, 'bypassPermissions', null, null, 'default', '', 'System workbench agent that organizes saved messages into structured library documents.', '', envVarsJson, CURATOR_DISABLED_TOOL_KINDS_JSON, nodeId, workspacePath, null, null, now, now);
|
|
305
|
+
this.db.prepare(`INSERT OR IGNORE INTO agent_channel_memberships(agent_id, channel_id, is_home, joined_at)
|
|
306
|
+
VALUES(?, 'default', 1, ?)`).run(curatorAgentId, now);
|
|
307
|
+
this.db.prepare(`INSERT OR IGNORE INTO user_agent_access(user_id, agent_id, granted_at) VALUES(?, ?, ?)`).run(params.userId, curatorAgentId, now);
|
|
308
|
+
const nextRunAt = now + DEFAULT_LIBRARY_SCHEDULE_INTERVAL_MS;
|
|
309
|
+
this.db.prepare(`INSERT INTO library_curator_configs(
|
|
310
|
+
user_id, curator_agent_id, node_id, library_root_path, schedule_interval_ms,
|
|
311
|
+
categories_json, last_run_at, next_run_at, enabled, created_at, updated_at
|
|
312
|
+
) VALUES(?, ?, ?, ?, ?, '[]', NULL, ?, 1, ?, ?)`).run(params.userId, curatorAgentId, nodeId, libraryRootPath, DEFAULT_LIBRARY_SCHEDULE_INTERVAL_MS, nextRunAt, now, now);
|
|
313
|
+
await this.ensureRemoteLibraryScaffold(nodeId, libraryRootPath);
|
|
314
|
+
const created = this.getConfig(params.userId);
|
|
315
|
+
if (!created)
|
|
316
|
+
throw new Error('Failed to create library curator config');
|
|
317
|
+
this.syncPlatformAgentInstance(created);
|
|
318
|
+
return created;
|
|
319
|
+
}
|
|
320
|
+
async syncExistingConfigForUser(params) {
|
|
321
|
+
const resolvedNodeId = this.resolveCuratorNodeId(params.existing.nodeId);
|
|
322
|
+
let libraryRootPath = params.existing.libraryRootPath.trim();
|
|
323
|
+
const expectedLibraryRootPath = buildPersonalLibraryRootPath(this.workspaceRoot, params.username);
|
|
324
|
+
const expectedWorkspacePath = buildCuratorWorkspacePath(this.workspaceRoot, params.username);
|
|
325
|
+
if (!libraryRootPath) {
|
|
326
|
+
libraryRootPath = expectedLibraryRootPath;
|
|
327
|
+
}
|
|
328
|
+
else if (isLegacyPersonalLibraryRootPath(this.workspaceRoot, libraryRootPath, params.username)) {
|
|
329
|
+
libraryRootPath = expectedLibraryRootPath;
|
|
330
|
+
}
|
|
331
|
+
if (path.resolve(libraryRootPath) !== path.resolve(params.existing.libraryRootPath)) {
|
|
332
|
+
migrateLocalLibraryRootIfNeeded(params.existing.libraryRootPath, libraryRootPath);
|
|
333
|
+
}
|
|
334
|
+
ensureLocalLibraryScaffold(libraryRootPath);
|
|
335
|
+
ensureLocalCuratorWorkspace(expectedWorkspacePath);
|
|
336
|
+
migrateLocalCuratorWorkspaceArtifacts(libraryRootPath, expectedWorkspacePath);
|
|
337
|
+
const assignmentChanged = resolvedNodeId !== params.existing.nodeId
|
|
338
|
+
|| path.resolve(libraryRootPath) !== path.resolve(params.existing.libraryRootPath);
|
|
339
|
+
const agentRow = this.db.prepare(`SELECT system_prompt as systemPrompt, workspace_path as workspacePath, disabled_tool_kinds as disabledToolKindsJson
|
|
340
|
+
FROM agents
|
|
341
|
+
WHERE agent_id = ?`).get(params.existing.curatorAgentId);
|
|
342
|
+
const hasPersistedSystemPrompt = Boolean(agentRow?.systemPrompt?.trim());
|
|
343
|
+
const workspacePathChanged = path.resolve(agentRow?.workspacePath?.trim() || '')
|
|
344
|
+
!== path.resolve(expectedWorkspacePath);
|
|
345
|
+
const disabledToolKindsChanged = agentRow?.disabledToolKindsJson !== CURATOR_DISABLED_TOOL_KINDS_JSON;
|
|
346
|
+
const conversationWorkspacePathNeedsRepair = this.db.prepare(`SELECT COUNT(*) as count
|
|
347
|
+
FROM conversations
|
|
348
|
+
WHERE agent_id = ?
|
|
349
|
+
AND COALESCE(workspace_path, '') != ?`).get(params.existing.curatorAgentId, expectedWorkspacePath).count > 0;
|
|
350
|
+
if ((hasPersistedSystemPrompt || disabledToolKindsChanged) && !assignmentChanged && !workspacePathChanged) {
|
|
351
|
+
this.db.prepare(`UPDATE agents
|
|
352
|
+
SET system_prompt = '',
|
|
353
|
+
disabled_tool_kinds = ?,
|
|
354
|
+
updated_at = ?
|
|
355
|
+
WHERE agent_id = ?`).run(CURATOR_DISABLED_TOOL_KINDS_JSON, Date.now(), params.existing.curatorAgentId);
|
|
356
|
+
}
|
|
357
|
+
if (assignmentChanged || workspacePathChanged || conversationWorkspacePathNeedsRepair || disabledToolKindsChanged) {
|
|
358
|
+
const now = Date.now();
|
|
359
|
+
this.db.prepare(`UPDATE library_curator_configs
|
|
360
|
+
SET node_id = ?,
|
|
361
|
+
library_root_path = ?,
|
|
362
|
+
updated_at = ?
|
|
363
|
+
WHERE user_id = ?`).run(resolvedNodeId, libraryRootPath, now, params.userId);
|
|
364
|
+
this.db.prepare(`UPDATE agents
|
|
365
|
+
SET node_id = ?,
|
|
366
|
+
workspace_path = ?,
|
|
367
|
+
system_prompt = '',
|
|
368
|
+
disabled_tool_kinds = ?,
|
|
369
|
+
updated_at = ?
|
|
370
|
+
WHERE agent_id = ?`).run(resolvedNodeId, expectedWorkspacePath, CURATOR_DISABLED_TOOL_KINDS_JSON, now, params.existing.curatorAgentId);
|
|
371
|
+
this.db.prepare(`UPDATE conversations
|
|
372
|
+
SET node_id = ?,
|
|
373
|
+
workspace_path = ?,
|
|
374
|
+
updated_at = ?
|
|
375
|
+
WHERE agent_id = ?`).run(resolvedNodeId, expectedWorkspacePath, now, params.existing.curatorAgentId);
|
|
376
|
+
await this.ensureRemoteLibraryScaffold(resolvedNodeId, libraryRootPath);
|
|
377
|
+
}
|
|
378
|
+
const synced = this.getConfig(params.userId) ?? params.existing;
|
|
379
|
+
this.syncPlatformAgentInstance(synced);
|
|
380
|
+
return synced;
|
|
381
|
+
}
|
|
382
|
+
resolveCuratorNodeId(currentNodeId) {
|
|
383
|
+
const normalizedCurrentNodeId = currentNodeId?.trim() || null;
|
|
384
|
+
if (normalizedCurrentNodeId) {
|
|
385
|
+
const currentNode = this.nodeRegistry.getNode(normalizedCurrentNodeId);
|
|
386
|
+
if (currentNode?.agentTypes.includes('claude_sdk'))
|
|
387
|
+
return normalizedCurrentNodeId;
|
|
388
|
+
}
|
|
389
|
+
return pickLibraryNodeId(this.nodeRegistry) ?? normalizedCurrentNodeId;
|
|
390
|
+
}
|
|
391
|
+
async ensureRemoteLibraryScaffold(nodeId, libraryRootPath, agentName = LIBRARY_CURATOR_AGENT_NAME) {
|
|
392
|
+
if (!nodeId)
|
|
393
|
+
return;
|
|
394
|
+
ensureLocalLibraryScaffold(libraryRootPath);
|
|
395
|
+
const files = [
|
|
396
|
+
{ relativePath: 'INDEX.md', absolutePath: path.join(libraryRootPath, 'INDEX.md') },
|
|
397
|
+
{ relativePath: LIBRARY_MANIFEST_RELATIVE_PATH, absolutePath: path.join(libraryRootPath, LIBRARY_MANIFEST_RELATIVE_PATH) },
|
|
398
|
+
];
|
|
399
|
+
for (const file of files) {
|
|
400
|
+
let alreadyPresent = false;
|
|
401
|
+
try {
|
|
402
|
+
await this.workspaceBroker.readFile(nodeId, libraryRootPath, file.relativePath, {
|
|
403
|
+
scaffold: false,
|
|
404
|
+
agentName,
|
|
405
|
+
});
|
|
406
|
+
alreadyPresent = true;
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
alreadyPresent = false;
|
|
410
|
+
}
|
|
411
|
+
if (alreadyPresent)
|
|
412
|
+
continue;
|
|
413
|
+
try {
|
|
414
|
+
const content = fs.readFileSync(file.absolutePath, 'utf8');
|
|
415
|
+
await this.workspaceBroker.writeFile(nodeId, libraryRootPath, file.relativePath, content, 'overwrite', { scaffold: false, agentName });
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
// Best-effort remote scaffold; local workspace remains authoritative.
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
updateConfig(userId, patch) {
|
|
423
|
+
const existing = this.getConfig(userId);
|
|
424
|
+
if (!existing)
|
|
425
|
+
throw new Error('Library config not found');
|
|
426
|
+
const now = Date.now();
|
|
427
|
+
const scheduleIntervalMs = patch.scheduleIntervalMs ?? existing.scheduleIntervalMs;
|
|
428
|
+
const categories = patch.categories ?? existing.categories;
|
|
429
|
+
const enabled = patch.enabled ?? existing.enabled;
|
|
430
|
+
const nextRunAt = enabled ? existing.nextRunAt ?? (now + scheduleIntervalMs) : null;
|
|
431
|
+
this.db.prepare(`UPDATE library_curator_configs
|
|
432
|
+
SET schedule_interval_ms = ?,
|
|
433
|
+
categories_json = ?,
|
|
434
|
+
enabled = ?,
|
|
435
|
+
next_run_at = ?,
|
|
436
|
+
updated_at = ?
|
|
437
|
+
WHERE user_id = ?`).run(Math.max(60_000, scheduleIntervalMs), JSON.stringify(categories), enabled ? 1 : 0, nextRunAt, now, userId);
|
|
438
|
+
const updated = this.getConfig(userId);
|
|
439
|
+
if (!updated)
|
|
440
|
+
throw new Error('Library config not found after update');
|
|
441
|
+
this.syncPlatformAgentInstance(updated, { overwriteCanonical: true });
|
|
442
|
+
return updated;
|
|
443
|
+
}
|
|
444
|
+
syncPlatformAgentInstance(config, options) {
|
|
445
|
+
const agent = this.getCuratorAgent(config.userId);
|
|
446
|
+
try {
|
|
447
|
+
const conflictUpdate = options?.overwriteCanonical
|
|
448
|
+
? `agent_id = excluded.agent_id,
|
|
449
|
+
enabled = excluded.enabled,
|
|
450
|
+
schedule_interval_ms = excluded.schedule_interval_ms,
|
|
451
|
+
workflow_config_json = excluded.workflow_config_json,
|
|
452
|
+
runtime_config_json = excluded.runtime_config_json,
|
|
453
|
+
last_run_at = excluded.last_run_at,
|
|
454
|
+
next_run_at = excluded.next_run_at,
|
|
455
|
+
updated_at = excluded.updated_at`
|
|
456
|
+
: `agent_id = excluded.agent_id,
|
|
457
|
+
last_run_at = COALESCE(platform_agent_instances.last_run_at, excluded.last_run_at),
|
|
458
|
+
updated_at = excluded.updated_at`;
|
|
459
|
+
this.db.prepare(`INSERT INTO platform_agent_instances(
|
|
460
|
+
user_id, definition_id, agent_id, enabled, schedule_interval_ms,
|
|
461
|
+
workflow_config_json, runtime_config_json, last_run_at, next_run_at,
|
|
462
|
+
created_at, updated_at
|
|
463
|
+
) VALUES(?, 'library_curator', ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
464
|
+
ON CONFLICT(user_id, definition_id) DO UPDATE SET
|
|
465
|
+
${conflictUpdate}`).run(config.userId, config.curatorAgentId, config.enabled ? 1 : 0, Math.max(60_000, config.scheduleIntervalMs), JSON.stringify({
|
|
466
|
+
categories: config.categories,
|
|
467
|
+
libraryRootPath: config.libraryRootPath,
|
|
468
|
+
}), JSON.stringify({
|
|
469
|
+
model: agent?.model ?? null,
|
|
470
|
+
reasoningEffort: agent?.reasoningEffort ?? null,
|
|
471
|
+
envVars: agent?.envVars ?? {},
|
|
472
|
+
}), config.lastRunAt, config.nextRunAt, config.createdAt, Date.now());
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
// Older tests and pre-v124 schemas may not have the generic platform-agent tables yet.
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
listInboxSavedItems(userId) {
|
|
479
|
+
const rows = this.db.prepare(`SELECT id, user_id as userId, message_id as messageId, sender_name as senderName,
|
|
480
|
+
content, context_url as contextUrl, source_kind as sourceKind,
|
|
481
|
+
process_status as processStatus, processed_at as processedAt,
|
|
482
|
+
processed_run_id as processedRunId,
|
|
483
|
+
source_deleted_at as sourceDeletedAt,
|
|
484
|
+
source_deleted_by_name as sourceDeletedByName,
|
|
485
|
+
created_at as createdAt
|
|
486
|
+
FROM saved_items
|
|
487
|
+
WHERE user_id = ? AND process_status = 'inbox' AND source_deleted_at IS NULL
|
|
488
|
+
ORDER BY created_at ASC`).all(userId);
|
|
489
|
+
return rows.map(mapSavedItemRow);
|
|
490
|
+
}
|
|
491
|
+
async searchDocuments(userId, query, scope = 'personal') {
|
|
492
|
+
const normalized = query.trim().toLowerCase();
|
|
493
|
+
if (!normalized || normalized.length < 2)
|
|
494
|
+
return [];
|
|
495
|
+
const docs = await this.listDocuments(userId, undefined, scope);
|
|
496
|
+
return docs.filter((doc) => {
|
|
497
|
+
const haystack = `${doc.title} ${doc.summary} ${doc.relativePath}`.toLowerCase();
|
|
498
|
+
return haystack.includes(normalized);
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
async listDocuments(userId, category, scope = 'personal') {
|
|
502
|
+
if (scope === 'all') {
|
|
503
|
+
const [personalDocs, sharedDocs] = await Promise.all([
|
|
504
|
+
this.listDocuments(userId, category, 'personal'),
|
|
505
|
+
this.listDocuments(userId, category, 'shared'),
|
|
506
|
+
]);
|
|
507
|
+
return [...sharedDocs, ...personalDocs];
|
|
508
|
+
}
|
|
509
|
+
const location = this.getLibraryLocation(userId, scope);
|
|
510
|
+
if (!location)
|
|
511
|
+
return [];
|
|
512
|
+
await this.reconcileIndexedDocumentsForLocation(location);
|
|
513
|
+
return this.queryIndexedDocumentsForLocation(location, category);
|
|
514
|
+
}
|
|
515
|
+
async buildDocumentTree(userId, scope = 'personal') {
|
|
516
|
+
if (scope === 'all') {
|
|
517
|
+
const [personalTree, sharedTree] = await Promise.all([
|
|
518
|
+
this.buildDocumentTree(userId, 'personal'),
|
|
519
|
+
this.buildDocumentTree(userId, 'shared'),
|
|
520
|
+
]);
|
|
521
|
+
const nodes = [];
|
|
522
|
+
if (personalTree.length > 0) {
|
|
523
|
+
nodes.push({
|
|
524
|
+
scope: 'personal',
|
|
525
|
+
name: 'personal',
|
|
526
|
+
path: 'personal',
|
|
527
|
+
kind: 'directory',
|
|
528
|
+
children: personalTree,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
if (sharedTree.length > 0) {
|
|
532
|
+
nodes.push({
|
|
533
|
+
scope: 'shared',
|
|
534
|
+
name: 'shared',
|
|
535
|
+
path: 'shared',
|
|
536
|
+
kind: 'directory',
|
|
537
|
+
children: sharedTree,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
return nodes;
|
|
541
|
+
}
|
|
542
|
+
const location = this.getLibraryLocation(userId, scope);
|
|
543
|
+
if (!location)
|
|
544
|
+
return [];
|
|
545
|
+
return this.buildDocumentTreeForLocation(location);
|
|
546
|
+
}
|
|
547
|
+
async readLibraryFile(userId, relativePath, scope = 'personal') {
|
|
548
|
+
const location = this.getRequiredLibraryLocation(userId, scope);
|
|
549
|
+
return this.readLibraryFileFromLocation(location, relativePath);
|
|
550
|
+
}
|
|
551
|
+
async getDocumentNotionMetadata(userId, relativePath, scope = 'personal') {
|
|
552
|
+
const location = this.getRequiredLibraryLocation(userId, scope);
|
|
553
|
+
const manifest = await this.readLibraryManifest(location);
|
|
554
|
+
const doc = manifest.documents.find((d) => d.relativePath === relativePath);
|
|
555
|
+
return {
|
|
556
|
+
notionPageId: doc?.notionPageId ?? null,
|
|
557
|
+
notionPageUrl: doc?.notionPageUrl ?? null,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
async setDocumentNotionMetadata(userId, relativePath, scope, notionPageId, notionPageUrl) {
|
|
561
|
+
const location = this.getRequiredLibraryLocation(userId, scope);
|
|
562
|
+
const manifest = await this.readLibraryManifest(location);
|
|
563
|
+
const existing = manifest.documents.find((d) => d.relativePath === relativePath);
|
|
564
|
+
if (!existing) {
|
|
565
|
+
throw new Error('Document not found in manifest');
|
|
566
|
+
}
|
|
567
|
+
const nextManifest = {
|
|
568
|
+
version: manifest.version || 1,
|
|
569
|
+
processedSavedItemIds: manifest.processedSavedItemIds ?? [],
|
|
570
|
+
documents: manifest.documents.map((doc) => doc.relativePath === relativePath
|
|
571
|
+
? { ...doc, notionPageId, notionPageUrl }
|
|
572
|
+
: doc),
|
|
573
|
+
};
|
|
574
|
+
await this.writeLibraryManifest(location, nextManifest);
|
|
575
|
+
await this.reconcileIndexedDocumentsForLocation(location);
|
|
576
|
+
}
|
|
577
|
+
async createDirectory(userId, relativePath, scope = 'personal') {
|
|
578
|
+
const location = this.getRequiredLibraryLocation(userId, scope);
|
|
579
|
+
const { normalizedPath, absolutePath } = resolveLibraryAbsolutePath(location.libraryRootPath, relativePath);
|
|
580
|
+
if (normalizedPath.toLowerCase().endsWith('.md')) {
|
|
581
|
+
throw new Error('Directory path cannot end with .md');
|
|
582
|
+
}
|
|
583
|
+
assertLibraryWritePathAllowed(normalizedPath);
|
|
584
|
+
if (location.nodeId) {
|
|
585
|
+
await this.workspaceBroker.writeFile(location.nodeId, location.libraryRootPath, `${normalizedPath}/${LIBRARY_DIRECTORY_PLACEHOLDER}`, '', 'overwrite', { scaffold: false, agentName: location.agentName });
|
|
586
|
+
}
|
|
587
|
+
const fsPromises = await import('node:fs/promises');
|
|
588
|
+
await fsPromises.mkdir(absolutePath, { recursive: true });
|
|
589
|
+
await this.reconcileIndexedDocumentsForLocation(location);
|
|
590
|
+
}
|
|
591
|
+
async writeDocumentFile(userId, relativePath, content, scope = 'personal') {
|
|
592
|
+
const location = this.getRequiredLibraryLocation(userId, scope);
|
|
593
|
+
const { normalizedPath, absolutePath } = resolveLibraryAbsolutePath(location.libraryRootPath, relativePath);
|
|
594
|
+
assertLibraryWritePathAllowed(normalizedPath);
|
|
595
|
+
if (!normalizedPath.toLowerCase().endsWith('.md')) {
|
|
596
|
+
throw new Error('Library documents must be markdown files');
|
|
597
|
+
}
|
|
598
|
+
const nextContent = content.replace(/\r\n/g, '\n');
|
|
599
|
+
if (location.nodeId) {
|
|
600
|
+
await this.workspaceBroker.writeFile(location.nodeId, location.libraryRootPath, normalizedPath, nextContent, 'overwrite', { scaffold: false, agentName: location.agentName });
|
|
601
|
+
}
|
|
602
|
+
const fsPromises = await import('node:fs/promises');
|
|
603
|
+
await fsPromises.mkdir(path.dirname(absolutePath), { recursive: true });
|
|
604
|
+
await fsPromises.writeFile(absolutePath, nextContent, 'utf8');
|
|
605
|
+
const existingManifest = await this.readLibraryManifest(location);
|
|
606
|
+
const nextManifest = upsertLibraryManifestDocument(existingManifest, normalizedPath, nextContent);
|
|
607
|
+
await this.writeLibraryManifest(location, nextManifest);
|
|
608
|
+
await this.reconcileIndexedDocumentsForLocation(location);
|
|
609
|
+
}
|
|
610
|
+
async applyStructuredCuratorPlan(params) {
|
|
611
|
+
const location = this.getRequiredLibraryLocation(params.userId, 'personal');
|
|
612
|
+
const allowedSavedItemIds = new Set(params.savedItems.map((item) => item.id));
|
|
613
|
+
const processedSavedItemIds = [...new Set(params.plan.processedSavedItemIds.filter((id) => allowedSavedItemIds.has(id)))];
|
|
614
|
+
if (processedSavedItemIds.length !== params.plan.processedSavedItemIds.length) {
|
|
615
|
+
throw new Error('Library curator plan referenced saved item ids outside the current inbox snapshot.');
|
|
616
|
+
}
|
|
617
|
+
const manifest = await this.readLibraryManifest(location);
|
|
618
|
+
const nextManifestDocuments = new Map(manifest.documents.map((doc) => [doc.relativePath, { ...doc }]));
|
|
619
|
+
const touchedPaths = new Set();
|
|
620
|
+
for (const operation of params.plan.operations) {
|
|
621
|
+
const { normalizedPath, absolutePath } = resolveLibraryAbsolutePath(location.libraryRootPath, operation.relativePath);
|
|
622
|
+
assertLibraryWritePathAllowed(normalizedPath);
|
|
623
|
+
if (!normalizedPath.toLowerCase().endsWith('.md')) {
|
|
624
|
+
throw new Error('Library curator may only target markdown documents.');
|
|
625
|
+
}
|
|
626
|
+
if (touchedPaths.has(normalizedPath)) {
|
|
627
|
+
throw new Error(`Library curator plan targeted ${normalizedPath} multiple times.`);
|
|
628
|
+
}
|
|
629
|
+
touchedPaths.add(normalizedPath);
|
|
630
|
+
if (operation.sourceSavedItemIds.some((id) => !allowedSavedItemIds.has(id))) {
|
|
631
|
+
throw new Error(`Library curator plan referenced unknown sourceSavedItemIds for ${normalizedPath}.`);
|
|
632
|
+
}
|
|
633
|
+
const existingKind = await this.getAuthorityEntryKind(location, normalizedPath);
|
|
634
|
+
if (operation.kind === 'create' && existingKind) {
|
|
635
|
+
throw new Error(`Library curator create target already exists: ${normalizedPath}`);
|
|
636
|
+
}
|
|
637
|
+
if (operation.kind === 'update' && existingKind !== 'file') {
|
|
638
|
+
throw new Error(`Library curator update target does not exist as a file: ${normalizedPath}`);
|
|
639
|
+
}
|
|
640
|
+
const nextContent = operation.content.replace(/\r\n/g, '\n');
|
|
641
|
+
if (location.nodeId) {
|
|
642
|
+
await this.workspaceBroker.writeFile(location.nodeId, location.libraryRootPath, normalizedPath, nextContent, 'overwrite', { scaffold: false, agentName: location.agentName });
|
|
643
|
+
}
|
|
644
|
+
const fsPromises = await import('node:fs/promises');
|
|
645
|
+
await fsPromises.mkdir(path.dirname(absolutePath), { recursive: true });
|
|
646
|
+
await fsPromises.writeFile(absolutePath, nextContent, 'utf8');
|
|
647
|
+
const extractedTitle = extractLibraryDocumentTitle(nextContent);
|
|
648
|
+
nextManifestDocuments.set(normalizedPath, {
|
|
649
|
+
title: extractedTitle || operation.title.trim() || path.posix.basename(normalizedPath, '.md'),
|
|
650
|
+
category: inferLibraryCategory(normalizedPath),
|
|
651
|
+
topicPath: inferLibraryTopicPath(normalizedPath),
|
|
652
|
+
relativePath: normalizedPath,
|
|
653
|
+
summary: operation.summary.trim(),
|
|
654
|
+
sourceSavedItemIds: [...new Set(operation.sourceSavedItemIds)],
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
const nextManifest = {
|
|
658
|
+
version: manifest.version || 1,
|
|
659
|
+
processedSavedItemIds,
|
|
660
|
+
documents: [...nextManifestDocuments.values()].sort((left, right) => left.relativePath.localeCompare(right.relativePath)),
|
|
661
|
+
};
|
|
662
|
+
await this.writeLibraryManifest(location, nextManifest);
|
|
663
|
+
await this.reconcileIndexedDocumentsForLocation(location, params.runId);
|
|
664
|
+
if (processedSavedItemIds.length > 0) {
|
|
665
|
+
const now = Date.now();
|
|
666
|
+
const markProcessed = this.db.prepare(`UPDATE saved_items
|
|
667
|
+
SET process_status = 'processed', processed_at = ?, processed_run_id = ?
|
|
668
|
+
WHERE user_id = ? AND id = ?`);
|
|
669
|
+
for (const savedItemId of processedSavedItemIds) {
|
|
670
|
+
markProcessed.run(now, params.runId, params.userId, savedItemId);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return {
|
|
674
|
+
writtenDocuments: touchedPaths.size,
|
|
675
|
+
processedSavedItemIds,
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
async renameEntry(userId, relativePath, kind, nextName, scope) {
|
|
679
|
+
const location = this.getRequiredLibraryLocation(userId, scope);
|
|
680
|
+
const { normalizedPath } = resolveLibraryAbsolutePath(location.libraryRootPath, relativePath);
|
|
681
|
+
assertLibraryWritePathAllowed(normalizedPath);
|
|
682
|
+
const entryKind = await this.getAuthorityEntryKind(location, normalizedPath);
|
|
683
|
+
if (!entryKind) {
|
|
684
|
+
throw new Error('Path not found');
|
|
685
|
+
}
|
|
686
|
+
if (entryKind !== kind) {
|
|
687
|
+
throw new Error(kind === 'file' ? 'Path is not a file' : 'Path is not a directory');
|
|
688
|
+
}
|
|
689
|
+
if (kind === 'file' && !normalizedPath.toLowerCase().endsWith('.md')) {
|
|
690
|
+
throw new Error('Only markdown files can be renamed in the library');
|
|
691
|
+
}
|
|
692
|
+
const nextRelativePath = buildRenamedLibraryPath(normalizedPath, nextName, kind);
|
|
693
|
+
assertLibraryWritePathAllowed(nextRelativePath);
|
|
694
|
+
if (nextRelativePath === normalizedPath) {
|
|
695
|
+
throw new Error('Source and destination paths are the same');
|
|
696
|
+
}
|
|
697
|
+
if (location.nodeId) {
|
|
698
|
+
try {
|
|
699
|
+
await this.workspaceBroker.renameLibraryPath(location.nodeId, location.libraryRootPath, normalizedPath, nextRelativePath, { agentName: location.agentName });
|
|
700
|
+
}
|
|
701
|
+
catch (error) {
|
|
702
|
+
const committed = await this.didRemoteAuthorityRenameCommit(location, normalizedPath, nextRelativePath, kind);
|
|
703
|
+
if (!committed)
|
|
704
|
+
throw error;
|
|
705
|
+
try {
|
|
706
|
+
await this.recoverCommittedRemoteAuthorityRename(location, normalizedPath, nextRelativePath, kind);
|
|
707
|
+
}
|
|
708
|
+
catch (recoveryError) {
|
|
709
|
+
throw new Error(`Remote library rename appears to have succeeded after a transport failure, but recovery failed: ${String(recoveryError?.message ?? recoveryError)}`);
|
|
710
|
+
}
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
try {
|
|
714
|
+
await this.syncRemoteAuthorityRenameToLocalCache(location, normalizedPath, nextRelativePath, kind);
|
|
715
|
+
await this.finalizeAuthorityRename(location, normalizedPath, nextRelativePath, kind);
|
|
716
|
+
}
|
|
717
|
+
catch (error) {
|
|
718
|
+
try {
|
|
719
|
+
await this.recoverCommittedRemoteAuthorityRename(location, normalizedPath, nextRelativePath, kind);
|
|
720
|
+
}
|
|
721
|
+
catch (recoveryError) {
|
|
722
|
+
throw new Error(`Remote library rename succeeded, but post-rename synchronization failed: ${String(recoveryError?.message ?? recoveryError)}`);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
await renameAuthorityLibraryPath(location.libraryRootPath, normalizedPath, nextRelativePath);
|
|
728
|
+
await this.finalizeAuthorityRename(location, normalizedPath, nextRelativePath, kind);
|
|
729
|
+
}
|
|
730
|
+
async deleteEntry(userId, relativePath, kind, scope) {
|
|
731
|
+
const location = this.getRequiredLibraryLocation(userId, scope);
|
|
732
|
+
const { normalizedPath } = resolveLibraryAbsolutePath(location.libraryRootPath, relativePath);
|
|
733
|
+
assertLibraryWritePathAllowed(normalizedPath);
|
|
734
|
+
const entryKind = await this.getAuthorityEntryKind(location, normalizedPath);
|
|
735
|
+
if (!entryKind) {
|
|
736
|
+
throw new Error('Path not found');
|
|
737
|
+
}
|
|
738
|
+
if (entryKind !== kind) {
|
|
739
|
+
throw new Error(kind === 'file' ? 'Path is not a file' : 'Path is not a directory');
|
|
740
|
+
}
|
|
741
|
+
if (kind === 'file' && !normalizedPath.toLowerCase().endsWith('.md')) {
|
|
742
|
+
throw new Error('Only markdown files can be deleted from the library');
|
|
743
|
+
}
|
|
744
|
+
if (location.nodeId) {
|
|
745
|
+
await this.workspaceBroker.deleteLibraryPath(location.nodeId, location.libraryRootPath, normalizedPath, { recursive: kind === 'directory', agentName: location.agentName });
|
|
746
|
+
}
|
|
747
|
+
await deleteAuthorityLibraryPath(location.libraryRootPath, normalizedPath);
|
|
748
|
+
const existingManifest = await this.readLibraryManifest(location);
|
|
749
|
+
const nextManifest = {
|
|
750
|
+
...existingManifest,
|
|
751
|
+
documents: existingManifest.documents.filter((doc) => doc.relativePath !== normalizedPath
|
|
752
|
+
&& !doc.relativePath.startsWith(`${normalizedPath}/`)),
|
|
753
|
+
};
|
|
754
|
+
await this.writeLibraryManifest(location, nextManifest);
|
|
755
|
+
await this.reconcileIndexedDocumentsForLocation(location);
|
|
756
|
+
}
|
|
757
|
+
async prepareRunLibraryMirror(params) {
|
|
758
|
+
this.deleteRunLibraryMirror(params.runId);
|
|
759
|
+
if (!params.workspacePath?.trim())
|
|
760
|
+
return;
|
|
761
|
+
const sharedLocation = this.getSharedMirrorLocation();
|
|
762
|
+
const personalLocation = params.personalEnabled && params.userId
|
|
763
|
+
? await this.getOrCreatePersonalMirrorLocation(params.userId)
|
|
764
|
+
: null;
|
|
765
|
+
const sharedTree = sharedLocation ? await this.buildAuthorityMirrorTree(sharedLocation) : null;
|
|
766
|
+
const personalTree = personalLocation ? await this.buildAuthorityMirrorTree(personalLocation) : null;
|
|
767
|
+
await this.workspaceBroker.applyLibraryMirror(params.nodeId, params.workspacePath, {
|
|
768
|
+
shared: sharedTree,
|
|
769
|
+
personal: personalTree,
|
|
770
|
+
});
|
|
771
|
+
const now = Date.now();
|
|
772
|
+
this.db.prepare(`INSERT INTO library_run_mirrors(
|
|
773
|
+
run_id, conversation_id, user_id, node_id, workspace_path,
|
|
774
|
+
shared_baseline_json, personal_baseline_json, created_at, updated_at
|
|
775
|
+
) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
776
|
+
ON CONFLICT(run_id) DO UPDATE SET
|
|
777
|
+
conversation_id = excluded.conversation_id,
|
|
778
|
+
user_id = excluded.user_id,
|
|
779
|
+
node_id = excluded.node_id,
|
|
780
|
+
workspace_path = excluded.workspace_path,
|
|
781
|
+
shared_baseline_json = excluded.shared_baseline_json,
|
|
782
|
+
personal_baseline_json = excluded.personal_baseline_json,
|
|
783
|
+
updated_at = excluded.updated_at`).run(params.runId, params.conversationId, params.userId, params.nodeId, params.workspacePath, serializeLibraryMirrorBaseline(buildLibraryMirrorBaseline(sharedTree)), serializeLibraryMirrorBaseline(buildLibraryMirrorBaseline(personalTree)), now, now);
|
|
784
|
+
}
|
|
785
|
+
async finalizeRunLibraryMirror(params) {
|
|
786
|
+
const runMirror = this.db.prepare(`SELECT run_id as runId,
|
|
787
|
+
conversation_id as conversationId,
|
|
788
|
+
user_id as userId,
|
|
789
|
+
node_id as nodeId,
|
|
790
|
+
workspace_path as workspacePath,
|
|
791
|
+
shared_baseline_json as sharedBaselineJson,
|
|
792
|
+
personal_baseline_json as personalBaselineJson
|
|
793
|
+
FROM library_run_mirrors
|
|
794
|
+
WHERE run_id = ?`).get(params.runId);
|
|
795
|
+
if (!runMirror)
|
|
796
|
+
return { warnings: [] };
|
|
797
|
+
let snapshot;
|
|
798
|
+
try {
|
|
799
|
+
snapshot = await this.workspaceBroker.snapshotLibraryMirror(runMirror.nodeId, runMirror.workspacePath);
|
|
800
|
+
}
|
|
801
|
+
catch (error) {
|
|
802
|
+
this.deleteRunLibraryMirror(params.runId);
|
|
803
|
+
throw error;
|
|
804
|
+
}
|
|
805
|
+
const warnings = [];
|
|
806
|
+
const sharedLocation = this.getSharedMirrorLocation();
|
|
807
|
+
const personalLocation = runMirror.userId ? this.getLibraryLocation(runMirror.userId, 'personal') : null;
|
|
808
|
+
warnings.push(...await this.writeBackMirrorScope({
|
|
809
|
+
runId: params.runId,
|
|
810
|
+
scope: 'shared',
|
|
811
|
+
location: sharedLocation,
|
|
812
|
+
baseline: parseLibraryMirrorBaseline(runMirror.sharedBaselineJson),
|
|
813
|
+
current: snapshot.shared,
|
|
814
|
+
}));
|
|
815
|
+
warnings.push(...await this.writeBackMirrorScope({
|
|
816
|
+
runId: params.runId,
|
|
817
|
+
scope: 'personal',
|
|
818
|
+
location: personalLocation,
|
|
819
|
+
baseline: parseLibraryMirrorBaseline(runMirror.personalBaselineJson),
|
|
820
|
+
current: snapshot.personal,
|
|
821
|
+
}));
|
|
822
|
+
this.deleteRunLibraryMirror(params.runId);
|
|
823
|
+
return { warnings };
|
|
824
|
+
}
|
|
825
|
+
abortRunLibraryMirror(runId) {
|
|
826
|
+
this.deleteRunLibraryMirror(runId);
|
|
827
|
+
}
|
|
828
|
+
deleteRunLibraryMirror(runId) {
|
|
829
|
+
this.db.prepare('DELETE FROM library_run_mirrors WHERE run_id = ?').run(runId);
|
|
830
|
+
}
|
|
831
|
+
getSharedMirrorLocation() {
|
|
832
|
+
const config = this.getSharedConfig();
|
|
833
|
+
if (!config)
|
|
834
|
+
return null;
|
|
835
|
+
ensureLocalLibraryScaffold(config.libraryRootPath);
|
|
836
|
+
return {
|
|
837
|
+
scope: 'shared',
|
|
838
|
+
libraryRootPath: config.libraryRootPath,
|
|
839
|
+
nodeId: null,
|
|
840
|
+
agentName: SHARED_LIBRARY_AGENT_NAME,
|
|
841
|
+
sharedKey: config.sharedKey,
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
async getOrCreatePersonalMirrorLocation(userId) {
|
|
845
|
+
const existing = this.getLibraryLocation(userId, 'personal');
|
|
846
|
+
if (existing)
|
|
847
|
+
return existing;
|
|
848
|
+
const username = this.getUsernameForUserId(userId);
|
|
849
|
+
if (!username)
|
|
850
|
+
throw new Error(`Missing username for library mirror user ${userId}`);
|
|
851
|
+
const config = await this.ensureConfigForUser({ userId, username });
|
|
852
|
+
return {
|
|
853
|
+
scope: 'personal',
|
|
854
|
+
libraryRootPath: config.libraryRootPath,
|
|
855
|
+
nodeId: config.nodeId,
|
|
856
|
+
agentName: LIBRARY_CURATOR_AGENT_NAME,
|
|
857
|
+
userId,
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
async buildAuthorityMirrorTree(location) {
|
|
861
|
+
return {
|
|
862
|
+
files: await this.collectAuthorityMirrorFiles(location),
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
async writeBackMirrorScope(params) {
|
|
866
|
+
const baselineFiles = filterMirrorBaselineFiles(params.baseline);
|
|
867
|
+
const currentFiles = filterMirrorCurrentFiles(params.current);
|
|
868
|
+
if (baselineFiles.length === 0 && currentFiles.length === 0) {
|
|
869
|
+
return [];
|
|
870
|
+
}
|
|
871
|
+
if (!params.location) {
|
|
872
|
+
return [`Document sync warning: skipped ${params.scope} library writeback because the authority root is unavailable.`];
|
|
873
|
+
}
|
|
874
|
+
if (!params.current && baselineFiles.length > 0) {
|
|
875
|
+
return [
|
|
876
|
+
`Document sync warning: skipped ${params.scope} library writeback because the mirrored scope root disappeared during the run. Preserved the authority library unchanged.`,
|
|
877
|
+
];
|
|
878
|
+
}
|
|
879
|
+
if (baselineFiles.length > 0 && currentFiles.length === 0) {
|
|
880
|
+
return [
|
|
881
|
+
`Document sync warning: skipped ${params.scope} library writeback because the run would remove every document in that scope. Preserved the authority library unchanged.`,
|
|
882
|
+
];
|
|
883
|
+
}
|
|
884
|
+
const baselineMap = new Map(baselineFiles.map((file) => [file.path, file.sha256]));
|
|
885
|
+
const currentMap = new Map(currentFiles.map((file) => [file.path, file]));
|
|
886
|
+
const changedPaths = Array.from(new Set([
|
|
887
|
+
...baselineMap.keys(),
|
|
888
|
+
...currentMap.keys(),
|
|
889
|
+
]))
|
|
890
|
+
.filter((relativePath) => baselineMap.get(relativePath) !== currentMap.get(relativePath)?.sha256)
|
|
891
|
+
.sort((left, right) => left.localeCompare(right));
|
|
892
|
+
const warnings = [];
|
|
893
|
+
for (const relativePath of changedPaths) {
|
|
894
|
+
const warning = await this.enqueueWriteback(buildWritebackKey(params.location, relativePath), async () => this.applyMirrorFileChange({
|
|
895
|
+
runId: params.runId,
|
|
896
|
+
location: params.location,
|
|
897
|
+
relativePath,
|
|
898
|
+
baselineSha: baselineMap.get(relativePath) ?? null,
|
|
899
|
+
currentFile: currentMap.get(relativePath) ?? null,
|
|
900
|
+
}));
|
|
901
|
+
if (warning)
|
|
902
|
+
warnings.push(warning);
|
|
903
|
+
}
|
|
904
|
+
await this.rebuildAuthoritySystemFiles(params.location);
|
|
905
|
+
await this.reconcileIndexedDocumentsForLocation(params.location);
|
|
906
|
+
return warnings;
|
|
907
|
+
}
|
|
908
|
+
async enqueueWriteback(key, work) {
|
|
909
|
+
const chained = (this.writebackChains.get(key) ?? Promise.resolve())
|
|
910
|
+
.catch(() => undefined)
|
|
911
|
+
.then(work);
|
|
912
|
+
const barrier = chained.then(() => undefined, () => undefined);
|
|
913
|
+
this.writebackChains.set(key, barrier);
|
|
914
|
+
try {
|
|
915
|
+
return await chained;
|
|
916
|
+
}
|
|
917
|
+
finally {
|
|
918
|
+
if (this.writebackChains.get(key) === barrier) {
|
|
919
|
+
this.writebackChains.delete(key);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
async applyMirrorFileChange(params) {
|
|
924
|
+
const authorityFile = await this.readAuthorityLibraryFile(params.location, params.relativePath);
|
|
925
|
+
const authoritySha = authorityFile ? sha256Text(authorityFile.content) : null;
|
|
926
|
+
if (authoritySha !== params.baselineSha) {
|
|
927
|
+
const conflictArtifactPath = await this.writeConflictArtifact({
|
|
928
|
+
runId: params.runId,
|
|
929
|
+
location: params.location,
|
|
930
|
+
relativePath: params.relativePath,
|
|
931
|
+
currentFile: params.currentFile,
|
|
932
|
+
baselineSha: params.baselineSha,
|
|
933
|
+
authoritySha,
|
|
934
|
+
});
|
|
935
|
+
return [
|
|
936
|
+
'Document sync warning:',
|
|
937
|
+
`skipped ${params.location.scope}/${params.relativePath} because the authority changed during the run.`,
|
|
938
|
+
conflictArtifactPath ? `Preserved the agent version at ${conflictArtifactPath}.` : 'Recorded the conflict metadata for manual review.',
|
|
939
|
+
].join(' ');
|
|
940
|
+
}
|
|
941
|
+
if (!params.currentFile) {
|
|
942
|
+
await this.deleteAuthorityLibraryPath(params.location, params.relativePath);
|
|
943
|
+
return undefined;
|
|
944
|
+
}
|
|
945
|
+
if (sha256Text(params.currentFile.content.replace(/\r\n/g, '\n')) !== params.currentFile.sha256) {
|
|
946
|
+
throw new Error(`Invalid mirror snapshot checksum for ${params.location.scope}/${params.relativePath}`);
|
|
947
|
+
}
|
|
948
|
+
await this.writeAuthorityLibraryFile(params.location, params.relativePath, params.currentFile.content);
|
|
949
|
+
return undefined;
|
|
950
|
+
}
|
|
951
|
+
async writeAuthorityLibraryFile(location, relativePath, content) {
|
|
952
|
+
const { normalizedPath, absolutePath } = resolveLibraryAbsolutePath(location.libraryRootPath, relativePath);
|
|
953
|
+
const nextContent = content.replace(/\r\n/g, '\n');
|
|
954
|
+
if (location.nodeId) {
|
|
955
|
+
await this.workspaceBroker.writeFile(location.nodeId, location.libraryRootPath, normalizedPath, nextContent, 'overwrite', { scaffold: false, agentName: location.agentName });
|
|
956
|
+
}
|
|
957
|
+
const fsPromises = await import('node:fs/promises');
|
|
958
|
+
await fsPromises.mkdir(path.dirname(absolutePath), { recursive: true });
|
|
959
|
+
await fsPromises.writeFile(absolutePath, nextContent, 'utf8');
|
|
960
|
+
}
|
|
961
|
+
async rebuildAuthoritySystemFiles(location) {
|
|
962
|
+
ensureLocalLibraryScaffold(location.libraryRootPath);
|
|
963
|
+
const existingManifest = await this.readAuthorityLibraryManifestOrDefault(location);
|
|
964
|
+
const manifestByPath = new Map(existingManifest.documents.map((doc) => [normalizeLibraryRelativePath(doc.relativePath), doc]));
|
|
965
|
+
const markdownPaths = await this.collectAuthorityMirrorDocumentPaths(location);
|
|
966
|
+
const nextDocuments = [];
|
|
967
|
+
for (const relativePath of markdownPaths) {
|
|
968
|
+
const file = await this.readAuthorityLibraryFile(location, relativePath);
|
|
969
|
+
if (!file)
|
|
970
|
+
continue;
|
|
971
|
+
const existing = manifestByPath.get(relativePath);
|
|
972
|
+
nextDocuments.push({
|
|
973
|
+
title: extractLibraryDocumentTitle(file.content)
|
|
974
|
+
?? existing?.title?.trim()
|
|
975
|
+
?? path.posix.basename(relativePath, path.posix.extname(relativePath)),
|
|
976
|
+
category: (existing?.category ?? inferLibraryCategory(relativePath)).trim(),
|
|
977
|
+
topicPath: (existing?.topicPath ?? inferLibraryTopicPath(relativePath)).trim(),
|
|
978
|
+
relativePath,
|
|
979
|
+
summary: (existing?.summary ?? '').trim(),
|
|
980
|
+
sourceSavedItemIds: Array.isArray(existing?.sourceSavedItemIds)
|
|
981
|
+
? existing.sourceSavedItemIds.filter((id) => typeof id === 'string' && id.trim())
|
|
982
|
+
: [],
|
|
983
|
+
notionPageId: existing?.notionPageId ?? null,
|
|
984
|
+
notionPageUrl: existing?.notionPageUrl ?? null,
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
const nextManifest = {
|
|
988
|
+
version: existingManifest.version || 1,
|
|
989
|
+
processedSavedItemIds: existingManifest.processedSavedItemIds ?? [],
|
|
990
|
+
documents: nextDocuments.sort((left, right) => left.relativePath.localeCompare(right.relativePath)),
|
|
991
|
+
};
|
|
992
|
+
await this.writeLibraryManifest(location, nextManifest);
|
|
993
|
+
await this.ensureAuthorityIndexFile(location);
|
|
994
|
+
}
|
|
995
|
+
async readAuthorityLibraryManifestOrDefault(location) {
|
|
996
|
+
const manifestFile = await this.readAuthorityLibraryFile(location, LIBRARY_MANIFEST_RELATIVE_PATH);
|
|
997
|
+
if (!manifestFile)
|
|
998
|
+
return buildDefaultLibraryManifest();
|
|
999
|
+
return parseLibraryManifest(manifestFile.content) ?? buildDefaultLibraryManifest();
|
|
1000
|
+
}
|
|
1001
|
+
async ensureAuthorityIndexFile(location) {
|
|
1002
|
+
const indexFile = await this.readAuthorityLibraryFile(location, 'INDEX.md');
|
|
1003
|
+
const nextContent = indexFile?.content?.trim()
|
|
1004
|
+
? indexFile.content.replace(/\r\n/g, '\n')
|
|
1005
|
+
: buildDefaultLibraryIndexContent();
|
|
1006
|
+
await this.writeAuthorityLibraryFile(location, 'INDEX.md', nextContent);
|
|
1007
|
+
}
|
|
1008
|
+
async collectAuthorityMirrorFiles(location) {
|
|
1009
|
+
ensureLocalLibraryScaffold(location.libraryRootPath);
|
|
1010
|
+
if (!location.nodeId) {
|
|
1011
|
+
return collectLibraryMirrorFiles(location.libraryRootPath);
|
|
1012
|
+
}
|
|
1013
|
+
return this.collectRemoteLibraryMirrorFiles(location, '');
|
|
1014
|
+
}
|
|
1015
|
+
async collectRemoteLibraryMirrorFiles(location, relativePath) {
|
|
1016
|
+
const entries = await this.listAllRemoteLibraryEntries(location, relativePath);
|
|
1017
|
+
const files = [];
|
|
1018
|
+
for (const entry of entries) {
|
|
1019
|
+
if (entry.isSymlink)
|
|
1020
|
+
continue;
|
|
1021
|
+
if (shouldSkipLibraryMirrorEntry(entry.path, entry.kind))
|
|
1022
|
+
continue;
|
|
1023
|
+
if (entry.kind === 'directory') {
|
|
1024
|
+
files.push(...await this.collectRemoteLibraryMirrorFiles(location, entry.path));
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
const file = await this.readAuthorityLibraryFile(location, entry.path);
|
|
1028
|
+
if (!file)
|
|
1029
|
+
continue;
|
|
1030
|
+
files.push({
|
|
1031
|
+
path: entry.path,
|
|
1032
|
+
content: file.content,
|
|
1033
|
+
sha256: sha256Text(file.content),
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
return files.sort((left, right) => left.path.localeCompare(right.path));
|
|
1037
|
+
}
|
|
1038
|
+
async listAllRemoteLibraryEntries(location, relativePath) {
|
|
1039
|
+
const entries = [];
|
|
1040
|
+
let cursor = null;
|
|
1041
|
+
do {
|
|
1042
|
+
const page = await this.workspaceBroker.listDirectory(location.nodeId, location.libraryRootPath, relativePath, {
|
|
1043
|
+
scaffold: false,
|
|
1044
|
+
agentName: location.agentName,
|
|
1045
|
+
...(cursor ? { cursor } : {}),
|
|
1046
|
+
limit: 1000,
|
|
1047
|
+
});
|
|
1048
|
+
entries.push(...page.entries.map((entry) => ({
|
|
1049
|
+
name: entry.name,
|
|
1050
|
+
path: entry.path,
|
|
1051
|
+
kind: entry.kind,
|
|
1052
|
+
...(entry.isSymlink ? { isSymlink: true } : {}),
|
|
1053
|
+
})));
|
|
1054
|
+
cursor = page.directoryPage?.nextCursor ?? null;
|
|
1055
|
+
if (!page.directoryPage?.hasMore)
|
|
1056
|
+
break;
|
|
1057
|
+
} while (cursor);
|
|
1058
|
+
entries.sort((left, right) => {
|
|
1059
|
+
if (left.kind !== right.kind)
|
|
1060
|
+
return left.kind === 'directory' ? -1 : 1;
|
|
1061
|
+
return left.name.localeCompare(right.name);
|
|
1062
|
+
});
|
|
1063
|
+
return entries;
|
|
1064
|
+
}
|
|
1065
|
+
async collectAuthorityMirrorDocumentPaths(location) {
|
|
1066
|
+
if (!location.nodeId) {
|
|
1067
|
+
return collectAuthorityMirrorDocumentPaths(location.libraryRootPath);
|
|
1068
|
+
}
|
|
1069
|
+
return collectLibraryFilePaths(await this.buildDocumentTreeForLocation(location))
|
|
1070
|
+
.filter((relativePath) => isMirrorWritableLibraryDocumentPath(relativePath))
|
|
1071
|
+
.sort((left, right) => left.localeCompare(right));
|
|
1072
|
+
}
|
|
1073
|
+
async readAuthorityLibraryFile(location, relativePath) {
|
|
1074
|
+
if (!location.nodeId) {
|
|
1075
|
+
return readAuthorityLibraryFile(location.libraryRootPath, relativePath);
|
|
1076
|
+
}
|
|
1077
|
+
try {
|
|
1078
|
+
const file = await this.workspaceBroker.readFile(location.nodeId, location.libraryRootPath, relativePath, {
|
|
1079
|
+
scaffold: false,
|
|
1080
|
+
agentName: location.agentName,
|
|
1081
|
+
});
|
|
1082
|
+
return {
|
|
1083
|
+
content: file.content.replace(/\r\n/g, '\n'),
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
catch (error) {
|
|
1087
|
+
if (isLibraryPathMissingError(error))
|
|
1088
|
+
return null;
|
|
1089
|
+
throw error;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
async getAuthorityEntryKind(location, relativePath) {
|
|
1093
|
+
const normalizedPath = normalizeLibraryRelativePath(relativePath);
|
|
1094
|
+
if (!location.nodeId) {
|
|
1095
|
+
return getAuthorityEntryKind(location.libraryRootPath, normalizedPath);
|
|
1096
|
+
}
|
|
1097
|
+
try {
|
|
1098
|
+
await this.workspaceBroker.listDirectory(location.nodeId, location.libraryRootPath, normalizedPath, {
|
|
1099
|
+
scaffold: false,
|
|
1100
|
+
agentName: location.agentName,
|
|
1101
|
+
limit: 1,
|
|
1102
|
+
});
|
|
1103
|
+
return 'directory';
|
|
1104
|
+
}
|
|
1105
|
+
catch (error) {
|
|
1106
|
+
if (!isLibraryPathMissingError(error) && !isLibraryPathNotDirectoryError(error)) {
|
|
1107
|
+
throw error;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
try {
|
|
1111
|
+
await this.workspaceBroker.readFile(location.nodeId, location.libraryRootPath, normalizedPath, {
|
|
1112
|
+
scaffold: false,
|
|
1113
|
+
agentName: location.agentName,
|
|
1114
|
+
});
|
|
1115
|
+
return 'file';
|
|
1116
|
+
}
|
|
1117
|
+
catch (error) {
|
|
1118
|
+
if (isLibraryPathMissingError(error))
|
|
1119
|
+
return null;
|
|
1120
|
+
if (isLibraryPathDirectoryReadError(error))
|
|
1121
|
+
return 'directory';
|
|
1122
|
+
throw error;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
async deleteAuthorityLibraryPath(location, relativePath) {
|
|
1126
|
+
if (location.nodeId) {
|
|
1127
|
+
await this.workspaceBroker.deleteLibraryPath(location.nodeId, location.libraryRootPath, relativePath, { recursive: false, agentName: location.agentName });
|
|
1128
|
+
}
|
|
1129
|
+
await deleteAuthorityLibraryPath(location.libraryRootPath, relativePath);
|
|
1130
|
+
}
|
|
1131
|
+
async syncRemoteAuthorityRenameToLocalCache(location, previousRelativePath, nextRelativePath, kind) {
|
|
1132
|
+
if (!location.nodeId)
|
|
1133
|
+
return;
|
|
1134
|
+
if (kind === 'file') {
|
|
1135
|
+
const content = await this.captureRemoteAuthorityFileSnapshot(location, nextRelativePath);
|
|
1136
|
+
await deleteAuthorityLibraryPath(location.libraryRootPath, previousRelativePath);
|
|
1137
|
+
await this.writeRemoteAuthorityFileSnapshotToLocalCache(location.libraryRootPath, nextRelativePath, content);
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
const snapshot = await this.captureRemoteAuthorityDirectorySnapshot(location, nextRelativePath);
|
|
1141
|
+
await deleteAuthorityLibraryPath(location.libraryRootPath, previousRelativePath);
|
|
1142
|
+
await this.writeRemoteAuthorityDirectorySnapshotToLocalCache(location.libraryRootPath, nextRelativePath, snapshot);
|
|
1143
|
+
}
|
|
1144
|
+
async recoverCommittedRemoteAuthorityRename(location, previousRelativePath, nextRelativePath, kind) {
|
|
1145
|
+
await this.syncRemoteAuthorityRenameToLocalCache(location, previousRelativePath, nextRelativePath, kind);
|
|
1146
|
+
await this.finalizeAuthorityRename(location, previousRelativePath, nextRelativePath, kind);
|
|
1147
|
+
}
|
|
1148
|
+
async didRemoteAuthorityRenameCommit(location, previousRelativePath, nextRelativePath, kind) {
|
|
1149
|
+
try {
|
|
1150
|
+
const [previousKind, nextKind] = await Promise.all([
|
|
1151
|
+
this.getAuthorityEntryKind(location, previousRelativePath),
|
|
1152
|
+
this.getAuthorityEntryKind(location, nextRelativePath),
|
|
1153
|
+
]);
|
|
1154
|
+
return previousKind === null && nextKind === kind;
|
|
1155
|
+
}
|
|
1156
|
+
catch {
|
|
1157
|
+
return false;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
async captureRemoteAuthorityFileSnapshot(location, relativePath) {
|
|
1161
|
+
if (!location.nodeId)
|
|
1162
|
+
throw new Error('Missing node-backed authority');
|
|
1163
|
+
const normalizedPath = normalizeLibraryRelativePath(relativePath);
|
|
1164
|
+
const file = await this.workspaceBroker.readFile(location.nodeId, location.libraryRootPath, normalizedPath, {
|
|
1165
|
+
scaffold: false,
|
|
1166
|
+
agentName: location.agentName,
|
|
1167
|
+
});
|
|
1168
|
+
return file.content.replace(/\r\n/g, '\n');
|
|
1169
|
+
}
|
|
1170
|
+
async captureRemoteAuthorityDirectorySnapshot(location, relativePath) {
|
|
1171
|
+
if (!location.nodeId)
|
|
1172
|
+
throw new Error('Missing node-backed authority');
|
|
1173
|
+
const normalizedPath = normalizeLibraryRelativePath(relativePath);
|
|
1174
|
+
const directories = [normalizedPath];
|
|
1175
|
+
const files = [];
|
|
1176
|
+
const walk = async (directoryPath) => {
|
|
1177
|
+
const entries = await this.listAllRemoteLibraryEntries(location, directoryPath);
|
|
1178
|
+
for (const entry of entries) {
|
|
1179
|
+
if (entry.isSymlink)
|
|
1180
|
+
continue;
|
|
1181
|
+
const entryPath = normalizeLibraryRelativePath(entry.path);
|
|
1182
|
+
if (entry.kind === 'directory') {
|
|
1183
|
+
directories.push(entryPath);
|
|
1184
|
+
await walk(entryPath);
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1187
|
+
const file = await this.workspaceBroker.readFile(location.nodeId, location.libraryRootPath, entryPath, {
|
|
1188
|
+
scaffold: false,
|
|
1189
|
+
agentName: location.agentName,
|
|
1190
|
+
});
|
|
1191
|
+
files.push({
|
|
1192
|
+
path: entryPath,
|
|
1193
|
+
content: file.content.replace(/\r\n/g, '\n'),
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
};
|
|
1197
|
+
await walk(normalizedPath);
|
|
1198
|
+
directories.sort((left, right) => left.localeCompare(right));
|
|
1199
|
+
files.sort((left, right) => left.path.localeCompare(right.path));
|
|
1200
|
+
return { directories, files };
|
|
1201
|
+
}
|
|
1202
|
+
async writeRemoteAuthorityFileSnapshotToLocalCache(libraryRootPath, relativePath, content) {
|
|
1203
|
+
const normalizedPath = normalizeLibraryRelativePath(relativePath);
|
|
1204
|
+
const existingKind = await getAuthorityEntryKind(libraryRootPath, normalizedPath);
|
|
1205
|
+
if (existingKind === 'directory') {
|
|
1206
|
+
await deleteAuthorityLibraryPath(libraryRootPath, normalizedPath);
|
|
1207
|
+
}
|
|
1208
|
+
const { absolutePath } = resolveLibraryAbsolutePath(libraryRootPath, normalizedPath);
|
|
1209
|
+
const fsPromises = await import('node:fs/promises');
|
|
1210
|
+
await fsPromises.mkdir(path.dirname(absolutePath), { recursive: true });
|
|
1211
|
+
await fsPromises.writeFile(absolutePath, content, 'utf8');
|
|
1212
|
+
}
|
|
1213
|
+
async writeRemoteAuthorityDirectorySnapshotToLocalCache(libraryRootPath, relativePath, snapshot) {
|
|
1214
|
+
const normalizedRoot = normalizeLibraryRelativePath(relativePath);
|
|
1215
|
+
const existingRootKind = await getAuthorityEntryKind(libraryRootPath, normalizedRoot);
|
|
1216
|
+
if (existingRootKind === 'file') {
|
|
1217
|
+
await deleteAuthorityLibraryPath(libraryRootPath, normalizedRoot);
|
|
1218
|
+
}
|
|
1219
|
+
const existingEntries = await collectAuthoritySubtreeEntries(libraryRootPath, normalizedRoot);
|
|
1220
|
+
const remoteDirectorySet = new Set(snapshot.directories);
|
|
1221
|
+
const remoteFileSet = new Set(snapshot.files.map((file) => file.path));
|
|
1222
|
+
for (const existingFile of existingEntries.files) {
|
|
1223
|
+
if (!remoteFileSet.has(existingFile)) {
|
|
1224
|
+
await deleteAuthorityLibraryPath(libraryRootPath, existingFile);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
for (const existingDirectory of existingEntries.directories.sort((left, right) => right.length - left.length)) {
|
|
1228
|
+
if (existingDirectory === normalizedRoot)
|
|
1229
|
+
continue;
|
|
1230
|
+
if (!remoteDirectorySet.has(existingDirectory)) {
|
|
1231
|
+
await deleteAuthorityLibraryPath(libraryRootPath, existingDirectory);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
const fsPromises = await import('node:fs/promises');
|
|
1235
|
+
for (const directoryPath of snapshot.directories.sort((left, right) => left.length - right.length)) {
|
|
1236
|
+
const directoryKind = await getAuthorityEntryKind(libraryRootPath, directoryPath);
|
|
1237
|
+
if (directoryKind === 'file') {
|
|
1238
|
+
await deleteAuthorityLibraryPath(libraryRootPath, directoryPath);
|
|
1239
|
+
}
|
|
1240
|
+
const { absolutePath } = resolveLibraryAbsolutePath(libraryRootPath, directoryPath);
|
|
1241
|
+
await fsPromises.mkdir(absolutePath, { recursive: true });
|
|
1242
|
+
}
|
|
1243
|
+
for (const file of snapshot.files) {
|
|
1244
|
+
const existingKind = await getAuthorityEntryKind(libraryRootPath, file.path);
|
|
1245
|
+
if (existingKind === 'directory') {
|
|
1246
|
+
await deleteAuthorityLibraryPath(libraryRootPath, file.path);
|
|
1247
|
+
}
|
|
1248
|
+
const { absolutePath } = resolveLibraryAbsolutePath(libraryRootPath, file.path);
|
|
1249
|
+
await fsPromises.mkdir(path.dirname(absolutePath), { recursive: true });
|
|
1250
|
+
await fsPromises.writeFile(absolutePath, file.content, 'utf8');
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
async finalizeAuthorityRename(location, previousRelativePath, nextRelativePath, kind) {
|
|
1254
|
+
const existingManifest = await this.readLibraryManifest(location);
|
|
1255
|
+
const nextManifest = renameLibraryManifestPaths(existingManifest, previousRelativePath, nextRelativePath, kind);
|
|
1256
|
+
await this.writeLibraryManifest(location, nextManifest);
|
|
1257
|
+
await this.reconcileIndexedDocumentsForLocation(location);
|
|
1258
|
+
}
|
|
1259
|
+
async writeConflictArtifact(params) {
|
|
1260
|
+
const scopeDirectory = params.location.scope;
|
|
1261
|
+
const ownerSegment = params.location.scope === 'shared'
|
|
1262
|
+
? (params.location.sharedKey ?? SHARED_LIBRARY_KEY)
|
|
1263
|
+
: (this.getUsernameForUserId(params.location.userId ?? '') ?? params.location.userId ?? 'user');
|
|
1264
|
+
const baseDirectory = path.join(this.workspaceRoot, '.library-curator-conflicts', scopeDirectory, sanitizeConflictPathSegment(ownerSegment), params.runId);
|
|
1265
|
+
const targetRelativePath = params.currentFile
|
|
1266
|
+
? params.relativePath
|
|
1267
|
+
: `${params.relativePath}.delete-conflict.json`;
|
|
1268
|
+
const targetPath = path.join(baseDirectory, targetRelativePath);
|
|
1269
|
+
const fsPromises = await import('node:fs/promises');
|
|
1270
|
+
await fsPromises.mkdir(path.dirname(targetPath), { recursive: true });
|
|
1271
|
+
const content = params.currentFile
|
|
1272
|
+
? params.currentFile.content.replace(/\r\n/g, '\n')
|
|
1273
|
+
: JSON.stringify({
|
|
1274
|
+
kind: 'delete_conflict',
|
|
1275
|
+
relativePath: params.relativePath,
|
|
1276
|
+
baselineSha256: params.baselineSha,
|
|
1277
|
+
authoritySha256: params.authoritySha,
|
|
1278
|
+
runId: params.runId,
|
|
1279
|
+
}, null, 2) + '\n';
|
|
1280
|
+
await fsPromises.writeFile(targetPath, content, 'utf8');
|
|
1281
|
+
return targetPath;
|
|
1282
|
+
}
|
|
1283
|
+
getLibraryLocation(userId, scope) {
|
|
1284
|
+
if (scope === 'shared') {
|
|
1285
|
+
const config = this.getSharedConfig();
|
|
1286
|
+
if (!config)
|
|
1287
|
+
return null;
|
|
1288
|
+
return {
|
|
1289
|
+
scope,
|
|
1290
|
+
libraryRootPath: config.libraryRootPath,
|
|
1291
|
+
nodeId: null,
|
|
1292
|
+
agentName: SHARED_LIBRARY_AGENT_NAME,
|
|
1293
|
+
sharedKey: config.sharedKey,
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
const config = this.getConfig(userId);
|
|
1297
|
+
if (!config)
|
|
1298
|
+
return null;
|
|
1299
|
+
return {
|
|
1300
|
+
scope,
|
|
1301
|
+
libraryRootPath: config.libraryRootPath,
|
|
1302
|
+
nodeId: config.nodeId,
|
|
1303
|
+
agentName: LIBRARY_CURATOR_AGENT_NAME,
|
|
1304
|
+
userId,
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
getRequiredLibraryLocation(userId, scope) {
|
|
1308
|
+
const location = this.getLibraryLocation(userId, scope);
|
|
1309
|
+
if (location)
|
|
1310
|
+
return location;
|
|
1311
|
+
throw new Error(scope === 'shared' ? 'Shared library not configured' : 'Library config not found');
|
|
1312
|
+
}
|
|
1313
|
+
async buildDocumentTreeForLocation(location) {
|
|
1314
|
+
if (location.nodeId) {
|
|
1315
|
+
return this.listRemoteTree(location, '');
|
|
1316
|
+
}
|
|
1317
|
+
return this.listLocalTree(location, '');
|
|
1318
|
+
}
|
|
1319
|
+
async readLibraryFileFromLocation(location, relativePath) {
|
|
1320
|
+
const { normalizedPath, absolutePath } = resolveLibraryAbsolutePath(location.libraryRootPath, relativePath);
|
|
1321
|
+
if (location.nodeId) {
|
|
1322
|
+
const result = await this.workspaceBroker.readFile(location.nodeId, location.libraryRootPath, normalizedPath, {
|
|
1323
|
+
scaffold: false,
|
|
1324
|
+
agentName: location.agentName,
|
|
1325
|
+
});
|
|
1326
|
+
return {
|
|
1327
|
+
content: result.content,
|
|
1328
|
+
mimeType: normalizedPath.endsWith('.md') ? 'text/markdown' : 'text/plain',
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
const fsPromises = await import('node:fs/promises');
|
|
1332
|
+
const content = await fsPromises.readFile(absolutePath, 'utf8');
|
|
1333
|
+
return {
|
|
1334
|
+
content,
|
|
1335
|
+
mimeType: normalizedPath.endsWith('.md') ? 'text/markdown' : 'text/plain',
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
async listRemoteTree(location, relativePath) {
|
|
1339
|
+
const result = await this.workspaceBroker.listDirectory(location.nodeId, location.libraryRootPath, relativePath, {
|
|
1340
|
+
scaffold: false,
|
|
1341
|
+
agentName: location.agentName,
|
|
1342
|
+
});
|
|
1343
|
+
const nodes = [];
|
|
1344
|
+
for (const entry of result.entries) {
|
|
1345
|
+
if (shouldIgnoreLibraryEntry(relativePath, entry.name, entry.kind))
|
|
1346
|
+
continue;
|
|
1347
|
+
if (entry.kind === 'directory') {
|
|
1348
|
+
const childPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
1349
|
+
const children = await this.listRemoteTree(location, childPath);
|
|
1350
|
+
nodes.push({ scope: location.scope, name: entry.name, path: childPath, kind: 'directory', children });
|
|
1351
|
+
continue;
|
|
1352
|
+
}
|
|
1353
|
+
nodes.push({
|
|
1354
|
+
scope: location.scope,
|
|
1355
|
+
name: entry.name,
|
|
1356
|
+
path: relativePath ? `${relativePath}/${entry.name}` : entry.name,
|
|
1357
|
+
kind: 'file',
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
return sortLibraryTreeNodes(nodes);
|
|
1361
|
+
}
|
|
1362
|
+
async listLocalTree(location, relativePath) {
|
|
1363
|
+
const fsPromises = await import('node:fs/promises');
|
|
1364
|
+
const absolutePath = relativePath ? path.join(location.libraryRootPath, relativePath) : location.libraryRootPath;
|
|
1365
|
+
const entries = await fsPromises.readdir(absolutePath, { withFileTypes: true });
|
|
1366
|
+
const nodes = [];
|
|
1367
|
+
for (const entry of entries) {
|
|
1368
|
+
const kind = entry.isDirectory() ? 'directory' : 'file';
|
|
1369
|
+
if (shouldIgnoreLibraryEntry(relativePath, entry.name, kind))
|
|
1370
|
+
continue;
|
|
1371
|
+
if (entry.isDirectory()) {
|
|
1372
|
+
const childPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
1373
|
+
const children = await this.listLocalTree(location, childPath);
|
|
1374
|
+
nodes.push({ scope: location.scope, name: entry.name, path: childPath, kind: 'directory', children });
|
|
1375
|
+
continue;
|
|
1376
|
+
}
|
|
1377
|
+
nodes.push({
|
|
1378
|
+
scope: location.scope,
|
|
1379
|
+
name: entry.name,
|
|
1380
|
+
path: relativePath ? `${relativePath}/${entry.name}` : entry.name,
|
|
1381
|
+
kind: 'file',
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
return sortLibraryTreeNodes(nodes);
|
|
1385
|
+
}
|
|
1386
|
+
async readLibraryManifest(location) {
|
|
1387
|
+
let manifestRaw = '';
|
|
1388
|
+
if (location.nodeId) {
|
|
1389
|
+
try {
|
|
1390
|
+
manifestRaw = (await this.workspaceBroker.readFile(location.nodeId, location.libraryRootPath, LIBRARY_MANIFEST_RELATIVE_PATH, {
|
|
1391
|
+
scaffold: false,
|
|
1392
|
+
agentName: location.agentName,
|
|
1393
|
+
})).content;
|
|
1394
|
+
}
|
|
1395
|
+
catch (error) {
|
|
1396
|
+
if (isLibraryPathMissingError(error))
|
|
1397
|
+
return buildDefaultLibraryManifest();
|
|
1398
|
+
throw new Error(`Failed to read library manifest: ${String(error?.message ?? error)}`);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
else {
|
|
1402
|
+
const fsPromises = await import('node:fs/promises');
|
|
1403
|
+
const { absolutePath } = resolveLibraryAbsolutePath(location.libraryRootPath, LIBRARY_MANIFEST_RELATIVE_PATH);
|
|
1404
|
+
try {
|
|
1405
|
+
manifestRaw = await fsPromises.readFile(absolutePath, 'utf8');
|
|
1406
|
+
}
|
|
1407
|
+
catch (error) {
|
|
1408
|
+
if (isLibraryPathMissingError(error))
|
|
1409
|
+
return buildDefaultLibraryManifest();
|
|
1410
|
+
throw new Error(`Failed to read library manifest: ${String(error?.message ?? error)}`);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
const manifest = parseLibraryManifest(manifestRaw);
|
|
1414
|
+
if (!manifest)
|
|
1415
|
+
throw new Error('Invalid library manifest');
|
|
1416
|
+
return manifest;
|
|
1417
|
+
}
|
|
1418
|
+
async writeLibraryManifest(location, manifest) {
|
|
1419
|
+
const serialized = `${JSON.stringify(manifest, null, 2)}\n`;
|
|
1420
|
+
if (location.nodeId) {
|
|
1421
|
+
await this.workspaceBroker.writeFile(location.nodeId, location.libraryRootPath, LIBRARY_MANIFEST_RELATIVE_PATH, serialized, 'overwrite', { scaffold: false, agentName: location.agentName });
|
|
1422
|
+
}
|
|
1423
|
+
const fsPromises = await import('node:fs/promises');
|
|
1424
|
+
const { absolutePath } = resolveLibraryAbsolutePath(location.libraryRootPath, LIBRARY_MANIFEST_RELATIVE_PATH);
|
|
1425
|
+
await fsPromises.mkdir(path.dirname(absolutePath), { recursive: true });
|
|
1426
|
+
await fsPromises.writeFile(absolutePath, serialized, 'utf8');
|
|
1427
|
+
}
|
|
1428
|
+
queryIndexedDocumentsForLocation(location, category) {
|
|
1429
|
+
const normalizedCategory = category?.trim();
|
|
1430
|
+
const tableName = location.scope === 'shared' ? 'shared_library_documents' : 'library_documents';
|
|
1431
|
+
const ownerColumn = location.scope === 'shared' ? 'shared_key' : 'user_id';
|
|
1432
|
+
const ownerValue = location.scope === 'shared' ? location.sharedKey : location.userId;
|
|
1433
|
+
const rows = normalizedCategory
|
|
1434
|
+
? this.db.prepare(`SELECT id, ${location.scope === 'shared' ? 'NULL' : 'user_id'} as userId, title, category, topic_path as topicPath,
|
|
1435
|
+
relative_path as relativePath, summary, source_saved_item_ids as sourceSavedItemIdsJson,
|
|
1436
|
+
created_at as createdAt, updated_at as updatedAt, last_run_id as lastRunId,
|
|
1437
|
+
notion_page_id as notionPageId, notion_page_url as notionPageUrl
|
|
1438
|
+
FROM ${tableName}
|
|
1439
|
+
WHERE ${ownerColumn} = ? AND category = ?
|
|
1440
|
+
ORDER BY updated_at DESC`).all(ownerValue, normalizedCategory)
|
|
1441
|
+
: this.db.prepare(`SELECT id, ${location.scope === 'shared' ? 'NULL' : 'user_id'} as userId, title, category, topic_path as topicPath,
|
|
1442
|
+
relative_path as relativePath, summary, source_saved_item_ids as sourceSavedItemIdsJson,
|
|
1443
|
+
created_at as createdAt, updated_at as updatedAt, last_run_id as lastRunId,
|
|
1444
|
+
notion_page_id as notionPageId, notion_page_url as notionPageUrl
|
|
1445
|
+
FROM ${tableName}
|
|
1446
|
+
WHERE ${ownerColumn} = ?
|
|
1447
|
+
ORDER BY category COLLATE NOCASE ASC, topic_path COLLATE NOCASE ASC, updated_at DESC`).all(ownerValue);
|
|
1448
|
+
return rows.map((row) => mapLibraryDocumentRow(row, location.scope));
|
|
1449
|
+
}
|
|
1450
|
+
async reconcileIndexedDocumentsForLocation(location, lastRunId) {
|
|
1451
|
+
ensureLocalLibraryScaffold(location.libraryRootPath);
|
|
1452
|
+
const tree = await this.buildDocumentTreeForLocation(location);
|
|
1453
|
+
const markdownPaths = collectLibraryFilePaths(tree);
|
|
1454
|
+
const manifest = await this.readLibraryManifest(location);
|
|
1455
|
+
const manifestByPath = new Map(manifest.documents.map((doc) => [doc.relativePath.trim().replace(/^\/+/, ''), doc]));
|
|
1456
|
+
const existingDocs = this.queryIndexedDocumentsForLocation(location);
|
|
1457
|
+
const existingByPath = new Map(existingDocs.map((doc) => [doc.relativePath, doc]));
|
|
1458
|
+
const now = Date.now();
|
|
1459
|
+
const tableName = location.scope === 'shared' ? 'shared_library_documents' : 'library_documents';
|
|
1460
|
+
const ownerColumn = location.scope === 'shared' ? 'shared_key' : 'user_id';
|
|
1461
|
+
const ownerValue = location.scope === 'shared' ? location.sharedKey : location.userId;
|
|
1462
|
+
const upsert = this.db.prepare(`INSERT INTO ${tableName}(
|
|
1463
|
+
id, ${ownerColumn}, title, category, topic_path, relative_path, summary,
|
|
1464
|
+
source_saved_item_ids, created_at, updated_at, last_run_id,
|
|
1465
|
+
notion_page_id, notion_page_url
|
|
1466
|
+
) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1467
|
+
ON CONFLICT(${ownerColumn}, relative_path) DO UPDATE SET
|
|
1468
|
+
title = excluded.title,
|
|
1469
|
+
category = excluded.category,
|
|
1470
|
+
topic_path = excluded.topic_path,
|
|
1471
|
+
summary = excluded.summary,
|
|
1472
|
+
source_saved_item_ids = excluded.source_saved_item_ids,
|
|
1473
|
+
updated_at = excluded.updated_at,
|
|
1474
|
+
last_run_id = excluded.last_run_id,
|
|
1475
|
+
notion_page_id = excluded.notion_page_id,
|
|
1476
|
+
notion_page_url = excluded.notion_page_url`);
|
|
1477
|
+
const deleteStale = markdownPaths.length > 0
|
|
1478
|
+
? this.db.prepare(`DELETE FROM ${tableName}
|
|
1479
|
+
WHERE ${ownerColumn} = ?
|
|
1480
|
+
AND relative_path NOT IN (${markdownPaths.map(() => '?').join(', ')})`)
|
|
1481
|
+
: this.db.prepare(`DELETE FROM ${tableName} WHERE ${ownerColumn} = ?`);
|
|
1482
|
+
for (const relativePath of markdownPaths) {
|
|
1483
|
+
const file = await this.readLibraryFileFromLocation(location, relativePath);
|
|
1484
|
+
const existing = existingByPath.get(relativePath);
|
|
1485
|
+
const manifestDoc = manifestByPath.get(relativePath);
|
|
1486
|
+
const title = extractLibraryDocumentTitle(file.content)
|
|
1487
|
+
?? manifestDoc?.title?.trim()
|
|
1488
|
+
?? existing?.title
|
|
1489
|
+
?? path.posix.basename(relativePath, path.posix.extname(relativePath));
|
|
1490
|
+
const categoryValue = (manifestDoc?.category ?? existing?.category ?? inferLibraryCategory(relativePath)).trim();
|
|
1491
|
+
const topicPathValue = (manifestDoc?.topicPath ?? existing?.topicPath ?? inferLibraryTopicPath(relativePath)).trim();
|
|
1492
|
+
const summaryValue = (manifestDoc?.summary ?? existing?.summary ?? '').trim();
|
|
1493
|
+
const sourceSavedItemIds = Array.isArray(manifestDoc?.sourceSavedItemIds)
|
|
1494
|
+
? manifestDoc?.sourceSavedItemIds.filter((id) => typeof id === 'string' && id.trim())
|
|
1495
|
+
: (existing?.sourceSavedItemIds ?? []);
|
|
1496
|
+
const notionPageId = manifestDoc?.notionPageId ?? existing?.notionPageId ?? null;
|
|
1497
|
+
const notionPageUrl = manifestDoc?.notionPageUrl ?? existing?.notionPageUrl ?? null;
|
|
1498
|
+
const metadataChanged = !existing
|
|
1499
|
+
|| existing.title !== title
|
|
1500
|
+
|| existing.category !== categoryValue
|
|
1501
|
+
|| existing.topicPath !== topicPathValue
|
|
1502
|
+
|| existing.summary !== summaryValue
|
|
1503
|
+
|| JSON.stringify(existing.sourceSavedItemIds) !== JSON.stringify(sourceSavedItemIds)
|
|
1504
|
+
|| existing.notionPageId !== notionPageId
|
|
1505
|
+
|| existing.notionPageUrl !== notionPageUrl;
|
|
1506
|
+
upsert.run(existing?.id ?? randomUUID(), ownerValue, title, categoryValue, topicPathValue, relativePath, summaryValue, JSON.stringify(sourceSavedItemIds), existing?.createdAt ?? now, metadataChanged ? now : existing.updatedAt, typeof lastRunId === 'string' ? lastRunId : (existing?.lastRunId ?? null), notionPageId, notionPageUrl);
|
|
1507
|
+
}
|
|
1508
|
+
if (markdownPaths.length > 0) {
|
|
1509
|
+
deleteStale.run(ownerValue, ...markdownPaths);
|
|
1510
|
+
}
|
|
1511
|
+
else {
|
|
1512
|
+
deleteStale.run(ownerValue);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
getCuratorAgent(userId) {
|
|
1516
|
+
const config = this.getConfig(userId);
|
|
1517
|
+
if (!config || typeof this.conversationManager.getAgent !== 'function')
|
|
1518
|
+
return null;
|
|
1519
|
+
return this.conversationManager.getAgent(config.curatorAgentId);
|
|
1520
|
+
}
|
|
1521
|
+
updateCuratorAgentRuntime(userId, patch) {
|
|
1522
|
+
const config = this.getConfig(userId);
|
|
1523
|
+
if (!config)
|
|
1524
|
+
return null;
|
|
1525
|
+
const existing = this.conversationManager.getAgent(config.curatorAgentId);
|
|
1526
|
+
if (!existing)
|
|
1527
|
+
return null;
|
|
1528
|
+
const nextModel = Object.prototype.hasOwnProperty.call(patch, 'model')
|
|
1529
|
+
? patch.model?.trim() || null
|
|
1530
|
+
: existing.model ?? null;
|
|
1531
|
+
const nextReasoning = Object.prototype.hasOwnProperty.call(patch, 'reasoningEffort')
|
|
1532
|
+
? patch.reasoningEffort?.trim() || null
|
|
1533
|
+
: existing.reasoningEffort ?? null;
|
|
1534
|
+
const nextEnvVars = Object.prototype.hasOwnProperty.call(patch, 'envVars')
|
|
1535
|
+
? patch.envVars ?? {}
|
|
1536
|
+
: existing.envVars ?? {};
|
|
1537
|
+
const envVarsJson = Object.keys(nextEnvVars).length > 0 ? JSON.stringify(nextEnvVars) : null;
|
|
1538
|
+
const now = Date.now();
|
|
1539
|
+
this.db.prepare(`UPDATE agents
|
|
1540
|
+
SET model = ?,
|
|
1541
|
+
reasoning_effort = ?,
|
|
1542
|
+
env_vars = ?,
|
|
1543
|
+
disabled_tool_kinds = ?,
|
|
1544
|
+
updated_at = ?
|
|
1545
|
+
WHERE agent_id = ?`).run(nextModel, nextReasoning, envVarsJson, CURATOR_DISABLED_TOOL_KINDS_JSON, now, config.curatorAgentId);
|
|
1546
|
+
this.db.prepare(`UPDATE conversations
|
|
1547
|
+
SET env_vars = ?,
|
|
1548
|
+
updated_at = ?
|
|
1549
|
+
WHERE agent_id = ?`).run(envVarsJson, now, config.curatorAgentId);
|
|
1550
|
+
const updatedAgent = this.conversationManager.getAgent(config.curatorAgentId);
|
|
1551
|
+
this.syncPlatformAgentInstance(this.getConfig(userId) ?? config);
|
|
1552
|
+
return updatedAgent;
|
|
1553
|
+
}
|
|
1554
|
+
openCuratorConversation(userId, mode = 'debug') {
|
|
1555
|
+
const config = this.getConfig(userId);
|
|
1556
|
+
if (!config)
|
|
1557
|
+
return null;
|
|
1558
|
+
const title = getPlatformAgentConversationTitle({
|
|
1559
|
+
definitionId: 'library_curator',
|
|
1560
|
+
mode,
|
|
1561
|
+
});
|
|
1562
|
+
return this.conversationManager.openNamedAgentPrimaryThread?.({
|
|
1563
|
+
agentId: config.curatorAgentId,
|
|
1564
|
+
title,
|
|
1565
|
+
userId,
|
|
1566
|
+
legacyTitles: mode === 'debug'
|
|
1567
|
+
? getPlatformAgentLegacyConversationTitles('library_curator')
|
|
1568
|
+
: undefined,
|
|
1569
|
+
}) ?? this.conversationManager.openAgentThread(config.curatorAgentId, userId);
|
|
1570
|
+
}
|
|
1571
|
+
buildOrganizePrompt(params) {
|
|
1572
|
+
const categoriesLine = params.config.categories.length > 0
|
|
1573
|
+
? params.config.categories.join(', ')
|
|
1574
|
+
: '(none — infer sensible categories and record them in the manifest)';
|
|
1575
|
+
const itemsBlock = params.savedItems.map((item, index) => [
|
|
1576
|
+
`### Saved Item ${index + 1}`,
|
|
1577
|
+
`- id: ${item.id}`,
|
|
1578
|
+
`- sender: ${item.senderName || 'Unknown'}`,
|
|
1579
|
+
`- source: ${item.sourceKind ?? inferSavedItemSourceKind(item.contextUrl)}`,
|
|
1580
|
+
`- savedAt: ${new Date(item.createdAt).toISOString()}`,
|
|
1581
|
+
`- contextUrl: ${item.contextUrl || '(none)'}`,
|
|
1582
|
+
'',
|
|
1583
|
+
item.content.trim(),
|
|
1584
|
+
].join('\n')).join('\n\n');
|
|
1585
|
+
return [
|
|
1586
|
+
'# Library organize run',
|
|
1587
|
+
'',
|
|
1588
|
+
'Organize the saved inbox items below into structured markdown documents under this workspace.',
|
|
1589
|
+
'',
|
|
1590
|
+
'## User categories',
|
|
1591
|
+
categoriesLine,
|
|
1592
|
+
'',
|
|
1593
|
+
'## Rules',
|
|
1594
|
+
'- Write or update markdown files under `{category}/{topic}/...` paths.',
|
|
1595
|
+
'- Prefer updating existing markdown files when they already fit the saved items.',
|
|
1596
|
+
'- Prefer following the user\'s existing folder and file structure before creating new documents.',
|
|
1597
|
+
'- Only create a new markdown document when no suitable existing document fits.',
|
|
1598
|
+
'- Do not rename or move existing user-created documents unless the user explicitly asks.',
|
|
1599
|
+
'- Merge related items into cohesive documents; update existing docs when appropriate.',
|
|
1600
|
+
'- Update `.meta/manifest.json` with all documents (title, category, topicPath, relativePath, summary, sourceSavedItemIds).',
|
|
1601
|
+
'- Include every processed saved item id in `processedSavedItemIds`.',
|
|
1602
|
+
'- Do not delete DB records; only write files and manifest.',
|
|
1603
|
+
'',
|
|
1604
|
+
'## Saved inbox items',
|
|
1605
|
+
itemsBlock,
|
|
1606
|
+
].join('\n');
|
|
1607
|
+
}
|
|
1608
|
+
async dispatchOrganizeRun(params) {
|
|
1609
|
+
const config = await this.ensureConfigForUser({ userId: params.userId, username: params.username });
|
|
1610
|
+
const runtimeBlockReason = this.getCuratorRuntimeBlockReason(config);
|
|
1611
|
+
if (runtimeBlockReason) {
|
|
1612
|
+
return { ok: true, skipped: true, reason: runtimeBlockReason };
|
|
1613
|
+
}
|
|
1614
|
+
if (!config.enabled) {
|
|
1615
|
+
return { ok: true, skipped: true, reason: 'Library curator is disabled' };
|
|
1616
|
+
}
|
|
1617
|
+
const runningRun = this.db.prepare(`SELECT run_id as runId FROM library_curator_runs
|
|
1618
|
+
WHERE user_id = ? AND status = 'running'
|
|
1619
|
+
ORDER BY created_at DESC LIMIT 1`).get(params.userId);
|
|
1620
|
+
if (runningRun) {
|
|
1621
|
+
return { ok: true, skipped: true, queued: true, runId: runningRun.runId, reason: 'Library curator run already in progress' };
|
|
1622
|
+
}
|
|
1623
|
+
const inboxItems = this.listInboxSavedItems(params.userId);
|
|
1624
|
+
if (inboxItems.length === 0) {
|
|
1625
|
+
const now = Date.now();
|
|
1626
|
+
this.db.prepare(`UPDATE library_curator_configs SET next_run_at = ?, updated_at = ? WHERE user_id = ?`).run(now + config.scheduleIntervalMs, now, params.userId);
|
|
1627
|
+
this.syncPlatformAgentInstance(this.getConfig(params.userId) ?? config, { overwriteCanonical: true });
|
|
1628
|
+
return { ok: true, skipped: true, reason: 'No inbox items to organize' };
|
|
1629
|
+
}
|
|
1630
|
+
const conversation = this.openCuratorConversation(params.userId, 'workflow');
|
|
1631
|
+
if (!conversation)
|
|
1632
|
+
return { ok: false, error: 'Failed to open curator conversation' };
|
|
1633
|
+
const savedItemIds = inboxItems.map((item) => item.id);
|
|
1634
|
+
const prompt = this.buildOrganizePrompt({ config, savedItems: inboxItems });
|
|
1635
|
+
const activationMetadata = {
|
|
1636
|
+
platformRun: {
|
|
1637
|
+
definitionId: 'library_curator',
|
|
1638
|
+
workflowType: 'library_curate',
|
|
1639
|
+
mode: 'workflow',
|
|
1640
|
+
},
|
|
1641
|
+
suppressReplyContract: true,
|
|
1642
|
+
};
|
|
1643
|
+
const result = await this.conversationManager.submitPrompt(conversation.id, prompt, {
|
|
1644
|
+
recordAsUserMessage: false,
|
|
1645
|
+
activationMetadata,
|
|
1646
|
+
suppressReplyContract: true,
|
|
1647
|
+
});
|
|
1648
|
+
if (!result.runId) {
|
|
1649
|
+
return { ok: false, error: 'Failed to start library organize run', queued: result.queued };
|
|
1650
|
+
}
|
|
1651
|
+
const now = Date.now();
|
|
1652
|
+
this.db.prepare(`INSERT INTO library_curator_runs(run_id, user_id, saved_item_ids_json, status, created_at)
|
|
1653
|
+
VALUES(?, ?, ?, 'running', ?)`).run(result.runId, params.userId, JSON.stringify(savedItemIds), now);
|
|
1654
|
+
this.db.prepare(`UPDATE library_curator_configs SET last_run_at = ?, updated_at = ? WHERE user_id = ?`).run(now, now, params.userId);
|
|
1655
|
+
this.syncPlatformAgentInstance(this.getConfig(params.userId) ?? config, { overwriteCanonical: true });
|
|
1656
|
+
await this.reconcileRunStateAfterInsert(result.runId, params.userId);
|
|
1657
|
+
return { ok: true, runId: result.runId, queued: result.queued };
|
|
1658
|
+
}
|
|
1659
|
+
async reconcileRunStateAfterInsert(runId, userId) {
|
|
1660
|
+
const runRow = this.db.prepare(`SELECT ended_at as endedAt, stop_reason as stopReason, error
|
|
1661
|
+
FROM runs
|
|
1662
|
+
WHERE run_id = ?`).get(runId);
|
|
1663
|
+
if (!runRow?.endedAt)
|
|
1664
|
+
return;
|
|
1665
|
+
await this.finalizeOrganizeRun({
|
|
1666
|
+
runId,
|
|
1667
|
+
userId,
|
|
1668
|
+
error: getLibraryCuratorTerminalError(runRow.stopReason, runRow.error),
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
async finalizeOrganizeRun(params) {
|
|
1672
|
+
const runRow = this.db.prepare(`SELECT run_id as runId, user_id as userId, saved_item_ids_json as savedItemIdsJson, status
|
|
1673
|
+
FROM library_curator_runs WHERE run_id = ?`).get(params.runId);
|
|
1674
|
+
if (!runRow || runRow.status !== 'running')
|
|
1675
|
+
return;
|
|
1676
|
+
const now = Date.now();
|
|
1677
|
+
if (params.error) {
|
|
1678
|
+
this.db.prepare(`UPDATE library_curator_runs SET status = 'failed', completed_at = ?, error = ? WHERE run_id = ?`).run(now, params.error, params.runId);
|
|
1679
|
+
this.scheduleNextRun(params.userId, now);
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
try {
|
|
1683
|
+
const manifestProcessedIds = await this.indexManifestForUser({ userId: params.userId, runId: params.runId });
|
|
1684
|
+
const runSavedItemIds = new Set(parseStringArrayJson(runRow.savedItemIdsJson));
|
|
1685
|
+
const processedSavedItemIds = manifestProcessedIds.filter((id) => runSavedItemIds.has(id));
|
|
1686
|
+
if (processedSavedItemIds.length > 0) {
|
|
1687
|
+
const markProcessed = this.db.prepare(`UPDATE saved_items
|
|
1688
|
+
SET process_status = 'processed', processed_at = ?, processed_run_id = ?
|
|
1689
|
+
WHERE user_id = ? AND id = ?`);
|
|
1690
|
+
for (const savedItemId of processedSavedItemIds) {
|
|
1691
|
+
markProcessed.run(now, params.runId, params.userId, savedItemId);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
this.db.prepare(`UPDATE library_curator_runs SET status = 'completed', completed_at = ? WHERE run_id = ?`).run(now, params.runId);
|
|
1695
|
+
}
|
|
1696
|
+
catch (error) {
|
|
1697
|
+
this.db.prepare(`UPDATE library_curator_runs SET status = 'failed', completed_at = ?, error = ? WHERE run_id = ?`).run(now, String(error?.message ?? error), params.runId);
|
|
1698
|
+
}
|
|
1699
|
+
this.scheduleNextRun(params.userId, now);
|
|
1700
|
+
}
|
|
1701
|
+
async indexManifestForUser(params) {
|
|
1702
|
+
const location = this.getRequiredLibraryLocation(params.userId, 'personal');
|
|
1703
|
+
const manifest = await this.readLibraryManifest(location);
|
|
1704
|
+
await this.reconcileIndexedDocumentsForLocation(location, params.runId);
|
|
1705
|
+
return manifest.processedSavedItemIds ?? [];
|
|
1706
|
+
}
|
|
1707
|
+
listDueConfigs(now) {
|
|
1708
|
+
const rows = this.db.prepare(`SELECT user_id as userId, curator_agent_id as curatorAgentId, node_id as nodeId,
|
|
1709
|
+
library_root_path as libraryRootPath, schedule_interval_ms as scheduleIntervalMs,
|
|
1710
|
+
categories_json as categoriesJson, last_run_at as lastRunAt, next_run_at as nextRunAt,
|
|
1711
|
+
enabled, created_at as createdAt, updated_at as updatedAt
|
|
1712
|
+
FROM library_curator_configs
|
|
1713
|
+
WHERE enabled = 1 AND next_run_at IS NOT NULL AND next_run_at <= ?`).all(now);
|
|
1714
|
+
return rows.map(mapConfigRow);
|
|
1715
|
+
}
|
|
1716
|
+
getUsernameForUserId(userId) {
|
|
1717
|
+
const row = this.db.prepare(`SELECT username FROM users WHERE id = ?`).get(userId);
|
|
1718
|
+
return row?.username?.trim() || null;
|
|
1719
|
+
}
|
|
1720
|
+
scheduleNextRun(userId, now) {
|
|
1721
|
+
const config = this.getConfig(userId);
|
|
1722
|
+
if (!config)
|
|
1723
|
+
return;
|
|
1724
|
+
this.db.prepare(`UPDATE library_curator_configs SET next_run_at = ?, updated_at = ? WHERE user_id = ?`).run(now + config.scheduleIntervalMs, now, userId);
|
|
1725
|
+
this.syncPlatformAgentInstance(this.getConfig(userId) ?? config, { overwriteCanonical: true });
|
|
1726
|
+
}
|
|
1727
|
+
getCuratorRuntimeBlockReason(config) {
|
|
1728
|
+
try {
|
|
1729
|
+
const row = this.db.prepare(`SELECT pai.enabled as instanceEnabled,
|
|
1730
|
+
pai.deleted_at as deletedAt,
|
|
1731
|
+
pad.enabled as definitionEnabled
|
|
1732
|
+
FROM platform_agent_instances pai
|
|
1733
|
+
JOIN platform_agent_definitions pad ON pad.definition_id = pai.definition_id
|
|
1734
|
+
WHERE pai.user_id = ?
|
|
1735
|
+
AND pai.definition_id = 'library_curator'
|
|
1736
|
+
AND pai.agent_id = ?
|
|
1737
|
+
LIMIT 1`).get(config.userId, config.curatorAgentId);
|
|
1738
|
+
if (!row)
|
|
1739
|
+
return null;
|
|
1740
|
+
if (row.deletedAt != null)
|
|
1741
|
+
return 'Library curator has been deleted';
|
|
1742
|
+
if (row.definitionEnabled !== 1)
|
|
1743
|
+
return 'Library curator definition is disabled';
|
|
1744
|
+
if (row.instanceEnabled !== 1)
|
|
1745
|
+
return 'Library curator is disabled';
|
|
1746
|
+
}
|
|
1747
|
+
catch {
|
|
1748
|
+
// Older schemas only have the legacy curator config.
|
|
1749
|
+
}
|
|
1750
|
+
return null;
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
function parseStringArrayJson(raw) {
|
|
1754
|
+
try {
|
|
1755
|
+
const parsed = JSON.parse(raw);
|
|
1756
|
+
if (!Array.isArray(parsed))
|
|
1757
|
+
return [];
|
|
1758
|
+
return parsed.filter((item) => typeof item === 'string' && item.trim().length > 0).map((item) => item.trim());
|
|
1759
|
+
}
|
|
1760
|
+
catch {
|
|
1761
|
+
return [];
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
function normalizeLibraryRelativePath(rawPath) {
|
|
1765
|
+
const trimmed = rawPath.trim().replace(/\\/g, '/');
|
|
1766
|
+
const normalized = path.posix.normalize(trimmed).replace(/^\/+/, '').replace(/\/+$/, '');
|
|
1767
|
+
if (!normalized
|
|
1768
|
+
|| normalized === '.'
|
|
1769
|
+
|| normalized === '..'
|
|
1770
|
+
|| normalized.startsWith('../')
|
|
1771
|
+
|| normalized.includes('/../')) {
|
|
1772
|
+
throw new Error('Invalid library path');
|
|
1773
|
+
}
|
|
1774
|
+
return normalized;
|
|
1775
|
+
}
|
|
1776
|
+
function resolveLibraryAbsolutePath(libraryRootPath, relativePath) {
|
|
1777
|
+
const normalizedPath = normalizeLibraryRelativePath(relativePath);
|
|
1778
|
+
const libraryRoot = path.resolve(libraryRootPath);
|
|
1779
|
+
const absolutePath = path.resolve(libraryRoot, normalizedPath);
|
|
1780
|
+
if (absolutePath !== libraryRoot && !absolutePath.startsWith(`${libraryRoot}${path.sep}`)) {
|
|
1781
|
+
throw new Error('Invalid library path');
|
|
1782
|
+
}
|
|
1783
|
+
return { normalizedPath, libraryRoot, absolutePath };
|
|
1784
|
+
}
|
|
1785
|
+
function assertLibraryWritePathAllowed(relativePath) {
|
|
1786
|
+
if (relativePath === '.meta' || relativePath.startsWith('.meta/')) {
|
|
1787
|
+
throw new Error('Cannot modify reserved library metadata paths');
|
|
1788
|
+
}
|
|
1789
|
+
if (relativePath === 'INDEX.md') {
|
|
1790
|
+
throw new Error('Cannot modify reserved library index file');
|
|
1791
|
+
}
|
|
1792
|
+
if (isReservedLibraryWorkspacePath(relativePath)) {
|
|
1793
|
+
throw new Error('Cannot modify reserved library workspace paths');
|
|
1794
|
+
}
|
|
1795
|
+
if (relativePath.split('/').some((segment) => segment.startsWith('.'))) {
|
|
1796
|
+
throw new Error('Hidden library paths are not allowed');
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
function isReservedLibraryWorkspaceTopLevelEntry(entryName) {
|
|
1800
|
+
return RESERVED_LIBRARY_WORKSPACE_TOP_LEVEL_ENTRIES.includes(entryName);
|
|
1801
|
+
}
|
|
1802
|
+
function isReservedLibraryWorkspacePath(relativePath) {
|
|
1803
|
+
const normalizedPath = normalizeLibraryRelativePath(relativePath);
|
|
1804
|
+
const [firstSegment] = normalizedPath.split('/');
|
|
1805
|
+
return Boolean(firstSegment && isReservedLibraryWorkspaceTopLevelEntry(firstSegment));
|
|
1806
|
+
}
|
|
1807
|
+
function shouldIgnoreLibraryEntry(parentPath, entryName, kind) {
|
|
1808
|
+
if (entryName === '.meta')
|
|
1809
|
+
return true;
|
|
1810
|
+
if (entryName === LIBRARY_DIRECTORY_PLACEHOLDER)
|
|
1811
|
+
return true;
|
|
1812
|
+
if (entryName.startsWith('.'))
|
|
1813
|
+
return true;
|
|
1814
|
+
if (!parentPath && entryName === 'INDEX.md')
|
|
1815
|
+
return true;
|
|
1816
|
+
if (!parentPath && isReservedLibraryWorkspaceTopLevelEntry(entryName))
|
|
1817
|
+
return true;
|
|
1818
|
+
if (kind === 'file' && !entryName.toLowerCase().endsWith('.md'))
|
|
1819
|
+
return true;
|
|
1820
|
+
return false;
|
|
1821
|
+
}
|
|
1822
|
+
function isMirrorReadableLibraryFilePath(relativePath) {
|
|
1823
|
+
const normalizedPath = normalizeLibraryRelativePath(relativePath);
|
|
1824
|
+
if (normalizedPath === 'INDEX.md' || normalizedPath === LIBRARY_MANIFEST_RELATIVE_PATH)
|
|
1825
|
+
return true;
|
|
1826
|
+
return isMirrorWritableLibraryDocumentPath(normalizedPath);
|
|
1827
|
+
}
|
|
1828
|
+
function shouldSkipLibraryMirrorEntry(relativePath, kind) {
|
|
1829
|
+
const normalizedPath = normalizeLibraryRelativePath(relativePath);
|
|
1830
|
+
const segments = normalizedPath.split('/').filter(Boolean);
|
|
1831
|
+
if (segments.length === 0)
|
|
1832
|
+
return false;
|
|
1833
|
+
const [firstSegment] = segments;
|
|
1834
|
+
if (!firstSegment)
|
|
1835
|
+
return false;
|
|
1836
|
+
if (segments.length === 1 && firstSegment === LIBRARY_DIRECTORY_PLACEHOLDER)
|
|
1837
|
+
return true;
|
|
1838
|
+
if (firstSegment === '.meta') {
|
|
1839
|
+
if (kind === 'directory')
|
|
1840
|
+
return false;
|
|
1841
|
+
return normalizedPath !== LIBRARY_MANIFEST_RELATIVE_PATH;
|
|
1842
|
+
}
|
|
1843
|
+
if (segments.some((segment) => segment.startsWith('.')))
|
|
1844
|
+
return true;
|
|
1845
|
+
if (isReservedLibraryWorkspaceTopLevelEntry(firstSegment))
|
|
1846
|
+
return true;
|
|
1847
|
+
if (kind === 'directory')
|
|
1848
|
+
return false;
|
|
1849
|
+
return !isMirrorReadableLibraryFilePath(normalizedPath);
|
|
1850
|
+
}
|
|
1851
|
+
function isLibraryPathMissingError(error) {
|
|
1852
|
+
const maybeErr = error;
|
|
1853
|
+
if (maybeErr?.code === 'ENOENT')
|
|
1854
|
+
return true;
|
|
1855
|
+
const message = String(maybeErr?.message ?? error).toLowerCase();
|
|
1856
|
+
return message.includes('not_found')
|
|
1857
|
+
|| message.includes('path not found')
|
|
1858
|
+
|| message.includes('enoent')
|
|
1859
|
+
|| message.includes('no such file');
|
|
1860
|
+
}
|
|
1861
|
+
function isLibraryPathNotDirectoryError(error) {
|
|
1862
|
+
const maybeErr = error;
|
|
1863
|
+
if (maybeErr?.code === 'ENOTDIR')
|
|
1864
|
+
return true;
|
|
1865
|
+
const message = String(maybeErr?.message ?? error).toLowerCase();
|
|
1866
|
+
return message.includes('enotdir') || message.includes('not a directory');
|
|
1867
|
+
}
|
|
1868
|
+
function isLibraryPathDirectoryReadError(error) {
|
|
1869
|
+
const maybeErr = error;
|
|
1870
|
+
if (maybeErr?.code === 'EISDIR')
|
|
1871
|
+
return true;
|
|
1872
|
+
const message = String(maybeErr?.message ?? error).toLowerCase();
|
|
1873
|
+
return message.includes('eisdir')
|
|
1874
|
+
|| message.includes('is a directory')
|
|
1875
|
+
|| message.includes('illegal operation on a directory');
|
|
1876
|
+
}
|
|
1877
|
+
function sortLibraryTreeNodes(nodes) {
|
|
1878
|
+
return nodes
|
|
1879
|
+
.map((node) => ({
|
|
1880
|
+
...node,
|
|
1881
|
+
...(node.children ? { children: sortLibraryTreeNodes(node.children) } : {}),
|
|
1882
|
+
}))
|
|
1883
|
+
.sort((left, right) => {
|
|
1884
|
+
if (left.kind !== right.kind)
|
|
1885
|
+
return left.kind === 'directory' ? -1 : 1;
|
|
1886
|
+
return left.name.localeCompare(right.name);
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
function collectLibraryFilePaths(nodes) {
|
|
1890
|
+
const paths = [];
|
|
1891
|
+
for (const node of nodes) {
|
|
1892
|
+
if (node.kind === 'file') {
|
|
1893
|
+
paths.push(node.path);
|
|
1894
|
+
continue;
|
|
1895
|
+
}
|
|
1896
|
+
if (node.children?.length) {
|
|
1897
|
+
paths.push(...collectLibraryFilePaths(node.children));
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
return paths;
|
|
1901
|
+
}
|
|
1902
|
+
function upsertLibraryManifestDocument(manifest, relativePath, content) {
|
|
1903
|
+
const existing = manifest.documents.find((doc) => doc.relativePath === relativePath);
|
|
1904
|
+
const nextDocument = {
|
|
1905
|
+
title: extractLibraryDocumentTitle(content) ?? existing?.title ?? path.posix.basename(relativePath, path.posix.extname(relativePath)),
|
|
1906
|
+
category: existing?.category ?? inferLibraryCategory(relativePath),
|
|
1907
|
+
topicPath: inferLibraryTopicPath(relativePath),
|
|
1908
|
+
relativePath,
|
|
1909
|
+
summary: existing?.summary ?? '',
|
|
1910
|
+
sourceSavedItemIds: existing?.sourceSavedItemIds ?? [],
|
|
1911
|
+
notionPageId: existing?.notionPageId ?? null,
|
|
1912
|
+
notionPageUrl: existing?.notionPageUrl ?? null,
|
|
1913
|
+
};
|
|
1914
|
+
return {
|
|
1915
|
+
version: manifest.version || 1,
|
|
1916
|
+
processedSavedItemIds: manifest.processedSavedItemIds ?? [],
|
|
1917
|
+
documents: [
|
|
1918
|
+
...manifest.documents.filter((doc) => doc.relativePath !== relativePath),
|
|
1919
|
+
nextDocument,
|
|
1920
|
+
].sort((left, right) => left.relativePath.localeCompare(right.relativePath)),
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
function extractLibraryDocumentTitle(content) {
|
|
1924
|
+
for (const line of content.split(/\r?\n/)) {
|
|
1925
|
+
const match = line.trim().match(/^#\s+(.+)$/);
|
|
1926
|
+
if (match?.[1]?.trim())
|
|
1927
|
+
return match[1].trim();
|
|
1928
|
+
}
|
|
1929
|
+
return null;
|
|
1930
|
+
}
|
|
1931
|
+
function inferLibraryCategory(relativePath) {
|
|
1932
|
+
const parts = relativePath.split('/').filter(Boolean);
|
|
1933
|
+
if (parts.length > 1)
|
|
1934
|
+
return parts[0] ?? '';
|
|
1935
|
+
return '';
|
|
1936
|
+
}
|
|
1937
|
+
function inferLibraryTopicPath(relativePath) {
|
|
1938
|
+
const normalized = relativePath.replace(/\.md$/i, '');
|
|
1939
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
1940
|
+
if (parts.length <= 1)
|
|
1941
|
+
return normalized;
|
|
1942
|
+
return parts.slice(1).join('/');
|
|
1943
|
+
}
|
|
1944
|
+
function buildLibraryMirrorBaseline(tree) {
|
|
1945
|
+
if (!tree)
|
|
1946
|
+
return null;
|
|
1947
|
+
return {
|
|
1948
|
+
files: tree.files
|
|
1949
|
+
.map((file) => ({
|
|
1950
|
+
path: normalizeLibraryRelativePath(file.path),
|
|
1951
|
+
sha256: file.sha256,
|
|
1952
|
+
}))
|
|
1953
|
+
.sort((left, right) => left.path.localeCompare(right.path)),
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
function serializeLibraryMirrorBaseline(baseline) {
|
|
1957
|
+
if (!baseline)
|
|
1958
|
+
return null;
|
|
1959
|
+
return JSON.stringify(baseline);
|
|
1960
|
+
}
|
|
1961
|
+
function parseLibraryMirrorBaseline(raw) {
|
|
1962
|
+
if (!raw?.trim())
|
|
1963
|
+
return null;
|
|
1964
|
+
try {
|
|
1965
|
+
const parsed = JSON.parse(raw);
|
|
1966
|
+
const files = Array.isArray(parsed.files)
|
|
1967
|
+
? parsed.files
|
|
1968
|
+
.filter((file) => (typeof file?.path === 'string'
|
|
1969
|
+
&& file.path.trim().length > 0
|
|
1970
|
+
&& typeof file?.sha256 === 'string'
|
|
1971
|
+
&& file.sha256.trim().length > 0))
|
|
1972
|
+
.map((file) => ({
|
|
1973
|
+
path: normalizeLibraryRelativePath(file.path),
|
|
1974
|
+
sha256: file.sha256.trim(),
|
|
1975
|
+
}))
|
|
1976
|
+
: [];
|
|
1977
|
+
return { files };
|
|
1978
|
+
}
|
|
1979
|
+
catch {
|
|
1980
|
+
return null;
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
function filterMirrorBaselineFiles(baseline) {
|
|
1984
|
+
return (baseline?.files ?? []).filter((file) => isMirrorWritableLibraryDocumentPath(file.path));
|
|
1985
|
+
}
|
|
1986
|
+
function filterMirrorCurrentFiles(tree) {
|
|
1987
|
+
return (tree?.files ?? []).filter((file) => isMirrorWritableLibraryDocumentPath(file.path));
|
|
1988
|
+
}
|
|
1989
|
+
function isMirrorWritableLibraryDocumentPath(relativePath) {
|
|
1990
|
+
try {
|
|
1991
|
+
const normalizedPath = normalizeLibraryRelativePath(relativePath);
|
|
1992
|
+
if (!normalizedPath.toLowerCase().endsWith('.md'))
|
|
1993
|
+
return false;
|
|
1994
|
+
if (normalizedPath === 'INDEX.md' || normalizedPath === '.meta' || normalizedPath.startsWith('.meta/')) {
|
|
1995
|
+
return false;
|
|
1996
|
+
}
|
|
1997
|
+
assertLibraryWritePathAllowed(normalizedPath);
|
|
1998
|
+
return true;
|
|
1999
|
+
}
|
|
2000
|
+
catch {
|
|
2001
|
+
return false;
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
async function collectLibraryMirrorFiles(libraryRootPath) {
|
|
2005
|
+
const fsPromises = await import('node:fs/promises');
|
|
2006
|
+
const files = [];
|
|
2007
|
+
const walk = async (relativePath) => {
|
|
2008
|
+
const absolutePath = relativePath
|
|
2009
|
+
? path.join(libraryRootPath, relativePath)
|
|
2010
|
+
: libraryRootPath;
|
|
2011
|
+
const entries = await fsPromises.readdir(absolutePath, { withFileTypes: true });
|
|
2012
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
2013
|
+
for (const entry of entries) {
|
|
2014
|
+
const childRelativePath = relativePath ? path.posix.join(relativePath, entry.name) : entry.name;
|
|
2015
|
+
const childAbsolutePath = path.join(libraryRootPath, childRelativePath);
|
|
2016
|
+
if (entry.isSymbolicLink())
|
|
2017
|
+
continue;
|
|
2018
|
+
if (entry.isDirectory()) {
|
|
2019
|
+
if (shouldSkipLibraryMirrorEntry(childRelativePath, 'directory'))
|
|
2020
|
+
continue;
|
|
2021
|
+
await walk(childRelativePath);
|
|
2022
|
+
continue;
|
|
2023
|
+
}
|
|
2024
|
+
if (!entry.isFile())
|
|
2025
|
+
continue;
|
|
2026
|
+
if (shouldSkipLibraryMirrorEntry(childRelativePath, 'file'))
|
|
2027
|
+
continue;
|
|
2028
|
+
const content = (await fsPromises.readFile(childAbsolutePath, 'utf8')).replace(/\r\n/g, '\n');
|
|
2029
|
+
files.push({
|
|
2030
|
+
path: childRelativePath.replace(/\\/g, '/'),
|
|
2031
|
+
content,
|
|
2032
|
+
sha256: sha256Text(content),
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
};
|
|
2036
|
+
await walk('');
|
|
2037
|
+
files.sort((left, right) => left.path.localeCompare(right.path));
|
|
2038
|
+
return files;
|
|
2039
|
+
}
|
|
2040
|
+
async function collectAuthorityMirrorDocumentPaths(libraryRootPath) {
|
|
2041
|
+
const fsPromises = await import('node:fs/promises');
|
|
2042
|
+
const paths = [];
|
|
2043
|
+
const walk = async (relativePath) => {
|
|
2044
|
+
const absolutePath = relativePath
|
|
2045
|
+
? path.join(libraryRootPath, relativePath)
|
|
2046
|
+
: libraryRootPath;
|
|
2047
|
+
const entries = await fsPromises.readdir(absolutePath, { withFileTypes: true });
|
|
2048
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
2049
|
+
for (const entry of entries) {
|
|
2050
|
+
const entryRelativePath = relativePath ? path.posix.join(relativePath, entry.name) : entry.name;
|
|
2051
|
+
const kind = entry.isDirectory() ? 'directory' : 'file';
|
|
2052
|
+
if (shouldIgnoreLibraryEntry(relativePath, entry.name, kind))
|
|
2053
|
+
continue;
|
|
2054
|
+
if (entry.isDirectory()) {
|
|
2055
|
+
await walk(entryRelativePath);
|
|
2056
|
+
continue;
|
|
2057
|
+
}
|
|
2058
|
+
if (!entry.isFile())
|
|
2059
|
+
continue;
|
|
2060
|
+
if (!isMirrorWritableLibraryDocumentPath(entryRelativePath))
|
|
2061
|
+
continue;
|
|
2062
|
+
paths.push(entryRelativePath);
|
|
2063
|
+
}
|
|
2064
|
+
};
|
|
2065
|
+
await walk('');
|
|
2066
|
+
return paths.sort((left, right) => left.localeCompare(right));
|
|
2067
|
+
}
|
|
2068
|
+
async function collectAuthoritySubtreeEntries(libraryRootPath, relativePath) {
|
|
2069
|
+
const normalizedPath = normalizeLibraryRelativePath(relativePath);
|
|
2070
|
+
const entryKind = await getAuthorityEntryKind(libraryRootPath, normalizedPath);
|
|
2071
|
+
if (!entryKind) {
|
|
2072
|
+
return { directories: [], files: [] };
|
|
2073
|
+
}
|
|
2074
|
+
if (entryKind === 'file') {
|
|
2075
|
+
return { directories: [], files: [normalizedPath] };
|
|
2076
|
+
}
|
|
2077
|
+
const fsPromises = await import('node:fs/promises');
|
|
2078
|
+
const directories = [normalizedPath];
|
|
2079
|
+
const files = [];
|
|
2080
|
+
const walk = async (currentPath) => {
|
|
2081
|
+
const { absolutePath } = resolveLibraryAbsolutePath(libraryRootPath, currentPath);
|
|
2082
|
+
const entries = await fsPromises.readdir(absolutePath, { withFileTypes: true });
|
|
2083
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
2084
|
+
for (const entry of entries) {
|
|
2085
|
+
const entryPath = path.posix.join(currentPath, entry.name);
|
|
2086
|
+
if (entry.isDirectory()) {
|
|
2087
|
+
directories.push(entryPath);
|
|
2088
|
+
await walk(entryPath);
|
|
2089
|
+
continue;
|
|
2090
|
+
}
|
|
2091
|
+
files.push(entryPath);
|
|
2092
|
+
}
|
|
2093
|
+
};
|
|
2094
|
+
await walk(normalizedPath);
|
|
2095
|
+
return {
|
|
2096
|
+
directories: directories.sort((left, right) => left.localeCompare(right)),
|
|
2097
|
+
files: files.sort((left, right) => left.localeCompare(right)),
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
async function readAuthorityLibraryFile(libraryRootPath, relativePath) {
|
|
2101
|
+
const { absolutePath } = resolveLibraryAbsolutePath(libraryRootPath, relativePath);
|
|
2102
|
+
const fsPromises = await import('node:fs/promises');
|
|
2103
|
+
try {
|
|
2104
|
+
const content = await fsPromises.readFile(absolutePath, 'utf8');
|
|
2105
|
+
return { content: content.replace(/\r\n/g, '\n') };
|
|
2106
|
+
}
|
|
2107
|
+
catch (error) {
|
|
2108
|
+
if (isLibraryPathMissingError(error))
|
|
2109
|
+
return null;
|
|
2110
|
+
throw error;
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
async function getAuthorityEntryKind(libraryRootPath, relativePath) {
|
|
2114
|
+
const { absolutePath } = resolveLibraryAbsolutePath(libraryRootPath, relativePath);
|
|
2115
|
+
const fsPromises = await import('node:fs/promises');
|
|
2116
|
+
try {
|
|
2117
|
+
const stat = await fsPromises.lstat(absolutePath);
|
|
2118
|
+
return stat.isDirectory() ? 'directory' : 'file';
|
|
2119
|
+
}
|
|
2120
|
+
catch (error) {
|
|
2121
|
+
if (isLibraryPathMissingError(error))
|
|
2122
|
+
return null;
|
|
2123
|
+
throw error;
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
async function deleteAuthorityLibraryPath(libraryRootPath, relativePath) {
|
|
2127
|
+
const { absolutePath, libraryRoot } = resolveLibraryAbsolutePath(libraryRootPath, relativePath);
|
|
2128
|
+
const fsPromises = await import('node:fs/promises');
|
|
2129
|
+
const sourceStat = await fsPromises.lstat(absolutePath).catch((error) => {
|
|
2130
|
+
if (isLibraryPathMissingError(error))
|
|
2131
|
+
return null;
|
|
2132
|
+
throw error;
|
|
2133
|
+
});
|
|
2134
|
+
if (!sourceStat)
|
|
2135
|
+
return;
|
|
2136
|
+
await fsPromises.rm(absolutePath, { recursive: sourceStat.isDirectory(), force: true });
|
|
2137
|
+
await pruneEmptyLibraryParents(libraryRoot, path.dirname(absolutePath));
|
|
2138
|
+
}
|
|
2139
|
+
async function pruneEmptyLibraryParents(libraryRoot, startDirectory) {
|
|
2140
|
+
const fsPromises = await import('node:fs/promises');
|
|
2141
|
+
let cursor = startDirectory;
|
|
2142
|
+
while (cursor.startsWith(libraryRoot) && cursor !== libraryRoot) {
|
|
2143
|
+
const remaining = await fsPromises.readdir(cursor).catch((error) => {
|
|
2144
|
+
if (isLibraryPathMissingError(error))
|
|
2145
|
+
return null;
|
|
2146
|
+
throw error;
|
|
2147
|
+
});
|
|
2148
|
+
if (!remaining || remaining.length > 0)
|
|
2149
|
+
break;
|
|
2150
|
+
await fsPromises.rmdir(cursor).catch((error) => {
|
|
2151
|
+
if (isLibraryPathMissingError(error))
|
|
2152
|
+
return;
|
|
2153
|
+
throw error;
|
|
2154
|
+
});
|
|
2155
|
+
cursor = path.dirname(cursor);
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
async function renameAuthorityLibraryPath(libraryRootPath, relativePath, nextRelativePath, options) {
|
|
2159
|
+
const { absolutePath, libraryRoot } = resolveLibraryAbsolutePath(libraryRootPath, relativePath);
|
|
2160
|
+
const { absolutePath: nextAbsolutePath } = resolveLibraryAbsolutePath(libraryRootPath, nextRelativePath);
|
|
2161
|
+
const fsPromises = await import('node:fs/promises');
|
|
2162
|
+
const sourceStat = await fsPromises.lstat(absolutePath).catch((error) => {
|
|
2163
|
+
if (isLibraryPathMissingError(error))
|
|
2164
|
+
return null;
|
|
2165
|
+
throw error;
|
|
2166
|
+
});
|
|
2167
|
+
if (!sourceStat) {
|
|
2168
|
+
if (options?.allowMissingSource)
|
|
2169
|
+
return 'missing_source';
|
|
2170
|
+
throw new Error('Path not found');
|
|
2171
|
+
}
|
|
2172
|
+
const targetStat = await fsPromises.lstat(nextAbsolutePath).catch((error) => {
|
|
2173
|
+
if (isLibraryPathMissingError(error))
|
|
2174
|
+
return null;
|
|
2175
|
+
throw error;
|
|
2176
|
+
});
|
|
2177
|
+
if (targetStat) {
|
|
2178
|
+
if (options?.allowExistingTarget)
|
|
2179
|
+
return 'existing_target';
|
|
2180
|
+
throw new Error('Destination already exists');
|
|
2181
|
+
}
|
|
2182
|
+
await fsPromises.mkdir(path.dirname(nextAbsolutePath), { recursive: true });
|
|
2183
|
+
await fsPromises.rename(absolutePath, nextAbsolutePath);
|
|
2184
|
+
await pruneEmptyLibraryParents(libraryRoot, path.dirname(absolutePath));
|
|
2185
|
+
return 'renamed';
|
|
2186
|
+
}
|
|
2187
|
+
function buildRenamedLibraryPath(relativePath, nextName, kind) {
|
|
2188
|
+
const parentPath = path.posix.dirname(relativePath);
|
|
2189
|
+
const siblingName = normalizeLibraryRenameName(nextName, kind);
|
|
2190
|
+
const nextRelativePath = parentPath === '.'
|
|
2191
|
+
? siblingName
|
|
2192
|
+
: path.posix.join(parentPath, siblingName);
|
|
2193
|
+
return normalizeLibraryRelativePath(nextRelativePath);
|
|
2194
|
+
}
|
|
2195
|
+
function normalizeLibraryRenameName(rawName, kind) {
|
|
2196
|
+
const trimmed = rawName.trim().replace(/\\/g, '/');
|
|
2197
|
+
if (!trimmed) {
|
|
2198
|
+
throw new Error(kind === 'directory' ? 'Folder name required' : 'Document name required');
|
|
2199
|
+
}
|
|
2200
|
+
if (trimmed.includes('/')) {
|
|
2201
|
+
throw new Error('Name cannot include path separators');
|
|
2202
|
+
}
|
|
2203
|
+
if (trimmed === '.' || trimmed === '..') {
|
|
2204
|
+
throw new Error('Invalid library path');
|
|
2205
|
+
}
|
|
2206
|
+
if (kind === 'directory') {
|
|
2207
|
+
if (trimmed.toLowerCase().endsWith('.md')) {
|
|
2208
|
+
throw new Error('Folder name cannot end with .md');
|
|
2209
|
+
}
|
|
2210
|
+
return trimmed;
|
|
2211
|
+
}
|
|
2212
|
+
const stem = trimmed.replace(/\.md$/i, '').trim();
|
|
2213
|
+
if (!stem) {
|
|
2214
|
+
throw new Error('Document name required');
|
|
2215
|
+
}
|
|
2216
|
+
return `${stem}.md`;
|
|
2217
|
+
}
|
|
2218
|
+
function renameLibraryManifestPaths(manifest, relativePath, nextRelativePath, kind) {
|
|
2219
|
+
const remapDocument = (doc, updatedRelativePath) => ({
|
|
2220
|
+
...doc,
|
|
2221
|
+
relativePath: updatedRelativePath,
|
|
2222
|
+
category: inferLibraryCategory(updatedRelativePath),
|
|
2223
|
+
topicPath: inferLibraryTopicPath(updatedRelativePath),
|
|
2224
|
+
});
|
|
2225
|
+
const documents = manifest.documents.map((doc) => {
|
|
2226
|
+
if (kind === 'file') {
|
|
2227
|
+
return doc.relativePath === relativePath
|
|
2228
|
+
? remapDocument(doc, nextRelativePath)
|
|
2229
|
+
: doc;
|
|
2230
|
+
}
|
|
2231
|
+
if (doc.relativePath === relativePath) {
|
|
2232
|
+
return remapDocument(doc, nextRelativePath);
|
|
2233
|
+
}
|
|
2234
|
+
if (doc.relativePath.startsWith(`${relativePath}/`)) {
|
|
2235
|
+
return remapDocument(doc, `${nextRelativePath}${doc.relativePath.slice(relativePath.length)}`);
|
|
2236
|
+
}
|
|
2237
|
+
return doc;
|
|
2238
|
+
});
|
|
2239
|
+
const deduped = new Map();
|
|
2240
|
+
for (const doc of documents) {
|
|
2241
|
+
deduped.set(doc.relativePath, doc);
|
|
2242
|
+
}
|
|
2243
|
+
return {
|
|
2244
|
+
version: manifest.version || 1,
|
|
2245
|
+
processedSavedItemIds: manifest.processedSavedItemIds ?? [],
|
|
2246
|
+
documents: Array.from(deduped.values()).sort((left, right) => left.relativePath.localeCompare(right.relativePath)),
|
|
2247
|
+
};
|
|
2248
|
+
}
|
|
2249
|
+
function buildWritebackKey(location, relativePath) {
|
|
2250
|
+
const owner = location.scope === 'shared'
|
|
2251
|
+
? (location.sharedKey ?? SHARED_LIBRARY_KEY)
|
|
2252
|
+
: (location.userId ?? 'user');
|
|
2253
|
+
return `${location.scope}:${owner}:${relativePath}`;
|
|
2254
|
+
}
|
|
2255
|
+
function sanitizeConflictPathSegment(value) {
|
|
2256
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, '_').trim() || 'scope';
|
|
2257
|
+
}
|
|
2258
|
+
function sha256Text(content) {
|
|
2259
|
+
return createHash('sha256').update(content, 'utf8').digest('hex');
|
|
2260
|
+
}
|
|
2261
|
+
export function isLibraryCuratorAgent(agent) {
|
|
2262
|
+
return agent?.agentRole === 'platform_agent' || agent?.agentRole === 'library_curator';
|
|
2263
|
+
}
|