@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.
Files changed (175) hide show
  1. package/dist/config.js +380 -0
  2. package/dist/execution/executionDispatcher.js +3810 -0
  3. package/dist/main.js +90 -0
  4. package/dist/nodeEventHistory.js +206 -0
  5. package/dist/scheduler/dreamLogic.js +50 -0
  6. package/dist/scheduler/dreamScheduler.js +65 -0
  7. package/dist/services/agentFileAccessService.js +1913 -0
  8. package/dist/services/agentRuntimeCleanupBroker.js +62 -0
  9. package/dist/services/agentSkillsBroker.js +118 -0
  10. package/dist/services/agentSkillsService.js +83 -0
  11. package/dist/services/agentWorkspaceBroker.js +937 -0
  12. package/dist/services/agentWorkspaceService.js +70 -0
  13. package/dist/services/appVersion.js +14 -0
  14. package/dist/services/auth.js +586 -0
  15. package/dist/services/claudeControlBroker.js +154 -0
  16. package/dist/services/claudeTranscriptBroker.js +100 -0
  17. package/dist/services/claudeTranscriptService.js +359 -0
  18. package/dist/services/codexAppServerBroker.js +155 -0
  19. package/dist/services/codexTranscriptBroker.js +98 -0
  20. package/dist/services/codexTranscriptService.js +961 -0
  21. package/dist/services/droidMissionBroker.js +124 -0
  22. package/dist/services/droidMissionImporter.js +630 -0
  23. package/dist/services/droidModelOptions.js +165 -0
  24. package/dist/services/hubServerRegistrationService.js +268 -0
  25. package/dist/services/libraryManifest.js +43 -0
  26. package/dist/services/libraryScaffold.js +26 -0
  27. package/dist/services/libraryService.js +2263 -0
  28. package/dist/services/memoryService.js +386 -0
  29. package/dist/services/missionEvidence.js +377 -0
  30. package/dist/services/missionService.js +2361 -0
  31. package/dist/services/missionTrace.js +158 -0
  32. package/dist/services/nativeMissionBriefParser.js +120 -0
  33. package/dist/services/nativeMissionOrchestrator.js +2045 -0
  34. package/dist/services/nativeMissionReportGenerator.js +227 -0
  35. package/dist/services/nativeMissionValidationRunner.js +452 -0
  36. package/dist/services/nativeMissionWorkerBroker.js +190 -0
  37. package/dist/services/nodeRegistry.js +34 -0
  38. package/dist/services/nodeStateReconciler.js +97 -0
  39. package/dist/services/panelMediaScanner.js +119 -0
  40. package/dist/services/persistentRuntimeJsonlClient.js +153 -0
  41. package/dist/services/platformAgentPolicy.js +180 -0
  42. package/dist/services/platformAgentService.js +2041 -0
  43. package/dist/services/projectAccessResolver.js +93 -0
  44. package/dist/services/projectService.js +392 -0
  45. package/dist/services/resourceSpaceService.js +140 -0
  46. package/dist/services/scenarioRuntimeService.js +1130 -0
  47. package/dist/services/suggestedPlannerService.js +868 -0
  48. package/dist/services/workbenchGitBroker.js +161 -0
  49. package/dist/services/workbenchGitService.js +69 -0
  50. package/dist/services/workbenchInspectBroker.js +65 -0
  51. package/dist/services/workbenchNodePathService.js +79 -0
  52. package/dist/services/workbenchRegistryService.js +240 -0
  53. package/dist/services/workbenchRootService.js +181 -0
  54. package/dist/services/workbenchTerminalBroker.js +378 -0
  55. package/dist/services/workspaceRunOwnership.js +60 -0
  56. package/dist/services/workspaceScaffold.js +105 -0
  57. package/dist/services/workspaceSessionRuntimeService.js +576 -0
  58. package/dist/services/workspaceSessionService.js +245 -0
  59. package/dist/services/workspaceToolActionRunner.js +1582 -0
  60. package/dist/services/workspaceToolErrors.js +10 -0
  61. package/dist/services/workspaceToolExecutionUtils.js +895 -0
  62. package/dist/services/workspaceToolLatestStateProjector.js +91 -0
  63. package/dist/services/workspaceToolManifest.js +572 -0
  64. package/dist/services/workspaceToolMutationQueue.js +43 -0
  65. package/dist/services/workspaceToolPanelProjection.js +460 -0
  66. package/dist/services/workspaceToolPromotion.js +255 -0
  67. package/dist/services/workspaceToolPromotionState.js +224 -0
  68. package/dist/services/workspaceToolPublishDiagnostics.js +189 -0
  69. package/dist/services/workspaceToolPublishIdentityResolver.js +146 -0
  70. package/dist/services/workspaceToolReadModel.js +378 -0
  71. package/dist/services/workspaceToolRunLedger.js +239 -0
  72. package/dist/services/workspaceToolService.js +3067 -0
  73. package/dist/services/workspaceToolSnapshotPanelSync.js +293 -0
  74. package/dist/services/workspaceToolTerminalLifecycle.js +283 -0
  75. package/dist/services/workspaceToolTypes.js +1 -0
  76. package/dist/services/workspaceToolUploadMaterializer.js +228 -0
  77. package/dist/web/actionCardRoutes.js +129 -0
  78. package/dist/web/actionCards.js +469 -0
  79. package/dist/web/activationContext.js +684 -0
  80. package/dist/web/agentChannelGuards.js +48 -0
  81. package/dist/web/agentMentionCooldowns.js +32 -0
  82. package/dist/web/agentReminders.js +1668 -0
  83. package/dist/web/agentRuntimePresence.js +197 -0
  84. package/dist/web/agentSelfState.js +494 -0
  85. package/dist/web/agentTaskLinks.js +26 -0
  86. package/dist/web/agentVisibility.js +79 -0
  87. package/dist/web/assets.js +95 -0
  88. package/dist/web/channelActivationPrompt.js +395 -0
  89. package/dist/web/channelMemoryNotes.js +127 -0
  90. package/dist/web/channelMentions.js +10 -0
  91. package/dist/web/channelMessageSequences.js +19 -0
  92. package/dist/web/channelSubscriptions.js +26 -0
  93. package/dist/web/clearedTaskRoots.js +10 -0
  94. package/dist/web/collaborationPromptGuidance.js +36 -0
  95. package/dist/web/collaborationSurfaceState.js +140 -0
  96. package/dist/web/contextBundleRanking.js +154 -0
  97. package/dist/web/contextBundleResolver.js +488 -0
  98. package/dist/web/conversationBuiltinSkillRoots.js +50 -0
  99. package/dist/web/conversationControls.js +232 -0
  100. package/dist/web/conversationHandoffs.js +612 -0
  101. package/dist/web/conversationManager.js +2511 -0
  102. package/dist/web/conversationSummaries.js +876 -0
  103. package/dist/web/conversationSurfaceKinds.js +17 -0
  104. package/dist/web/conversationTargets.js +173 -0
  105. package/dist/web/directActivationPrompt.js +122 -0
  106. package/dist/web/directReplyTargets.js +69 -0
  107. package/dist/web/directThreadResolver.js +129 -0
  108. package/dist/web/dmTaskHandoffPrompt.js +120 -0
  109. package/dist/web/dmTaskThreadStatusProjection.js +229 -0
  110. package/dist/web/ftsQuery.js +33 -0
  111. package/dist/web/internalAgentRouter.js +11341 -0
  112. package/dist/web/libraryCuratorScheduler.js +58 -0
  113. package/dist/web/libraryDocumentPromptGuidance.js +8 -0
  114. package/dist/web/messageCheckpoints.js +19 -0
  115. package/dist/web/nodeWsHandler.js +2495 -0
  116. package/dist/web/notificationRounds.js +1061 -0
  117. package/dist/web/panelActionMessages.js +108 -0
  118. package/dist/web/panelActivationPrompt.js +18 -0
  119. package/dist/web/panelAudit.js +273 -0
  120. package/dist/web/panelLifecycle.js +222 -0
  121. package/dist/web/panelMediaPolicy.js +43 -0
  122. package/dist/web/panelPathPolicy.js +63 -0
  123. package/dist/web/panelPreviews.js +175 -0
  124. package/dist/web/panelQueryHandles.js +2749 -0
  125. package/dist/web/panelRoutes.js +2147 -0
  126. package/dist/web/panels.js +904 -0
  127. package/dist/web/peerInboxAggregates.js +1247 -0
  128. package/dist/web/planApprovalState.js +92 -0
  129. package/dist/web/platformAgentScheduler.js +66 -0
  130. package/dist/web/proactiveOpportunities.js +452 -0
  131. package/dist/web/promptContextSections.js +242 -0
  132. package/dist/web/promptHistorySanitizer.js +26 -0
  133. package/dist/web/promptSlashCommands.js +158 -0
  134. package/dist/web/rollingConversationSummary.js +453 -0
  135. package/dist/web/routeHelpers.js +11 -0
  136. package/dist/web/routes/handoff.js +288 -0
  137. package/dist/web/routes/history.js +345 -0
  138. package/dist/web/routes/memory.js +258 -0
  139. package/dist/web/routes/selfState.js +171 -0
  140. package/dist/web/routes/workspace.js +154 -0
  141. package/dist/web/runSurfaceWatermarks.js +431 -0
  142. package/dist/web/runtimeCapabilities.js +48 -0
  143. package/dist/web/sameAgentHandoffs.js +494 -0
  144. package/dist/web/server.js +15567 -0
  145. package/dist/web/sharedCollaborationCapsules.js +163 -0
  146. package/dist/web/soloSessionRelay.js +42 -0
  147. package/dist/web/soloWsHandler.js +138 -0
  148. package/dist/web/suggestedPlannerScheduler.js +56 -0
  149. package/dist/web/surfaceActivationPolicy.js +108 -0
  150. package/dist/web/surfaceCollaborators.js +61 -0
  151. package/dist/web/surfaceSystemStatus.js +263 -0
  152. package/dist/web/targetParticipants.js +77 -0
  153. package/dist/web/taskEvents.js +49 -0
  154. package/dist/web/taskLifecycleMessages.js +165 -0
  155. package/dist/web/taskLoops.js +732 -0
  156. package/dist/web/taskMemoryNotes.js +224 -0
  157. package/dist/web/taskNumbers.js +16 -0
  158. package/dist/web/taskOwnerGuards.js +49 -0
  159. package/dist/web/taskParticipantResolver.js +42 -0
  160. package/dist/web/taskParticipants.js +97 -0
  161. package/dist/web/taskSourceDetails.js +20 -0
  162. package/dist/web/taskStateViews.js +210 -0
  163. package/dist/web/taskStatusTransitions.js +9 -0
  164. package/dist/web/taskThreadFollowups.js +599 -0
  165. package/dist/web/taskThreadRuntimeClosure.js +685 -0
  166. package/dist/web/taskUpdateDelivery.js +104 -0
  167. package/dist/web/threadReplyContentHeuristics.js +30 -0
  168. package/dist/web/threadRoots.js +61 -0
  169. package/dist/web/threadTaskBindings.js +365 -0
  170. package/dist/web/uiPanelPromptGuidance.js +27 -0
  171. package/dist/web/workspaceMemoryHints.js +143 -0
  172. package/dist/web/workspaceToolPromptGuidance.js +30 -0
  173. package/dist/web/wsHandler.js +397 -0
  174. package/dist/web/wsSink.js +116 -0
  175. 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
+ }