studiograph 1.3.48-next.13 → 1.3.48-next.130
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/dist/agent/agent-pool.d.ts +14 -1
- package/dist/agent/agent-pool.js +37 -12
- package/dist/agent/agent-pool.js.map +1 -1
- package/dist/agent/followups.d.ts +34 -0
- package/dist/agent/followups.js +52 -0
- package/dist/agent/followups.js.map +1 -0
- package/dist/agent/orchestrator.d.ts +29 -2
- package/dist/agent/orchestrator.js +239 -38
- package/dist/agent/orchestrator.js.map +1 -1
- package/dist/agent/prompts/entity-types-section.d.ts +30 -0
- package/dist/agent/prompts/entity-types-section.js +83 -0
- package/dist/agent/prompts/entity-types-section.js.map +1 -0
- package/dist/agent/prompts/system.md +106 -47
- package/dist/agent/skill-loader.d.ts +30 -17
- package/dist/agent/skill-loader.js +63 -30
- package/dist/agent/skill-loader.js.map +1 -1
- package/dist/agent/skills/entity-schema.md +79 -343
- package/dist/agent/skills/interview/entity-templates.md +38 -37
- package/dist/agent/skills/interview/question-domains.md +54 -49
- package/dist/agent/skills/interview/skill.md +89 -14
- package/dist/agent/tools/capture-tools.d.ts +31 -0
- package/dist/agent/tools/capture-tools.js +40 -0
- package/dist/agent/tools/capture-tools.js.map +1 -0
- package/dist/agent/tools/folder-tools.d.ts +47 -0
- package/dist/agent/tools/folder-tools.js +249 -0
- package/dist/agent/tools/folder-tools.js.map +1 -0
- package/dist/agent/tools/fs-tools.d.ts +6 -5
- package/dist/agent/tools/fs-tools.js +5 -5
- package/dist/agent/tools/fs-tools.js.map +1 -1
- package/dist/agent/tools/graph-tools.d.ts +25 -72
- package/dist/agent/tools/graph-tools.js +362 -366
- package/dist/agent/tools/graph-tools.js.map +1 -1
- package/dist/agent/tools/history-tools.d.ts +95 -0
- package/dist/agent/tools/history-tools.js +461 -0
- package/dist/agent/tools/history-tools.js.map +1 -0
- package/dist/agent/tools/load-skill.d.ts +6 -5
- package/dist/agent/tools/load-skill.js +3 -3
- package/dist/agent/tools/load-skill.js.map +1 -1
- package/dist/agent/tools/message-tools.d.ts +13 -11
- package/dist/agent/tools/message-tools.js +31 -39
- package/dist/agent/tools/message-tools.js.map +1 -1
- package/dist/agent/tools/ops-tools.d.ts +5 -4
- package/dist/agent/tools/ops-tools.js +99 -212
- package/dist/agent/tools/ops-tools.js.map +1 -1
- package/dist/agent/tools/permission-tools.d.ts +39 -16
- package/dist/agent/tools/permission-tools.js +66 -41
- package/dist/agent/tools/permission-tools.js.map +1 -1
- package/dist/agent/tools/tool-loader.js +5 -4
- package/dist/agent/tools/tool-loader.js.map +1 -1
- package/dist/agent/tools/web-tools.js +58 -9
- package/dist/agent/tools/web-tools.js.map +1 -1
- package/dist/cli/commands/about.d.ts +1 -0
- package/dist/cli/commands/about.js +55 -3
- package/dist/cli/commands/about.js.map +1 -1
- package/dist/cli/commands/audit.d.ts +21 -0
- package/dist/cli/commands/audit.js +174 -0
- package/dist/cli/commands/audit.js.map +1 -0
- package/dist/cli/commands/clear.d.ts +5 -0
- package/dist/cli/commands/clear.js +144 -1
- package/dist/cli/commands/clear.js.map +1 -1
- package/dist/cli/commands/clone.d.ts +1 -1
- package/dist/cli/commands/clone.js +47 -12
- package/dist/cli/commands/clone.js.map +1 -1
- package/dist/cli/commands/deploy.js +79 -18
- package/dist/cli/commands/deploy.js.map +1 -1
- package/dist/cli/commands/folder.d.ts +11 -0
- package/dist/cli/commands/folder.js +186 -0
- package/dist/cli/commands/folder.js.map +1 -0
- package/dist/cli/commands/index.js +2 -2
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/init.js +16 -14
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/join.d.ts +1 -1
- package/dist/cli/commands/join.js +26 -14
- package/dist/cli/commands/join.js.map +1 -1
- package/dist/cli/commands/mcp.js +12 -4
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/r2.d.ts +3 -0
- package/dist/cli/commands/r2.js +196 -1
- package/dist/cli/commands/r2.js.map +1 -1
- package/dist/cli/commands/redeploy.js +41 -1
- package/dist/cli/commands/redeploy.js.map +1 -1
- package/dist/cli/commands/secrets.d.ts +23 -0
- package/dist/cli/commands/secrets.js +256 -0
- package/dist/cli/commands/secrets.js.map +1 -0
- package/dist/cli/commands/serve.js +143 -24
- package/dist/cli/commands/serve.js.map +1 -1
- package/dist/cli/commands/start.js +140 -71
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/commands/sync.d.ts +1 -1
- package/dist/cli/commands/sync.js +509 -163
- package/dist/cli/commands/sync.js.map +1 -1
- package/dist/cli/commands/user.js +8 -24
- package/dist/cli/commands/user.js.map +1 -1
- package/dist/cli/index.js +8 -7
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/railway-provisioning.d.ts +53 -0
- package/dist/cli/railway-provisioning.js +85 -0
- package/dist/cli/railway-provisioning.js.map +1 -0
- package/dist/cli/scaffolding.d.ts +4 -2
- package/dist/cli/scaffolding.js +42 -47
- package/dist/cli/scaffolding.js.map +1 -1
- package/dist/cli/setup-wizard.d.ts +30 -49
- package/dist/cli/setup-wizard.js +35 -38
- package/dist/cli/setup-wizard.js.map +1 -1
- package/dist/core/accent-palette.d.ts +22 -0
- package/dist/core/accent-palette.js +47 -0
- package/dist/core/accent-palette.js.map +1 -0
- package/dist/core/entity-body-templates.d.ts +21 -0
- package/dist/core/entity-body-templates.js +48 -0
- package/dist/core/entity-body-templates.js.map +1 -0
- package/dist/core/entity-types-catalog.d.ts +65 -0
- package/dist/core/entity-types-catalog.js +136 -0
- package/dist/core/entity-types-catalog.js.map +1 -0
- package/dist/core/feature-flags.d.ts +22 -0
- package/dist/core/feature-flags.js +15 -0
- package/dist/core/feature-flags.js.map +1 -0
- package/dist/core/features.d.ts +16 -0
- package/dist/core/features.js +48 -0
- package/dist/core/features.js.map +1 -0
- package/dist/core/folder-paths.d.ts +41 -0
- package/dist/core/folder-paths.js +95 -0
- package/dist/core/folder-paths.js.map +1 -0
- package/dist/core/folder-sidecar.d.ts +16 -0
- package/dist/core/folder-sidecar.js +45 -0
- package/dist/core/folder-sidecar.js.map +1 -0
- package/dist/core/graph.d.ts +471 -19
- package/dist/core/graph.js +1411 -284
- package/dist/core/graph.js.map +1 -1
- package/dist/core/schema-registry.js +1 -19
- package/dist/core/schema-registry.js.map +1 -1
- package/dist/core/schemas/connector.d.ts +67 -0
- package/dist/core/schemas/connector.js +100 -0
- package/dist/core/schemas/connector.js.map +1 -0
- package/dist/core/schemas/workspace.d.ts +122 -0
- package/dist/core/schemas/workspace.js +211 -0
- package/dist/core/schemas/workspace.js.map +1 -0
- package/dist/core/secrets/SecretStore.d.ts +77 -0
- package/dist/core/secrets/SecretStore.js +13 -0
- package/dist/core/secrets/SecretStore.js.map +1 -0
- package/dist/core/secrets/SettingsResolver.d.ts +54 -0
- package/dist/core/secrets/SettingsResolver.js +80 -0
- package/dist/core/secrets/SettingsResolver.js.map +1 -0
- package/dist/core/secrets/SettingsStore.d.ts +30 -0
- package/dist/core/secrets/SettingsStore.js +9 -0
- package/dist/core/secrets/SettingsStore.js.map +1 -0
- package/dist/core/secrets/SqliteEncryptedStore.d.ts +78 -0
- package/dist/core/secrets/SqliteEncryptedStore.js +581 -0
- package/dist/core/secrets/SqliteEncryptedStore.js.map +1 -0
- package/dist/core/secrets/audit.d.ts +36 -0
- package/dist/core/secrets/audit.js +60 -0
- package/dist/core/secrets/audit.js.map +1 -0
- package/dist/core/secrets/auth-db-migration.d.ts +129 -0
- package/dist/core/secrets/auth-db-migration.js +653 -0
- package/dist/core/secrets/auth-db-migration.js.map +1 -0
- package/dist/core/secrets/bundle.d.ts +141 -0
- package/dist/core/secrets/bundle.js +435 -0
- package/dist/core/secrets/bundle.js.map +1 -0
- package/dist/core/secrets/dual-write.d.ts +76 -0
- package/dist/core/secrets/dual-write.js +236 -0
- package/dist/core/secrets/dual-write.js.map +1 -0
- package/dist/core/secrets/encryption.d.ts +44 -0
- package/dist/core/secrets/encryption.js +97 -0
- package/dist/core/secrets/encryption.js.map +1 -0
- package/dist/core/secrets/importer.d.ts +94 -0
- package/dist/core/secrets/importer.js +319 -0
- package/dist/core/secrets/importer.js.map +1 -0
- package/dist/core/secrets/index.d.ts +35 -0
- package/dist/core/secrets/index.js +59 -0
- package/dist/core/secrets/index.js.map +1 -0
- package/dist/core/secrets/master-key.d.ts +38 -0
- package/dist/core/secrets/master-key.js +122 -0
- package/dist/core/secrets/master-key.js.map +1 -0
- package/dist/core/secrets/probes/anthropic.d.ts +98 -0
- package/dist/core/secrets/probes/anthropic.js +300 -0
- package/dist/core/secrets/probes/anthropic.js.map +1 -0
- package/dist/core/secrets/probes/brave.d.ts +9 -0
- package/dist/core/secrets/probes/brave.js +15 -0
- package/dist/core/secrets/probes/brave.js.map +1 -0
- package/dist/core/secrets/probes/r2.d.ts +15 -0
- package/dist/core/secrets/probes/r2.js +14 -0
- package/dist/core/secrets/probes/r2.js.map +1 -0
- package/dist/core/secrets/probes/voyage.d.ts +13 -0
- package/dist/core/secrets/probes/voyage.js +52 -0
- package/dist/core/secrets/probes/voyage.js.map +1 -0
- package/dist/core/secrets/read-flip.d.ts +59 -0
- package/dist/core/secrets/read-flip.js +87 -0
- package/dist/core/secrets/read-flip.js.map +1 -0
- package/dist/core/secrets/registry.d.ts +188 -0
- package/dist/core/secrets/registry.js +539 -0
- package/dist/core/secrets/registry.js.map +1 -0
- package/dist/core/types.d.ts +70 -483
- package/dist/core/types.js +6 -3
- package/dist/core/types.js.map +1 -1
- package/dist/core/user-config.d.ts +52 -12
- package/dist/core/user-config.js +119 -14
- package/dist/core/user-config.js.map +1 -1
- package/dist/core/validation.d.ts +603 -1409
- package/dist/core/validation.js +248 -146
- package/dist/core/validation.js.map +1 -1
- package/dist/core/workspace-manager.d.ts +35 -14
- package/dist/core/workspace-manager.js +105 -56
- package/dist/core/workspace-manager.js.map +1 -1
- package/dist/core/workspace.d.ts +19 -5
- package/dist/core/workspace.js +66 -39
- package/dist/core/workspace.js.map +1 -1
- package/dist/mcp/connector-manager.d.ts +16 -1
- package/dist/mcp/connector-manager.js +44 -5
- package/dist/mcp/connector-manager.js.map +1 -1
- package/dist/mcp/server.d.ts +11 -3
- package/dist/mcp/server.js +30 -5
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.d.ts +27 -2
- package/dist/mcp/tools.js +544 -113
- package/dist/mcp/tools.js.map +1 -1
- package/dist/server/__tests__/helpers/graph-api.d.ts +18 -0
- package/dist/server/__tests__/helpers/graph-api.js +16 -0
- package/dist/server/__tests__/helpers/graph-api.js.map +1 -0
- package/dist/server/commit-audit.d.ts +26 -0
- package/dist/server/commit-audit.js +30 -0
- package/dist/server/commit-audit.js.map +1 -0
- package/dist/server/index.d.ts +14 -3
- package/dist/server/index.js +388 -177
- package/dist/server/index.js.map +1 -1
- package/dist/server/middleware/sanitize.d.ts +46 -0
- package/dist/server/middleware/sanitize.js +171 -0
- package/dist/server/middleware/sanitize.js.map +1 -0
- package/dist/server/routes/asset-api.js +217 -65
- package/dist/server/routes/asset-api.js.map +1 -1
- package/dist/server/routes/audit-api.d.ts +24 -0
- package/dist/server/routes/audit-api.js +117 -0
- package/dist/server/routes/audit-api.js.map +1 -0
- package/dist/server/routes/auth-api.js +220 -44
- package/dist/server/routes/auth-api.js.map +1 -1
- package/dist/server/routes/capture-api.d.ts +10 -3
- package/dist/server/routes/capture-api.js +180 -24
- package/dist/server/routes/capture-api.js.map +1 -1
- package/dist/server/routes/chat.d.ts +4 -1
- package/dist/server/routes/chat.js +178 -46
- package/dist/server/routes/chat.js.map +1 -1
- package/dist/server/routes/config-api.d.ts +21 -0
- package/dist/server/routes/config-api.js +256 -0
- package/dist/server/routes/config-api.js.map +1 -0
- package/dist/server/routes/connectors-api.js +24 -18
- package/dist/server/routes/connectors-api.js.map +1 -1
- package/dist/server/routes/event-ingest-api.d.ts +15 -0
- package/dist/server/routes/event-ingest-api.js +125 -0
- package/dist/server/routes/event-ingest-api.js.map +1 -0
- package/dist/server/routes/features-api.d.ts +19 -0
- package/dist/server/routes/features-api.js +28 -0
- package/dist/server/routes/features-api.js.map +1 -0
- package/dist/server/routes/git-http.d.ts +2 -2
- package/dist/server/routes/git-http.js +69 -34
- package/dist/server/routes/git-http.js.map +1 -1
- package/dist/server/routes/graph-api-access.d.ts +35 -0
- package/dist/server/routes/graph-api-access.js +93 -0
- package/dist/server/routes/graph-api-access.js.map +1 -0
- package/dist/server/routes/graph-api.d.ts +3 -2
- package/dist/server/routes/graph-api.js +1014 -315
- package/dist/server/routes/graph-api.js.map +1 -1
- package/dist/server/routes/health.d.ts +18 -0
- package/dist/server/routes/health.js +74 -0
- package/dist/server/routes/health.js.map +1 -0
- package/dist/server/routes/insights-api.js +156 -33
- package/dist/server/routes/insights-api.js.map +1 -1
- package/dist/server/routes/mcp.d.ts +6 -3
- package/dist/server/routes/mcp.js +22 -6
- package/dist/server/routes/mcp.js.map +1 -1
- package/dist/server/routes/messages-api.d.ts +3 -3
- package/dist/server/routes/messages-api.js +308 -120
- package/dist/server/routes/messages-api.js.map +1 -1
- package/dist/server/routes/permissions-api.d.ts +9 -2
- package/dist/server/routes/permissions-api.js +87 -31
- package/dist/server/routes/permissions-api.js.map +1 -1
- package/dist/server/routes/secrets-api.d.ts +36 -0
- package/dist/server/routes/secrets-api.js +303 -0
- package/dist/server/routes/secrets-api.js.map +1 -0
- package/dist/server/routes/settings-api.d.ts +29 -0
- package/dist/server/routes/settings-api.js +177 -0
- package/dist/server/routes/settings-api.js.map +1 -0
- package/dist/server/routes/workspace-api.d.ts +2 -1
- package/dist/server/routes/workspace-api.js +131 -20
- package/dist/server/routes/workspace-api.js.map +1 -1
- package/dist/server/routes/ws.d.ts +3 -2
- package/dist/server/routes/ws.js +68 -18
- package/dist/server/routes/ws.js.map +1 -1
- package/dist/server/session-manager.d.ts +48 -5
- package/dist/server/session-manager.js +175 -11
- package/dist/server/session-manager.js.map +1 -1
- package/dist/server/ws-hub.d.ts +134 -22
- package/dist/server/ws-hub.js +0 -0
- package/dist/server/ws-hub.js.map +1 -1
- package/dist/server/yjs-manager.d.ts +107 -7
- package/dist/server/yjs-manager.js +444 -38
- package/dist/server/yjs-manager.js.map +1 -1
- package/dist/server/yjs-persistence.d.ts +143 -0
- package/dist/server/yjs-persistence.js +543 -0
- package/dist/server/yjs-persistence.js.map +1 -0
- package/dist/server/yjs-text-diff.d.ts +32 -0
- package/dist/server/yjs-text-diff.js +61 -0
- package/dist/server/yjs-text-diff.js.map +1 -0
- package/dist/services/access-control.d.ts +53 -0
- package/dist/services/access-control.js +132 -0
- package/dist/services/access-control.js.map +1 -0
- package/dist/services/asset-upload-service.d.ts +53 -0
- package/dist/services/asset-upload-service.js +200 -0
- package/dist/services/asset-upload-service.js.map +1 -0
- package/dist/services/assets/base.d.ts +38 -0
- package/dist/services/assets/base.js +55 -0
- package/dist/services/assets/base.js.map +1 -1
- package/dist/services/assets/index.d.ts +41 -1
- package/dist/services/assets/index.js +65 -0
- package/dist/services/assets/index.js.map +1 -1
- package/dist/services/assets/local.d.ts +7 -1
- package/dist/services/assets/local.js +83 -0
- package/dist/services/assets/local.js.map +1 -1
- package/dist/services/assets/r2.d.ts +28 -1
- package/dist/services/assets/r2.js +75 -1
- package/dist/services/assets/r2.js.map +1 -1
- package/dist/services/auth-service.d.ts +184 -36
- package/dist/services/auth-service.js +763 -138
- package/dist/services/auth-service.js.map +1 -1
- package/dist/services/briefing.d.ts +1 -1
- package/dist/services/briefing.js +12 -35
- package/dist/services/briefing.js.map +1 -1
- package/dist/services/event-processor.d.ts +104 -0
- package/dist/services/event-processor.js +443 -0
- package/dist/services/event-processor.js.map +1 -0
- package/dist/services/folder-creation.d.ts +33 -0
- package/dist/services/folder-creation.js +103 -0
- package/dist/services/folder-creation.js.map +1 -0
- package/dist/services/git.d.ts +44 -0
- package/dist/services/git.js +162 -55
- package/dist/services/git.js.map +1 -1
- package/dist/services/heartbeat.d.ts +19 -12
- package/dist/services/heartbeat.js +162 -107
- package/dist/services/heartbeat.js.map +1 -1
- package/dist/services/import-service.d.ts +37 -2
- package/dist/services/import-service.js +222 -123
- package/dist/services/import-service.js.map +1 -1
- package/dist/services/lint-service.js +7 -14
- package/dist/services/lint-service.js.map +1 -1
- package/dist/services/markdown.js +6 -2
- package/dist/services/markdown.js.map +1 -1
- package/dist/services/message-service.d.ts +62 -21
- package/dist/services/message-service.js +160 -43
- package/dist/services/message-service.js.map +1 -1
- package/dist/services/personal-folder.d.ts +40 -0
- package/dist/services/personal-folder.js +101 -0
- package/dist/services/personal-folder.js.map +1 -0
- package/dist/services/vector-service.d.ts +73 -3
- package/dist/services/vector-service.js +106 -7
- package/dist/services/vector-service.js.map +1 -1
- package/dist/services/workspace-access.d.ts +106 -0
- package/dist/services/workspace-access.js +149 -0
- package/dist/services/workspace-access.js.map +1 -0
- package/dist/utils/git.js +3 -3
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/log.d.ts +42 -0
- package/dist/utils/log.js +137 -0
- package/dist/utils/log.js.map +1 -0
- package/dist/utils/preflight.js +2 -2
- package/dist/utils/preflight.js.map +1 -1
- package/dist/utils/version-checker.d.ts +4 -1
- package/dist/utils/version-checker.js +8 -8
- package/dist/utils/version-checker.js.map +1 -1
- package/dist/web/_app/immutable/assets/0.Bp6xXZUi.css +2 -0
- package/dist/web/_app/immutable/assets/11.DlIf1mKh.css +1 -0
- package/dist/web/_app/immutable/assets/12.Dmlt28l2.css +1 -0
- package/dist/web/_app/immutable/assets/13.CmLrbzZp.css +1 -0
- package/dist/web/_app/immutable/assets/2.oo1ac0x_.css +1 -0
- package/dist/web/_app/immutable/assets/3.DJJsQzDb.css +1 -0
- package/dist/web/_app/immutable/assets/4.CZ-fNJ91.css +1 -0
- package/dist/web/_app/immutable/assets/AgentOverlay.BfWESlQU.css +1 -0
- package/dist/web/_app/immutable/assets/AssetManagerModal.ZfasE2L3.css +1 -0
- package/dist/web/_app/immutable/assets/CalendarView.CeNagvyK.css +1 -0
- package/dist/web/_app/immutable/assets/ChatPanel.po0gYtVH.css +1 -0
- package/dist/web/_app/immutable/assets/FolderMembersPanel.Bepw-r0F.css +1 -0
- package/dist/web/_app/immutable/assets/FolderMutationDialogs.BMRF6Z3z.css +1 -0
- package/dist/web/_app/immutable/assets/FolderPicker.B5KdvRYl.css +1 -0
- package/dist/web/_app/immutable/assets/GalleryView.Dy-qQjEc.css +1 -0
- package/dist/web/_app/immutable/assets/GraphView.BHnGIltS.css +1 -0
- package/dist/web/_app/immutable/assets/KanbanView.G3NZ0uHb.css +1 -0
- package/dist/web/_app/immutable/assets/SettingsDialog.tBn8y9DD.css +1 -0
- package/dist/web/_app/immutable/assets/Spinner.UBfl0pab.css +1 -0
- package/dist/web/_app/immutable/assets/TagFilter.DNipjifq.css +1 -0
- package/dist/web/_app/immutable/assets/TagPickerSheet.DJPuqnTg.css +1 -0
- package/dist/web/_app/immutable/assets/WorkspaceView.B9zA92UB.css +1 -0
- package/dist/web/_app/immutable/assets/button.BlLm4Dg1.css +1 -0
- package/dist/web/_app/immutable/assets/dialog.BxVal_19.css +1 -0
- package/dist/web/_app/immutable/assets/dist.B5hGjnfj.css +1 -0
- package/dist/web/_app/immutable/assets/dist.DDCWxn3B.css +1 -0
- package/dist/web/_app/immutable/assets/dropdown-menu.BSsUUQ3o.css +1 -0
- package/dist/web/_app/immutable/assets/entity-row.Bl5FyNCt.css +1 -0
- package/dist/web/_app/immutable/assets/greeting.Bm7lTneV.css +1 -0
- package/dist/web/_app/immutable/assets/ibm-plex-mono-latin-400-normal.CvHOgSBP.woff +0 -0
- package/dist/web/_app/immutable/assets/ibm-plex-mono-latin-400-normal.DMJ8VG8y.woff2 +0 -0
- package/dist/web/_app/immutable/assets/ibm-plex-mono-latin-500-normal.CB9ihrfo.woff +0 -0
- package/dist/web/_app/immutable/assets/ibm-plex-mono-latin-500-normal.DSY6xOcd.woff2 +0 -0
- package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-400-italic.CZTNEAuW.woff2 +0 -0
- package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-400-italic.CsGl1sm0.woff +0 -0
- package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-400-normal.CDDApCn2.woff2 +0 -0
- package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-400-normal.CYLoc0-x.woff +0 -0
- package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-500-italic.BNK2_mGO.woff2 +0 -0
- package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-500-italic.DpEwFAQM.woff +0 -0
- package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-500-normal.6ng42L7E.woff2 +0 -0
- package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-500-normal.BgVn5rGT.woff +0 -0
- package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-600-normal.Cu4Hd6ag.woff +0 -0
- package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-600-normal.CuJfVYMP.woff2 +0 -0
- package/dist/web/_app/immutable/assets/inline-list.DORLo4xF.css +1 -0
- package/dist/web/_app/immutable/assets/notifications.clodJdp5.css +1 -0
- package/dist/web/_app/immutable/assets/radio-group.GJJRVPUJ.css +1 -0
- package/dist/web/_app/immutable/assets/select.D_AV992j.css +1 -0
- package/dist/web/_app/immutable/chunks/-KAQXGuI2.js +1 -0
- package/dist/web/_app/immutable/chunks/-wzwcH00.js +1 -0
- package/dist/web/_app/immutable/chunks/07l_1Zpm2.js +1 -0
- package/dist/web/_app/immutable/chunks/1i1V0Z172.js +2 -0
- package/dist/web/_app/immutable/chunks/38oWKNzM2.js +6 -0
- package/dist/web/_app/immutable/chunks/54VmiJfH.js +1 -0
- package/dist/web/_app/immutable/chunks/6LRpFvjo.js +185 -0
- package/dist/web/_app/immutable/chunks/9bCTHkeS.js +1 -0
- package/dist/web/_app/immutable/chunks/B-822WnD2.js +22 -0
- package/dist/web/_app/immutable/chunks/B0HUvliX2.js +2 -0
- package/dist/web/_app/immutable/chunks/B12eXGpE.js +1 -0
- package/dist/web/_app/immutable/chunks/B2-Cx5XA.js +1 -0
- package/dist/web/_app/immutable/chunks/B5af0X0p2.js +1 -0
- package/dist/web/_app/immutable/chunks/B6iYNwo72.js +1 -0
- package/dist/web/_app/immutable/chunks/B7EMtoAB.js +1 -0
- package/dist/web/_app/immutable/chunks/B9qBGxh62.js +3 -0
- package/dist/web/_app/immutable/chunks/B9rVXblv.js +1 -0
- package/dist/web/_app/immutable/chunks/BEpMEjHO.js +1 -0
- package/dist/web/_app/immutable/chunks/BErNpj-S2.js +1 -0
- package/dist/web/_app/immutable/chunks/BHBk4zfV.js +1 -0
- package/dist/web/_app/immutable/chunks/BHH-JGNF.js +1 -0
- package/dist/web/_app/immutable/chunks/BNTlsKDJ.js +1 -0
- package/dist/web/_app/immutable/chunks/BOgcXgis2.js +1 -0
- package/dist/web/_app/immutable/chunks/BOyyDrjn2.js +1 -0
- package/dist/web/_app/immutable/chunks/BPR_ufQ0.js +41 -0
- package/dist/web/_app/immutable/chunks/BRcFsc6v.js +5 -0
- package/dist/web/_app/immutable/chunks/BT3KAag_.js +3 -0
- package/dist/web/_app/immutable/chunks/BX2T6xiZ.js +224 -0
- package/dist/web/_app/immutable/chunks/BZnJ8tbJ.js +1 -0
- package/dist/web/_app/immutable/chunks/Ba1Yzb7u.js +1 -0
- package/dist/web/_app/immutable/chunks/BcsvgA3B2.js +1 -0
- package/dist/web/_app/immutable/chunks/BfbS7rGT.js +1 -0
- package/dist/web/_app/immutable/chunks/BgagL41Z.js +1 -0
- package/dist/web/_app/immutable/chunks/Bk-AcswV.js +1 -0
- package/dist/web/_app/immutable/chunks/Bl9Pzopy.js +1 -0
- package/dist/web/_app/immutable/chunks/BmcU05mO.js +1 -0
- package/dist/web/_app/immutable/chunks/Bmgp0jlT.js +1 -0
- package/dist/web/_app/immutable/chunks/BpDONXUo.js +8 -0
- package/dist/web/_app/immutable/chunks/BpTXLx8I2.js +5 -0
- package/dist/web/_app/immutable/chunks/Bq0-jBGC2.js +1 -0
- package/dist/web/_app/immutable/chunks/BqGj0_GS.js +1 -0
- package/dist/web/_app/immutable/chunks/Bsc9K3Pz.js +1 -0
- package/dist/web/_app/immutable/chunks/Bssb691Q.js +1 -0
- package/dist/web/_app/immutable/chunks/BuzQceOn.js +1 -0
- package/dist/web/_app/immutable/chunks/BzeVNab0.js +1 -0
- package/dist/web/_app/immutable/chunks/C-OaypFN.js +1 -0
- package/dist/web/_app/immutable/chunks/C0fjDjvE2.js +1 -0
- package/dist/web/_app/immutable/chunks/C0pTXwEX.js +2 -0
- package/dist/web/_app/immutable/chunks/C3Enww9L2.js +1 -0
- package/dist/web/_app/immutable/chunks/C3oa0Cks.js +1 -0
- package/dist/web/_app/immutable/chunks/C7GIvT8T.js +2 -0
- package/dist/web/_app/immutable/chunks/CC-YWp-f.js +1 -0
- package/dist/web/_app/immutable/chunks/CGdKvXV92.js +1 -0
- package/dist/web/_app/immutable/chunks/CH0siYFL.js +1 -0
- package/dist/web/_app/immutable/chunks/CIc2MMme2.js +1 -0
- package/dist/web/_app/immutable/chunks/CIchr8Tk2.js +1 -0
- package/dist/web/_app/immutable/chunks/CK_yXDQI.js +1 -0
- package/dist/web/_app/immutable/chunks/CLSxJmTj.js +2 -0
- package/dist/web/_app/immutable/chunks/CMhHc1Bq2.js +2 -0
- package/dist/web/_app/immutable/chunks/CNGba6T4.js +1 -0
- package/dist/web/_app/immutable/chunks/CPt2D5aU.js +1 -0
- package/dist/web/_app/immutable/chunks/CQ-2yccd.js +7 -0
- package/dist/web/_app/immutable/chunks/CS6VEwn7.js +1 -0
- package/dist/web/_app/immutable/chunks/CSsDOIrk.js +1 -0
- package/dist/web/_app/immutable/chunks/CVXvEWhv2.js +1 -0
- package/dist/web/_app/immutable/chunks/CaChtuYU2.js +1 -0
- package/dist/web/_app/immutable/chunks/CbiwvM3R2.js +1 -0
- package/dist/web/_app/immutable/chunks/CdCuBMLm.js +156 -0
- package/dist/web/_app/immutable/chunks/CiyqPsJo.js +1 -0
- package/dist/web/_app/immutable/chunks/CjGzUnEF2.js +1 -0
- package/dist/web/_app/immutable/chunks/Cjgbiel7.js +1 -0
- package/dist/web/_app/immutable/chunks/CnqSKqA62.js +1 -0
- package/dist/web/_app/immutable/chunks/CqyqKPuh2.js +1 -0
- package/dist/web/_app/immutable/chunks/CrnX-oFV.js +1 -0
- package/dist/web/_app/immutable/chunks/CsGahE3a.js +1 -0
- package/dist/web/_app/immutable/chunks/Ct3quP0F.js +1 -0
- package/dist/web/_app/immutable/chunks/CtlAYPxw.js +1 -0
- package/dist/web/_app/immutable/chunks/CvZaxjRC.js +1 -0
- package/dist/web/_app/immutable/chunks/CwD4G56R.js +5 -0
- package/dist/web/_app/immutable/chunks/CwROuREs.js +1 -0
- package/dist/web/_app/immutable/chunks/CxalqrMB.js +1 -0
- package/dist/web/_app/immutable/chunks/D7qgUfnr.js +2487 -0
- package/dist/web/_app/immutable/chunks/D9-_Euza2.js +1 -0
- package/dist/web/_app/immutable/chunks/DC69SASj.js +2 -0
- package/dist/web/_app/immutable/chunks/DCCaoKt42.js +1 -0
- package/dist/web/_app/immutable/chunks/DDKweOZr2.js +23 -0
- package/dist/web/_app/immutable/chunks/DEY60osH.js +1 -0
- package/dist/web/_app/immutable/chunks/DG3QZPgI.js +1 -0
- package/dist/web/_app/immutable/chunks/DHepUItL.js +1 -0
- package/dist/web/_app/immutable/chunks/DNtqqgv32.js +1 -0
- package/dist/web/_app/immutable/chunks/DRl7vwNC2.js +184 -0
- package/dist/web/_app/immutable/chunks/DTc5W_V6.js +1 -0
- package/dist/web/_app/immutable/chunks/DaiJs3St.js +1 -0
- package/dist/web/_app/immutable/chunks/DfITyMKh2.js +1 -0
- package/dist/web/_app/immutable/chunks/DhuJRrEX2.js +1 -0
- package/dist/web/_app/immutable/chunks/DiP2EplP.js +3 -0
- package/dist/web/_app/immutable/chunks/DjlDV2JG.js +1 -0
- package/dist/web/_app/immutable/chunks/Dm25ot5x2.js +1 -0
- package/dist/web/_app/immutable/chunks/DpuSt-SO.js +2 -0
- package/dist/web/_app/immutable/chunks/DqOcb9RX2.js +83 -0
- package/dist/web/_app/immutable/chunks/DrbUJCAE.js +1 -0
- package/dist/web/_app/immutable/chunks/Du2-B3c8.js +1 -0
- package/dist/web/_app/immutable/chunks/DwdkE4UD2.js +1 -0
- package/dist/web/_app/immutable/chunks/DxWesPbn.js +1 -0
- package/dist/web/_app/immutable/chunks/DyYZqUV72.js +1 -0
- package/dist/web/_app/immutable/chunks/HfUnhvrv2.js +1 -0
- package/dist/web/_app/immutable/chunks/Je8-BtCk2.js +1 -0
- package/dist/web/_app/immutable/chunks/MSB7eA4_.js +1 -0
- package/dist/web/_app/immutable/chunks/NPe9WyW1.js +1 -0
- package/dist/web/_app/immutable/chunks/O6IzcxXJ.js +2 -0
- package/dist/web/_app/immutable/chunks/SXhc9rJW.js +1 -0
- package/dist/web/_app/immutable/chunks/SbWhkPMt.js +6 -0
- package/dist/web/_app/immutable/chunks/U40J2SnU.js +1 -0
- package/dist/web/_app/immutable/chunks/VS3l21Gu.js +1 -0
- package/dist/web/_app/immutable/chunks/YEPEvlbU2.js +1 -0
- package/dist/web/_app/immutable/chunks/Z7TNRcgK.js +11 -0
- package/dist/web/_app/immutable/chunks/ZCekGjo6.js +5 -0
- package/dist/web/_app/immutable/chunks/_rnxQ655.js +4 -0
- package/dist/web/_app/immutable/chunks/aALpjBW2.js +1 -0
- package/dist/web/_app/immutable/chunks/gNXLhsdy.js +1 -0
- package/dist/web/_app/immutable/chunks/hKSTEN36.js +1 -0
- package/dist/web/_app/immutable/chunks/jZZTbzL5.js +1 -0
- package/dist/web/_app/immutable/chunks/l4FIBKss.js +1 -0
- package/dist/web/_app/immutable/chunks/l_4snZnh2.js +1 -0
- package/dist/web/_app/immutable/chunks/mz-cgRgP.js +3 -0
- package/dist/web/_app/immutable/chunks/qgprkMcL.js +1 -0
- package/dist/web/_app/immutable/chunks/rOisBLC62.js +14 -0
- package/dist/web/_app/immutable/chunks/t08TznXc2.js +1 -0
- package/dist/web/_app/immutable/chunks/t62M88qj.js +1 -0
- package/dist/web/_app/immutable/chunks/uCohGWC42.js +1 -0
- package/dist/web/_app/immutable/chunks/uqxmB78b.js +1 -0
- package/dist/web/_app/immutable/chunks/vJkT0IPN.js +1 -0
- package/dist/web/_app/immutable/chunks/vwTMEIJQ.js +1 -0
- package/dist/web/_app/immutable/chunks/w1FCELam.js +64 -0
- package/dist/web/_app/immutable/chunks/xEzUlKaR2.js +1 -0
- package/dist/web/_app/immutable/chunks/zumDjcgJ.js +1 -0
- package/dist/web/_app/immutable/entry/app.DtdPIxaY.js +2 -0
- package/dist/web/_app/immutable/entry/start.Cqf8YmDj.js +1 -0
- package/dist/web/_app/immutable/nodes/0.CktqlAe7.js +2 -0
- package/dist/web/_app/immutable/nodes/1.Bs7cz_ba.js +1 -0
- package/dist/web/_app/immutable/nodes/10.CAQtOBqO.js +1 -0
- package/dist/web/_app/immutable/nodes/11.l9TQGnJm.js +1 -0
- package/dist/web/_app/immutable/nodes/12.CttDoIpY.js +1 -0
- package/dist/web/_app/immutable/nodes/13.E5fh69kS.js +1 -0
- package/dist/web/_app/immutable/nodes/2.DnLSJfx-.js +106 -0
- package/dist/web/_app/immutable/nodes/3.Bw8R0Jw5.js +2 -0
- package/dist/web/_app/immutable/nodes/4.Cl0HgeTG.js +2 -0
- package/dist/web/_app/immutable/nodes/5.BpIWwWi9.js +1 -0
- package/dist/web/_app/immutable/nodes/6.jpx6ZgOI.js +1 -0
- package/dist/web/_app/immutable/nodes/7.YTFykZQe.js +1 -0
- package/dist/web/_app/immutable/nodes/8.Chy0HNU1.js +1 -0
- package/dist/web/_app/immutable/nodes/9.D10JpFt2.js +1 -0
- package/dist/web/_app/version.json +1 -1
- package/dist/web/favicon-16.png +0 -0
- package/dist/web/favicon-180.png +0 -0
- package/dist/web/favicon-32.png +0 -0
- package/dist/web/favicon-48.png +0 -0
- package/dist/web/favicon-512.png +0 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/favicon.svg +8 -0
- package/dist/web/index.html +64 -21
- package/dist/web/studiograph-logomark.svg +29 -0
- package/package.json +28 -14
- package/dist/agent/role-loader.d.ts +0 -24
- package/dist/agent/role-loader.js +0 -67
- package/dist/agent/role-loader.js.map +0 -1
- package/dist/agent/skills/enrich-entities.md +0 -136
- package/dist/agent/skills/obsidian-source-setup.md +0 -246
- package/dist/agent/skills/skill-loader.d.ts +0 -48
- package/dist/agent/skills/skill-loader.js +0 -166
- package/dist/agent/skills/skill-loader.js.map +0 -1
- package/dist/agent/skills/sync-configuration.md +0 -119
- package/dist/agent/skills/sync-setup.md +0 -82
- package/dist/agent/tools/sync-tools.d.ts +0 -35
- package/dist/agent/tools/sync-tools.js +0 -992
- package/dist/agent/tools/sync-tools.js.map +0 -1
- package/dist/auth/github.d.ts +0 -56
- package/dist/auth/github.js +0 -169
- package/dist/auth/github.js.map +0 -1
- package/dist/cli/commands/access.d.ts +0 -11
- package/dist/cli/commands/access.js +0 -156
- package/dist/cli/commands/access.js.map +0 -1
- package/dist/cli/commands/app.d.ts +0 -7
- package/dist/cli/commands/app.js +0 -268
- package/dist/cli/commands/app.js.map +0 -1
- package/dist/cli/commands/auth.d.ts +0 -10
- package/dist/cli/commands/auth.js +0 -79
- package/dist/cli/commands/auth.js.map +0 -1
- package/dist/cli/commands/collection.d.ts +0 -10
- package/dist/cli/commands/collection.js +0 -185
- package/dist/cli/commands/collection.js.map +0 -1
- package/dist/cli/commands/enrich.d.ts +0 -11
- package/dist/cli/commands/enrich.js +0 -135
- package/dist/cli/commands/enrich.js.map +0 -1
- package/dist/cli/commands/graphrag.d.ts +0 -12
- package/dist/cli/commands/graphrag.js +0 -122
- package/dist/cli/commands/graphrag.js.map +0 -1
- package/dist/cli/commands/members.d.ts +0 -11
- package/dist/cli/commands/members.js +0 -230
- package/dist/cli/commands/members.js.map +0 -1
- package/dist/cli/commands/org.d.ts +0 -10
- package/dist/cli/commands/org.js +0 -132
- package/dist/cli/commands/org.js.map +0 -1
- package/dist/cli/commands/provision.d.ts +0 -8
- package/dist/cli/commands/provision.js +0 -116
- package/dist/cli/commands/provision.js.map +0 -1
- package/dist/cli/commands/resolve.d.ts +0 -8
- package/dist/cli/commands/resolve.js +0 -85
- package/dist/cli/commands/resolve.js.map +0 -1
- package/dist/cli/commands/review.d.ts +0 -19
- package/dist/cli/commands/review.js +0 -128
- package/dist/cli/commands/review.js.map +0 -1
- package/dist/cli/commands/source.d.ts +0 -16
- package/dist/cli/commands/source.js +0 -159
- package/dist/cli/commands/source.js.map +0 -1
- package/dist/cli/commands/sync-collection.d.ts +0 -14
- package/dist/cli/commands/sync-collection.js +0 -355
- package/dist/cli/commands/sync-collection.js.map +0 -1
- package/dist/cli/commands/sync-entities.d.ts +0 -13
- package/dist/cli/commands/sync-entities.js +0 -242
- package/dist/cli/commands/sync-entities.js.map +0 -1
- package/dist/cli/commands/team.d.ts +0 -12
- package/dist/cli/commands/team.js +0 -185
- package/dist/cli/commands/team.js.map +0 -1
- package/dist/cli/sync-review-interactive.d.ts +0 -31
- package/dist/cli/sync-review-interactive.js +0 -393
- package/dist/cli/sync-review-interactive.js.map +0 -1
- package/dist/core/migration-runner.d.ts +0 -42
- package/dist/core/migration-runner.js +0 -232
- package/dist/core/migration-runner.js.map +0 -1
- package/dist/core/migration-types.d.ts +0 -101
- package/dist/core/migration-types.js +0 -21
- package/dist/core/migration-types.js.map +0 -1
- package/dist/core/migrations/20260219-formalize-memory-location.d.ts +0 -2
- package/dist/core/migrations/20260219-formalize-memory-location.js +0 -35
- package/dist/core/migrations/20260219-formalize-memory-location.js.map +0 -1
- package/dist/core/migrations/20260220-add-workspace-metadata.d.ts +0 -12
- package/dist/core/migrations/20260220-add-workspace-metadata.js +0 -65
- package/dist/core/migrations/20260220-add-workspace-metadata.js.map +0 -1
- package/dist/core/migrations/20260220-add-workspace-readme.d.ts +0 -11
- package/dist/core/migrations/20260220-add-workspace-readme.js +0 -82
- package/dist/core/migrations/20260220-add-workspace-readme.js.map +0 -1
- package/dist/core/migrations/20260220-migrate-yaml-to-json.d.ts +0 -9
- package/dist/core/migrations/20260220-migrate-yaml-to-json.js +0 -64
- package/dist/core/migrations/20260220-migrate-yaml-to-json.js.map +0 -1
- package/dist/core/migrations/index.d.ts +0 -11
- package/dist/core/migrations/index.js +0 -23
- package/dist/core/migrations/index.js.map +0 -1
- package/dist/integrations/asana.d.ts +0 -26
- package/dist/integrations/asana.js +0 -78
- package/dist/integrations/asana.js.map +0 -1
- package/dist/integrations/figma-local.d.ts +0 -16
- package/dist/integrations/figma-local.js +0 -10
- package/dist/integrations/figma-local.js.map +0 -1
- package/dist/integrations/figma.d.ts +0 -17
- package/dist/integrations/figma.js +0 -17
- package/dist/integrations/figma.js.map +0 -1
- package/dist/integrations/granola.d.ts +0 -24
- package/dist/integrations/granola.js +0 -60
- package/dist/integrations/granola.js.map +0 -1
- package/dist/integrations/linear.d.ts +0 -14
- package/dist/integrations/linear.js +0 -80
- package/dist/integrations/linear.js.map +0 -1
- package/dist/integrations/paper-local.d.ts +0 -14
- package/dist/integrations/paper-local.js +0 -10
- package/dist/integrations/paper-local.js.map +0 -1
- package/dist/integrations/paper.d.ts +0 -2
- package/dist/integrations/paper.js +0 -10
- package/dist/integrations/paper.js.map +0 -1
- package/dist/integrations/pipedrive.d.ts +0 -26
- package/dist/integrations/pipedrive.js +0 -97
- package/dist/integrations/pipedrive.js.map +0 -1
- package/dist/integrations/registry.d.ts +0 -8
- package/dist/integrations/registry.js +0 -7
- package/dist/integrations/registry.js.map +0 -1
- package/dist/integrations/types.d.ts +0 -43
- package/dist/integrations/types.js +0 -5
- package/dist/integrations/types.js.map +0 -1
- package/dist/lib/lib/utils.d.ts +0 -2
- package/dist/lib/lib/utils.js +0 -6
- package/dist/lib/lib/utils.js.map +0 -1
- package/dist/mcp/connectors/asana.d.ts +0 -2
- package/dist/mcp/connectors/asana.js +0 -20
- package/dist/mcp/connectors/asana.js.map +0 -1
- package/dist/mcp/connectors/definitions.d.ts +0 -45
- package/dist/mcp/connectors/definitions.js +0 -32
- package/dist/mcp/connectors/definitions.js.map +0 -1
- package/dist/mcp/connectors/figma.d.ts +0 -5
- package/dist/mcp/connectors/figma.js +0 -21
- package/dist/mcp/connectors/figma.js.map +0 -1
- package/dist/mcp/connectors/gdrive.d.ts +0 -2
- package/dist/mcp/connectors/gdrive.js +0 -20
- package/dist/mcp/connectors/gdrive.js.map +0 -1
- package/dist/mcp/connectors/granola.d.ts +0 -2
- package/dist/mcp/connectors/granola.js +0 -12
- package/dist/mcp/connectors/granola.js.map +0 -1
- package/dist/mcp/connectors/linear.d.ts +0 -2
- package/dist/mcp/connectors/linear.js +0 -19
- package/dist/mcp/connectors/linear.js.map +0 -1
- package/dist/mcp/connectors/obsidian.d.ts +0 -2
- package/dist/mcp/connectors/obsidian.js +0 -19
- package/dist/mcp/connectors/obsidian.js.map +0 -1
- package/dist/mcp/connectors/pipedrive.d.ts +0 -2
- package/dist/mcp/connectors/pipedrive.js +0 -20
- package/dist/mcp/connectors/pipedrive.js.map +0 -1
- package/dist/mcp/connectors/slack.d.ts +0 -2
- package/dist/mcp/connectors/slack.js +0 -21
- package/dist/mcp/connectors/slack.js.map +0 -1
- package/dist/mcp/server-oauth-provider.d.ts +0 -56
- package/dist/mcp/server-oauth-provider.js +0 -142
- package/dist/mcp/server-oauth-provider.js.map +0 -1
- package/dist/server/agents/agent-loader.d.ts +0 -16
- package/dist/server/agents/agent-loader.js +0 -175
- package/dist/server/agents/agent-loader.js.map +0 -1
- package/dist/server/agents/agent-runner.d.ts +0 -33
- package/dist/server/agents/agent-runner.js +0 -187
- package/dist/server/agents/agent-runner.js.map +0 -1
- package/dist/server/agents/agent-runtime.d.ts +0 -39
- package/dist/server/agents/agent-runtime.js +0 -214
- package/dist/server/agents/agent-runtime.js.map +0 -1
- package/dist/server/agents/base-agent.d.ts +0 -30
- package/dist/server/agents/base-agent.js +0 -88
- package/dist/server/agents/base-agent.js.map +0 -1
- package/dist/server/agents/executive-digest.d.ts +0 -16
- package/dist/server/agents/executive-digest.js +0 -115
- package/dist/server/agents/executive-digest.js.map +0 -1
- package/dist/server/agents/graph-steward.d.ts +0 -16
- package/dist/server/agents/graph-steward.js +0 -59
- package/dist/server/agents/graph-steward.js.map +0 -1
- package/dist/server/agents/meeting-followup.d.ts +0 -16
- package/dist/server/agents/meeting-followup.js +0 -88
- package/dist/server/agents/meeting-followup.js.map +0 -1
- package/dist/server/agents/pipeline-monitor.d.ts +0 -15
- package/dist/server/agents/pipeline-monitor.js +0 -86
- package/dist/server/agents/pipeline-monitor.js.map +0 -1
- package/dist/server/agents/types.d.ts +0 -81
- package/dist/server/agents/types.js +0 -41
- package/dist/server/agents/types.js.map +0 -1
- package/dist/server/chrome/chrome.css +0 -691
- package/dist/server/chrome/chrome.js +0 -374
- package/dist/server/collab-authority.d.ts +0 -70
- package/dist/server/collab-authority.js +0 -218
- package/dist/server/collab-authority.js.map +0 -1
- package/dist/server/collab.d.ts +0 -29
- package/dist/server/collab.js +0 -195
- package/dist/server/collab.js.map +0 -1
- package/dist/server/commit-scheduler.d.ts +0 -39
- package/dist/server/commit-scheduler.js +0 -113
- package/dist/server/commit-scheduler.js.map +0 -1
- package/dist/server/plugin-loader.d.ts +0 -53
- package/dist/server/plugin-loader.js +0 -155
- package/dist/server/plugin-loader.js.map +0 -1
- package/dist/server/routes/agents-api.d.ts +0 -10
- package/dist/server/routes/agents-api.js +0 -25
- package/dist/server/routes/agents-api.js.map +0 -1
- package/dist/server/routes/collab.d.ts +0 -6
- package/dist/server/routes/collab.js +0 -10
- package/dist/server/routes/collab.js.map +0 -1
- package/dist/server/routes/git-api.d.ts +0 -9
- package/dist/server/routes/git-api.js +0 -82
- package/dist/server/routes/git-api.js.map +0 -1
- package/dist/server/routes/meeting-ingest-api.d.ts +0 -14
- package/dist/server/routes/meeting-ingest-api.js +0 -98
- package/dist/server/routes/meeting-ingest-api.js.map +0 -1
- package/dist/server/routes/sync-api.d.ts +0 -26
- package/dist/server/routes/sync-api.js +0 -848
- package/dist/server/routes/sync-api.js.map +0 -1
- package/dist/server/routes/webhook.d.ts +0 -14
- package/dist/server/routes/webhook.js +0 -102
- package/dist/server/routes/webhook.js.map +0 -1
- package/dist/services/batch-processor.d.ts +0 -49
- package/dist/services/batch-processor.js +0 -166
- package/dist/services/batch-processor.js.map +0 -1
- package/dist/services/csv-service.d.ts +0 -36
- package/dist/services/csv-service.js +0 -143
- package/dist/services/csv-service.js.map +0 -1
- package/dist/services/github-provisioner.d.ts +0 -36
- package/dist/services/github-provisioner.js +0 -126
- package/dist/services/github-provisioner.js.map +0 -1
- package/dist/services/github-service.d.ts +0 -99
- package/dist/services/github-service.js +0 -310
- package/dist/services/github-service.js.map +0 -1
- package/dist/services/image-import-service.d.ts +0 -24
- package/dist/services/image-import-service.js +0 -108
- package/dist/services/image-import-service.js.map +0 -1
- package/dist/services/insights.d.ts +0 -38
- package/dist/services/insights.js +0 -269
- package/dist/services/insights.js.map +0 -1
- package/dist/services/meeting-processor.d.ts +0 -91
- package/dist/services/meeting-processor.js +0 -331
- package/dist/services/meeting-processor.js.map +0 -1
- package/dist/services/sync/collection-sync.d.ts +0 -60
- package/dist/services/sync/collection-sync.js +0 -509
- package/dist/services/sync/collection-sync.js.map +0 -1
- package/dist/services/sync/commit.d.ts +0 -60
- package/dist/services/sync/commit.js +0 -354
- package/dist/services/sync/commit.js.map +0 -1
- package/dist/services/sync/context-index.d.ts +0 -69
- package/dist/services/sync/context-index.js +0 -280
- package/dist/services/sync/context-index.js.map +0 -1
- package/dist/services/sync/data-fetcher.d.ts +0 -31
- package/dist/services/sync/data-fetcher.js +0 -12
- package/dist/services/sync/data-fetcher.js.map +0 -1
- package/dist/services/sync/derive.d.ts +0 -34
- package/dist/services/sync/derive.js +0 -164
- package/dist/services/sync/derive.js.map +0 -1
- package/dist/services/sync/enrichment-state.d.ts +0 -31
- package/dist/services/sync/enrichment-state.js +0 -63
- package/dist/services/sync/enrichment-state.js.map +0 -1
- package/dist/services/sync/enrichment.d.ts +0 -25
- package/dist/services/sync/enrichment.js +0 -121
- package/dist/services/sync/enrichment.js.map +0 -1
- package/dist/services/sync/entity-refresh.d.ts +0 -30
- package/dist/services/sync/entity-refresh.js +0 -275
- package/dist/services/sync/entity-refresh.js.map +0 -1
- package/dist/services/sync/frontmatter-extractor.d.ts +0 -40
- package/dist/services/sync/frontmatter-extractor.js +0 -279
- package/dist/services/sync/frontmatter-extractor.js.map +0 -1
- package/dist/services/sync/graph-match-state.d.ts +0 -33
- package/dist/services/sync/graph-match-state.js +0 -61
- package/dist/services/sync/graph-match-state.js.map +0 -1
- package/dist/services/sync/graph-match.d.ts +0 -53
- package/dist/services/sync/graph-match.js +0 -316
- package/dist/services/sync/graph-match.js.map +0 -1
- package/dist/services/sync/graphrag-client.d.ts +0 -43
- package/dist/services/sync/graphrag-client.js +0 -94
- package/dist/services/sync/graphrag-client.js.map +0 -1
- package/dist/services/sync/graphrag-config.d.ts +0 -16
- package/dist/services/sync/graphrag-config.js +0 -39
- package/dist/services/sync/graphrag-config.js.map +0 -1
- package/dist/services/sync/graphrag-context.d.ts +0 -14
- package/dist/services/sync/graphrag-context.js +0 -109
- package/dist/services/sync/graphrag-context.js.map +0 -1
- package/dist/services/sync/graphrag-indexer.d.ts +0 -30
- package/dist/services/sync/graphrag-indexer.js +0 -358
- package/dist/services/sync/graphrag-indexer.js.map +0 -1
- package/dist/services/sync/llm.d.ts +0 -32
- package/dist/services/sync/llm.js +0 -115
- package/dist/services/sync/llm.js.map +0 -1
- package/dist/services/sync/mcp-client.d.ts +0 -71
- package/dist/services/sync/mcp-client.js +0 -299
- package/dist/services/sync/mcp-client.js.map +0 -1
- package/dist/services/sync/model-factory.d.ts +0 -13
- package/dist/services/sync/model-factory.js +0 -38
- package/dist/services/sync/model-factory.js.map +0 -1
- package/dist/services/sync/name-quality.d.ts +0 -31
- package/dist/services/sync/name-quality.js +0 -60
- package/dist/services/sync/name-quality.js.map +0 -1
- package/dist/services/sync/output-schemas.d.ts +0 -92
- package/dist/services/sync/output-schemas.js +0 -43
- package/dist/services/sync/output-schemas.js.map +0 -1
- package/dist/services/sync/prompts.d.ts +0 -19
- package/dist/services/sync/prompts.js +0 -128
- package/dist/services/sync/prompts.js.map +0 -1
- package/dist/services/sync/reconciler.d.ts +0 -48
- package/dist/services/sync/reconciler.js +0 -294
- package/dist/services/sync/reconciler.js.map +0 -1
- package/dist/services/sync/rest-client.d.ts +0 -40
- package/dist/services/sync/rest-client.js +0 -100
- package/dist/services/sync/rest-client.js.map +0 -1
- package/dist/services/sync/source-config.d.ts +0 -67
- package/dist/services/sync/source-config.js +0 -304
- package/dist/services/sync/source-config.js.map +0 -1
- package/dist/services/sync/source-definitions/asana.d.ts +0 -14
- package/dist/services/sync/source-definitions/asana.js +0 -42
- package/dist/services/sync/source-definitions/asana.js.map +0 -1
- package/dist/services/sync/source-definitions/definitions.d.ts +0 -8
- package/dist/services/sync/source-definitions/definitions.js +0 -8
- package/dist/services/sync/source-definitions/definitions.js.map +0 -1
- package/dist/services/sync/source-definitions/gdrive.d.ts +0 -16
- package/dist/services/sync/source-definitions/gdrive.js +0 -68
- package/dist/services/sync/source-definitions/gdrive.js.map +0 -1
- package/dist/services/sync/source-definitions/granola.d.ts +0 -2
- package/dist/services/sync/source-definitions/granola.js +0 -21
- package/dist/services/sync/source-definitions/granola.js.map +0 -1
- package/dist/services/sync/source-definitions/linear.d.ts +0 -2
- package/dist/services/sync/source-definitions/linear.js +0 -62
- package/dist/services/sync/source-definitions/linear.js.map +0 -1
- package/dist/services/sync/source-definitions/obsidian.d.ts +0 -2
- package/dist/services/sync/source-definitions/obsidian.js +0 -55
- package/dist/services/sync/source-definitions/obsidian.js.map +0 -1
- package/dist/services/sync/source-definitions/pipedrive.d.ts +0 -2
- package/dist/services/sync/source-definitions/pipedrive.js +0 -43
- package/dist/services/sync/source-definitions/pipedrive.js.map +0 -1
- package/dist/services/sync/staging.d.ts +0 -53
- package/dist/services/sync/staging.js +0 -130
- package/dist/services/sync/staging.js.map +0 -1
- package/dist/services/sync/structured-extractor.d.ts +0 -57
- package/dist/services/sync/structured-extractor.js +0 -584
- package/dist/services/sync/structured-extractor.js.map +0 -1
- package/dist/services/sync/sync-runner.d.ts +0 -32
- package/dist/services/sync/sync-runner.js +0 -195
- package/dist/services/sync/sync-runner.js.map +0 -1
- package/dist/services/sync/sync-state.d.ts +0 -43
- package/dist/services/sync/sync-state.js +0 -154
- package/dist/services/sync/sync-state.js.map +0 -1
- package/dist/services/sync/types.d.ts +0 -381
- package/dist/services/sync/types.js +0 -8
- package/dist/services/sync/types.js.map +0 -1
- package/dist/services/sync/unstructured-extractor.d.ts +0 -27
- package/dist/services/sync/unstructured-extractor.js +0 -185
- package/dist/services/sync/unstructured-extractor.js.map +0 -1
- package/dist/utils/merge-resolver.d.ts +0 -59
- package/dist/utils/merge-resolver.js +0 -303
- package/dist/utils/merge-resolver.js.map +0 -1
- package/dist/utils/workspace-config.d.ts +0 -8
- package/dist/utils/workspace-config.js +0 -22
- package/dist/utils/workspace-config.js.map +0 -1
- package/dist/web/_app/immutable/assets/0.QUqBcyuu.css +0 -1
- package/dist/web/_app/immutable/assets/11.2jzI3cZY.css +0 -1
- package/dist/web/_app/immutable/assets/12.CT4xL4K3.css +0 -1
- package/dist/web/_app/immutable/assets/13.BUrHkYry.css +0 -1
- package/dist/web/_app/immutable/assets/2.5jNm7Pm1.css +0 -1
- package/dist/web/_app/immutable/assets/3.BxdqT7zi.css +0 -1
- package/dist/web/_app/immutable/assets/4.BjPwtpNd.css +0 -1
- package/dist/web/_app/immutable/assets/5.DFeGxLYf.css +0 -1
- package/dist/web/_app/immutable/assets/6.DXZr_rI_.css +0 -1
- package/dist/web/_app/immutable/assets/GraphView.D9ePpZez.css +0 -1
- package/dist/web/_app/immutable/assets/Toaster.rN8r_Hzv.css +0 -1
- package/dist/web/_app/immutable/assets/ViewToolbar.CZjsdjy4.css +0 -1
- package/dist/web/_app/immutable/assets/WorkspaceView.B-1DbivC.css +0 -1
- package/dist/web/_app/immutable/assets/wikilinks.CtSydmQl.css +0 -1
- package/dist/web/_app/immutable/chunks/3--t-YNA.js +0 -1
- package/dist/web/_app/immutable/chunks/5OU-4caN.js +0 -1
- package/dist/web/_app/immutable/chunks/7S2LGqoP.js +0 -2
- package/dist/web/_app/immutable/chunks/B6FUcAv5.js +0 -1
- package/dist/web/_app/immutable/chunks/B9gDgSlp.js +0 -1
- package/dist/web/_app/immutable/chunks/BB_5th5W.js +0 -3383
- package/dist/web/_app/immutable/chunks/BFtIWl5z.js +0 -1
- package/dist/web/_app/immutable/chunks/BHKpz6_7.js +0 -1
- package/dist/web/_app/immutable/chunks/BNfrhBeA.js +0 -1
- package/dist/web/_app/immutable/chunks/BgO5HlCg.js +0 -2
- package/dist/web/_app/immutable/chunks/BqnODQDb.js +0 -1
- package/dist/web/_app/immutable/chunks/BrebtJNG.js +0 -18
- package/dist/web/_app/immutable/chunks/BvIFhEn7.js +0 -2
- package/dist/web/_app/immutable/chunks/BvfdTHv8.js +0 -1
- package/dist/web/_app/immutable/chunks/BxGm65PH.js +0 -1
- package/dist/web/_app/immutable/chunks/BxyxnEKw.js +0 -1
- package/dist/web/_app/immutable/chunks/BygFIGq4.js +0 -1
- package/dist/web/_app/immutable/chunks/Bz0ZjVQE.js +0 -2
- package/dist/web/_app/immutable/chunks/C0LVZhvn.js +0 -1
- package/dist/web/_app/immutable/chunks/CIb_hHMy.js +0 -1
- package/dist/web/_app/immutable/chunks/CNKK6nZb.js +0 -1
- package/dist/web/_app/immutable/chunks/CQ5FjHZK.js +0 -1
- package/dist/web/_app/immutable/chunks/CSVbGg_b.js +0 -5
- package/dist/web/_app/immutable/chunks/CX_61fY8.js +0 -5
- package/dist/web/_app/immutable/chunks/CYrVHOHA.js +0 -1
- package/dist/web/_app/immutable/chunks/CaEaeeKb.js +0 -1
- package/dist/web/_app/immutable/chunks/Ch6e_wpP.js +0 -1
- package/dist/web/_app/immutable/chunks/Ci4zzCVB.js +0 -2
- package/dist/web/_app/immutable/chunks/CknqBVQE.js +0 -1
- package/dist/web/_app/immutable/chunks/Cmfiirqp.js +0 -4
- package/dist/web/_app/immutable/chunks/CqkleIqs.js +0 -1
- package/dist/web/_app/immutable/chunks/CrFpCC6p.js +0 -1
- package/dist/web/_app/immutable/chunks/CxX8hcT6.js +0 -1
- package/dist/web/_app/immutable/chunks/D0T1ps1G.js +0 -1
- package/dist/web/_app/immutable/chunks/D2ZEj9xD.js +0 -1
- package/dist/web/_app/immutable/chunks/D3sy0IkA.js +0 -2
- package/dist/web/_app/immutable/chunks/DFYOFE3o.js +0 -1
- package/dist/web/_app/immutable/chunks/DHWoAC4y.js +0 -1
- package/dist/web/_app/immutable/chunks/DYYBfXU3.js +0 -1
- package/dist/web/_app/immutable/chunks/DiNPRA8T.js +0 -1
- package/dist/web/_app/immutable/chunks/DkjsqHkM.js +0 -1
- package/dist/web/_app/immutable/chunks/DnL9f0lc.js +0 -7
- package/dist/web/_app/immutable/chunks/DpMCynC3.js +0 -1
- package/dist/web/_app/immutable/chunks/DqrybUuA.js +0 -1
- package/dist/web/_app/immutable/chunks/DsnmJJEf.js +0 -1
- package/dist/web/_app/immutable/chunks/KyjzlWRN.js +0 -2
- package/dist/web/_app/immutable/chunks/O9MAeAgV.js +0 -5
- package/dist/web/_app/immutable/chunks/PPVm8Dsz.js +0 -1
- package/dist/web/_app/immutable/chunks/SKSQ4m85.js +0 -2
- package/dist/web/_app/immutable/chunks/WEBTyD-H.js +0 -1
- package/dist/web/_app/immutable/chunks/WSUKABI_.js +0 -1
- package/dist/web/_app/immutable/chunks/bBXT5iJP.js +0 -1
- package/dist/web/_app/immutable/chunks/vxEtkL8z.js +0 -1
- package/dist/web/_app/immutable/chunks/wDIGUaoU.js +0 -3
- package/dist/web/_app/immutable/chunks/xkNhbnff.js +0 -1
- package/dist/web/_app/immutable/chunks/zoVX1Zab.js +0 -1
- package/dist/web/_app/immutable/entry/app.GfE4z2GO.js +0 -2
- package/dist/web/_app/immutable/entry/start.H-LFJK9u.js +0 -1
- package/dist/web/_app/immutable/nodes/0.Bwi7tTNC.js +0 -2
- package/dist/web/_app/immutable/nodes/1.DgNJPEWk.js +0 -1
- package/dist/web/_app/immutable/nodes/10.BW_XCD2O.js +0 -1
- package/dist/web/_app/immutable/nodes/11.DM7RX3Ms.js +0 -1
- package/dist/web/_app/immutable/nodes/12.DKgssZo8.js +0 -1
- package/dist/web/_app/immutable/nodes/13.DCdmTre-.js +0 -1
- package/dist/web/_app/immutable/nodes/2.DOA5QBVG.js +0 -266
- package/dist/web/_app/immutable/nodes/3.bpoHQwAI.js +0 -5
- package/dist/web/_app/immutable/nodes/4.ye0Vy7A1.js +0 -11
- package/dist/web/_app/immutable/nodes/5.rxpnYrx_.js +0 -4
- package/dist/web/_app/immutable/nodes/6.DP8HSxtc.js +0 -1
- package/dist/web/_app/immutable/nodes/7.C1h4Bf-E.js +0 -1
- package/dist/web/_app/immutable/nodes/8.BzmQ7S7Y.js +0 -1
- package/dist/web/_app/immutable/nodes/9.CEakpXpF.js +0 -1
package/dist/core/graph.js
CHANGED
|
@@ -4,14 +4,34 @@
|
|
|
4
4
|
* Provides unified entity management across any repo type.
|
|
5
5
|
* Integrates Git, Markdown, and Asset services.
|
|
6
6
|
*/
|
|
7
|
-
import { join, dirname } from 'path';
|
|
8
|
-
import { existsSync, readdirSync, statSync, mkdirSync } from 'fs';
|
|
7
|
+
import { join, dirname, relative } from 'path';
|
|
8
|
+
import { existsSync, readdirSync, rmSync, statSync, writeFileSync, mkdirSync } from 'fs';
|
|
9
9
|
import { GitService } from '../services/git.js';
|
|
10
10
|
import { MarkdownService } from '../services/markdown.js';
|
|
11
11
|
import { AssetService } from '../services/assets/index.js';
|
|
12
|
-
import { ENTITY_SCHEMAS, EntityBaseSchema,
|
|
13
|
-
import {
|
|
12
|
+
import { ENTITY_SCHEMAS, EntityBaseSchema, PLURAL_TO_SINGULAR } from './validation.js';
|
|
13
|
+
import { folderPathFromFile, normalizeFolderPath, validateFolderPath, InvalidFolderPathError } from './folder-paths.js';
|
|
14
|
+
import { FOLDER_SIDECAR, folderSidecarPath, readFolderSidecar, removeFolderSidecar, writeFolderSidecar } from './folder-sidecar.js';
|
|
14
15
|
import { z } from 'zod';
|
|
16
|
+
/** Names skipped during recursive repo walks. */
|
|
17
|
+
const WALK_IGNORE = new Set(['.git', '.studiograph', 'node_modules']);
|
|
18
|
+
/**
|
|
19
|
+
* Thrown by `restoreFile` when the path the recreate would land at is
|
|
20
|
+
* currently occupied by a different entity. Carries the occupant's
|
|
21
|
+
* coordinates so callers (agent tool, REST endpoint) can surface a
|
|
22
|
+
* useful error to the user.
|
|
23
|
+
*/
|
|
24
|
+
export class PathConflictError extends Error {
|
|
25
|
+
path;
|
|
26
|
+
occupiedBy;
|
|
27
|
+
code = 'PATH_CONFLICT';
|
|
28
|
+
constructor(path, occupiedBy) {
|
|
29
|
+
super(`Path ${path} is occupied by ${occupiedBy.entityType}/${occupiedBy.entityId}`);
|
|
30
|
+
this.path = path;
|
|
31
|
+
this.occupiedBy = occupiedBy;
|
|
32
|
+
this.name = 'PathConflictError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
15
35
|
export class BaseGraphManager {
|
|
16
36
|
repoPath;
|
|
17
37
|
repoName;
|
|
@@ -20,9 +40,10 @@ export class BaseGraphManager {
|
|
|
20
40
|
markdownService;
|
|
21
41
|
assetService;
|
|
22
42
|
gitUser;
|
|
23
|
-
csvService;
|
|
24
43
|
vectorService;
|
|
25
44
|
schemaRegistry;
|
|
45
|
+
workspaceRoot;
|
|
46
|
+
mutationCallbacks;
|
|
26
47
|
constructor(config) {
|
|
27
48
|
this.repoPath = config.repoPath;
|
|
28
49
|
this.repoName = config.repoName;
|
|
@@ -30,12 +51,12 @@ export class BaseGraphManager {
|
|
|
30
51
|
this.noGit = config.noGit ?? false;
|
|
31
52
|
this.vectorService = config.vectorService;
|
|
32
53
|
this.schemaRegistry = config.schemaRegistry;
|
|
54
|
+
this.workspaceRoot = config.workspaceRoot;
|
|
33
55
|
// Initialize services
|
|
34
56
|
if (!this.noGit) {
|
|
35
57
|
this.gitService = new GitService(this.repoPath);
|
|
36
58
|
}
|
|
37
59
|
this.markdownService = new MarkdownService();
|
|
38
|
-
this.csvService = new CSVService();
|
|
39
60
|
// Asset service is optional (can be disabled for repos without assets)
|
|
40
61
|
if (config.assetConfig) {
|
|
41
62
|
this.assetService = new AssetService(config.assetConfig);
|
|
@@ -43,10 +64,41 @@ export class BaseGraphManager {
|
|
|
43
64
|
// Ensure repo is initialized
|
|
44
65
|
this.ensureRepo();
|
|
45
66
|
}
|
|
46
|
-
/** Expose git service for deferred commit operations (
|
|
67
|
+
/** Expose git service for deferred commit operations (SessionManager). */
|
|
47
68
|
getGitService() {
|
|
48
69
|
return this.gitService;
|
|
49
70
|
}
|
|
71
|
+
/** Absolute repo path on disk. Used by callers that need to convert
|
|
72
|
+
* an entity's absolute path to a repo-relative one (e.g. for `git log`). */
|
|
73
|
+
getRepoPath() {
|
|
74
|
+
return this.repoPath;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Find an entity at a specific commit (existing or deleted) and
|
|
78
|
+
* return its repo-relative path + content. Public surface for the
|
|
79
|
+
* history tools — wraps the private `findEntityInCommit` walker.
|
|
80
|
+
* Returns null when no .md file at that commit matches the
|
|
81
|
+
* (entityType, entityId) pair.
|
|
82
|
+
*/
|
|
83
|
+
getEntityAtCommit(entityType, entityId, commitHash) {
|
|
84
|
+
return this.findEntityInCommit(entityType, entityId, commitHash);
|
|
85
|
+
}
|
|
86
|
+
/** Workspace root passed in via GraphConfig. Used by the asset upload
|
|
87
|
+
* service to anchor PDF text sidecars at workspace level (so all repos
|
|
88
|
+
* share the same `assets-text/` tree, mirroring the shared
|
|
89
|
+
* `.studiograph/assets/` directory the local backend uses). */
|
|
90
|
+
getWorkspaceRoot() {
|
|
91
|
+
return this.workspaceRoot;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Install post-mutation callbacks. Called once after construction by the
|
|
95
|
+
* server bootstrap (or test setup) so the Yjs persistence layer can react
|
|
96
|
+
* to structural mutations without route-layer code calling persistence
|
|
97
|
+
* APIs directly.
|
|
98
|
+
*/
|
|
99
|
+
setMutationCallbacks(callbacks) {
|
|
100
|
+
this.mutationCallbacks = callbacks;
|
|
101
|
+
}
|
|
50
102
|
/**
|
|
51
103
|
* Ensure repository exists
|
|
52
104
|
*/
|
|
@@ -57,11 +109,127 @@ export class BaseGraphManager {
|
|
|
57
109
|
// GitService automatically initializes in constructor
|
|
58
110
|
}
|
|
59
111
|
/**
|
|
60
|
-
*
|
|
112
|
+
* Compute the on-disk path for a new entity.
|
|
113
|
+
*
|
|
114
|
+
* - When `folderPath` is provided, the entry lives at {repo}/{folderPath}/{id}.md
|
|
115
|
+
* regardless of type. This is the nested-folders layout.
|
|
116
|
+
* - Without `folderPath` (or empty string), the entry lands at the repo
|
|
117
|
+
* root: {repo}/{id}.md. The historical {repo}/{plural-type}/{id}.md
|
|
118
|
+
* fallback was confusing — entries hidden under a typed directory the
|
|
119
|
+
* list view never showed at repo root, contradicting the agent tool
|
|
120
|
+
* description that says "Omit to land at repo root."
|
|
121
|
+
*
|
|
122
|
+
* Note: this is for *new* paths only. Existing entries living under the
|
|
123
|
+
* legacy {plural-type}/ directories continue to be readable via
|
|
124
|
+
* findEntityFile() — only the create path moved.
|
|
125
|
+
*/
|
|
126
|
+
getEntityPath(entityType, entityId, folderPath) {
|
|
127
|
+
const normalized = folderPath ? normalizeFolderPath(folderPath) : '';
|
|
128
|
+
if (normalized) {
|
|
129
|
+
return join(this.repoPath, normalized, `${entityId}.md`);
|
|
130
|
+
}
|
|
131
|
+
return join(this.repoPath, `${entityId}.md`);
|
|
132
|
+
}
|
|
133
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
134
|
+
// Recursive file walk + (type, id) → path index
|
|
135
|
+
//
|
|
136
|
+
// With nested folders, an entry can live at any depth under the repo root.
|
|
137
|
+
// We can't compute its path from (type, id) alone — we have to find it.
|
|
138
|
+
// The index caches that lookup; mutations invalidate it via invalidateIndex().
|
|
139
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
140
|
+
fileIndex;
|
|
141
|
+
/** Drop the cached file index so the next read rebuilds it. */
|
|
142
|
+
invalidateIndex() {
|
|
143
|
+
this.fileIndex = undefined;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Walk every .md file under this repo, calling `visit` with the absolute
|
|
147
|
+
* path. Skips hidden directories (.git, .studiograph, etc.) and files that
|
|
148
|
+
* start with ".".
|
|
149
|
+
*/
|
|
150
|
+
walkMarkdownFiles(visit) {
|
|
151
|
+
if (!existsSync(this.repoPath))
|
|
152
|
+
return;
|
|
153
|
+
const stack = [this.repoPath];
|
|
154
|
+
while (stack.length > 0) {
|
|
155
|
+
const dir = stack.pop();
|
|
156
|
+
let entries;
|
|
157
|
+
try {
|
|
158
|
+
entries = readdirSync(dir);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
for (const entry of entries) {
|
|
164
|
+
if (entry.startsWith('.'))
|
|
165
|
+
continue;
|
|
166
|
+
if (WALK_IGNORE.has(entry))
|
|
167
|
+
continue;
|
|
168
|
+
const full = join(dir, entry);
|
|
169
|
+
let st;
|
|
170
|
+
try {
|
|
171
|
+
st = statSync(full);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (st.isDirectory()) {
|
|
177
|
+
stack.push(full);
|
|
178
|
+
}
|
|
179
|
+
else if (st.isFile() && entry.endsWith('.md')) {
|
|
180
|
+
visit(full);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Build (or fetch the cached) "type/id" → absolute path index.
|
|
187
|
+
* Type comes from frontmatter `entity_type`; if missing, falls back to the
|
|
188
|
+
* top-level plural-type directory name (legacy layout). Files without either
|
|
189
|
+
* (e.g. README.md) are skipped — they aren't entities.
|
|
61
190
|
*/
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
191
|
+
getFileIndex() {
|
|
192
|
+
if (this.fileIndex)
|
|
193
|
+
return this.fileIndex;
|
|
194
|
+
const index = new Map();
|
|
195
|
+
this.walkMarkdownFiles((filePath) => {
|
|
196
|
+
try {
|
|
197
|
+
const doc = this.markdownService.parseFile(filePath);
|
|
198
|
+
const fmType = doc.frontmatter.entity_type;
|
|
199
|
+
const inferred = this.inferLegacyType(filePath);
|
|
200
|
+
const type = fmType || inferred;
|
|
201
|
+
if (!type)
|
|
202
|
+
return; // README, CONTRIBUTING, etc.
|
|
203
|
+
const id = filePath.split('/').pop().replace(/\.md$/, '');
|
|
204
|
+
index.set(`${type}/${id}`, filePath);
|
|
205
|
+
}
|
|
206
|
+
catch { /* skip unparsable */ }
|
|
207
|
+
});
|
|
208
|
+
this.fileIndex = index;
|
|
209
|
+
return index;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Infer entity type from a file's location for legacy layouts where the
|
|
213
|
+
* file sits directly inside a top-level plural-type directory
|
|
214
|
+
* ({repo}/meetings/foo.md). Returns undefined for any other layout.
|
|
215
|
+
*/
|
|
216
|
+
inferLegacyType(absPath) {
|
|
217
|
+
const rel = relative(this.repoPath, absPath);
|
|
218
|
+
const parts = rel.split('/');
|
|
219
|
+
if (parts.length !== 2)
|
|
220
|
+
return undefined; // not at depth 1
|
|
221
|
+
return PLURAL_TO_SINGULAR[parts[0]];
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Resolve an entity's absolute path by (type, id). Returns undefined if not
|
|
225
|
+
* found. Uses the file index — the index keys by `${entityType}/${entityId}`
|
|
226
|
+
* which is type-aware, unlike the on-disk filename. (We previously had a
|
|
227
|
+
* legacy {plural-type}/{id}.md fast path here; once the create path moved
|
|
228
|
+
* to flat repo-root layout, that fast path could return a different
|
|
229
|
+
* type's file at the same id.)
|
|
230
|
+
*/
|
|
231
|
+
findEntityFile(entityType, entityId) {
|
|
232
|
+
return this.getFileIndex().get(`${entityType}/${entityId}`);
|
|
65
233
|
}
|
|
66
234
|
/**
|
|
67
235
|
* Validate entity data against schema
|
|
@@ -111,7 +279,7 @@ export class BaseGraphManager {
|
|
|
111
279
|
}
|
|
112
280
|
catch (error) {
|
|
113
281
|
if (error instanceof z.ZodError) {
|
|
114
|
-
const messages = error.
|
|
282
|
+
const messages = error.issues.map(e => `${e.path.join('.')}: ${e.message}`);
|
|
115
283
|
throw new Error(`Validation failed for ${entityType}:\n${messages.join('\n')}`);
|
|
116
284
|
}
|
|
117
285
|
throw error;
|
|
@@ -121,12 +289,18 @@ export class BaseGraphManager {
|
|
|
121
289
|
* Create a new entity
|
|
122
290
|
*/
|
|
123
291
|
async create(options) {
|
|
124
|
-
const { entityType, entityId, data, content = '', commitMessage, skipCommit, user } = options;
|
|
292
|
+
const { entityType, entityId, data, content = '', commitMessage, skipCommit, folderPath, user } = options;
|
|
293
|
+
const normalizedFolder = folderPath ? validateFolderPath(folderPath) : '';
|
|
125
294
|
// Prepopulate date from created_at if not provided
|
|
126
295
|
if (!data.date && data.created_at) {
|
|
127
296
|
data.date = String(data.created_at).split('T')[0];
|
|
128
297
|
}
|
|
129
|
-
// Add missing schema fields with empty defaults so the frontmatter is complete
|
|
298
|
+
// Add missing schema fields with empty defaults so the frontmatter is complete.
|
|
299
|
+
// Only string and array fields get populated — empty string isn't a valid
|
|
300
|
+
// member of number/boolean/enum/url types, and seeding them with '' caused
|
|
301
|
+
// optional numeric fields like `deal_value` to fail validation as
|
|
302
|
+
// "Expected number, received string". Anything that can't legitimately
|
|
303
|
+
// hold '' is left absent so the optional check passes.
|
|
130
304
|
const schema = ENTITY_SCHEMAS[entityType];
|
|
131
305
|
if (schema) {
|
|
132
306
|
const shape = schema.shape;
|
|
@@ -139,28 +313,44 @@ export class BaseGraphManager {
|
|
|
139
313
|
// Unwrap ZodOptional/ZodDefault to get the inner type
|
|
140
314
|
const inner = field instanceof z.ZodOptional ? field._def.innerType
|
|
141
315
|
: field instanceof z.ZodDefault ? field._def.innerType : field;
|
|
142
|
-
if (inner instanceof z.
|
|
143
|
-
|
|
316
|
+
if (inner instanceof z.ZodArray) {
|
|
317
|
+
data[key] = [];
|
|
144
318
|
continue;
|
|
145
319
|
}
|
|
146
|
-
|
|
147
|
-
|
|
320
|
+
if (inner instanceof z.ZodString) {
|
|
321
|
+
// Skip URL-typed strings — empty string fails URL validation.
|
|
322
|
+
if (inner._def.checks?.some((c) => c.def?.format === 'url'))
|
|
323
|
+
continue;
|
|
324
|
+
data[key] = '';
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
// Numbers, booleans, enums, literals, unions: leave absent.
|
|
148
328
|
}
|
|
149
329
|
}
|
|
150
330
|
}
|
|
151
331
|
// Validate data
|
|
152
332
|
const validatedData = this.validateEntity(entityType, data);
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
333
|
+
// Reject if (type, id) is already taken anywhere in the repo — IDs are
|
|
334
|
+
// unique within (repo, type) regardless of folder.
|
|
335
|
+
const existingPath = this.findEntityFile(entityType, entityId);
|
|
336
|
+
if (existingPath) {
|
|
156
337
|
throw new Error(`Entity already exists: ${entityType}/${entityId}`);
|
|
157
338
|
}
|
|
339
|
+
// Compute target path. With folderPath set, entries land at
|
|
340
|
+
// {repo}/{folderPath}/{id}.md (parents auto-created); without, fall back
|
|
341
|
+
// to legacy {plural-type}/{id}.md.
|
|
342
|
+
const entityPath = this.getEntityPath(entityType, entityId, normalizedFolder);
|
|
343
|
+
mkdirSync(dirname(entityPath), { recursive: true });
|
|
158
344
|
// Write markdown file
|
|
159
345
|
this.markdownService.writeFile(entityPath, {
|
|
160
346
|
frontmatter: validatedData,
|
|
161
347
|
content,
|
|
162
348
|
wikilinks: this.markdownService.extractWikilinks(content, validatedData),
|
|
163
349
|
});
|
|
350
|
+
// The folder now has an .md child, so any pre-existing .gitkeep is
|
|
351
|
+
// redundant. Drop it as part of the same commit so the repo stays tidy.
|
|
352
|
+
const removedMarker = this.removeFolderMarker(dirname(entityPath));
|
|
353
|
+
this.invalidateIndex();
|
|
164
354
|
if (this.gitService && !skipCommit) {
|
|
165
355
|
const transaction = this.gitService.createTransaction();
|
|
166
356
|
transaction.add({
|
|
@@ -169,21 +359,42 @@ export class BaseGraphManager {
|
|
|
169
359
|
entityId,
|
|
170
360
|
filePath: entityPath,
|
|
171
361
|
});
|
|
362
|
+
if (removedMarker) {
|
|
363
|
+
transaction.add({
|
|
364
|
+
operation: 'delete',
|
|
365
|
+
entityType,
|
|
366
|
+
entityId,
|
|
367
|
+
filePath: removedMarker,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
172
370
|
const message = commitMessage || `Create ${entityType}: ${entityId}`;
|
|
173
371
|
await transaction.commit(user ?? this.gitUser, message);
|
|
174
372
|
}
|
|
175
373
|
// Return created entity
|
|
176
374
|
const created = this.get(entityType, entityId);
|
|
177
|
-
// Update vector index (fire-and-forget, non-fatal)
|
|
178
|
-
|
|
375
|
+
// Update vector index (fire-and-forget, non-fatal). Failures are
|
|
376
|
+
// tracked on VectorService for /api/health visibility — the audit
|
|
377
|
+
// (2026-05) flagged that silent .catch(console.warn) made a steady
|
|
378
|
+
// index-failure leak undetectable until search results went stale.
|
|
379
|
+
if (this.vectorService) {
|
|
380
|
+
const v = this.vectorService;
|
|
381
|
+
v.beginOp();
|
|
382
|
+
v.upsert(this.repoName, created)
|
|
383
|
+
.then(() => v.recordSuccess())
|
|
384
|
+
.catch(err => {
|
|
385
|
+
v.recordFailure('upsert', `${entityType}/${entityId}`, err);
|
|
386
|
+
console.warn(`[vector] index failed for ${entityType}/${entityId}:`, err.message);
|
|
387
|
+
});
|
|
388
|
+
}
|
|
179
389
|
return created;
|
|
180
390
|
}
|
|
181
391
|
/**
|
|
182
|
-
* Get an entity by type and ID
|
|
392
|
+
* Get an entity by type and ID. Searches nested folders if the entry isn't
|
|
393
|
+
* at the legacy {plural-type}/{id}.md path.
|
|
183
394
|
*/
|
|
184
395
|
get(entityType, entityId) {
|
|
185
|
-
const entityPath = this.
|
|
186
|
-
if (!
|
|
396
|
+
const entityPath = this.findEntityFile(entityType, entityId);
|
|
397
|
+
if (!entityPath) {
|
|
187
398
|
throw new Error(`Entity not found: ${entityType}/${entityId}`);
|
|
188
399
|
}
|
|
189
400
|
const document = this.markdownService.parseFile(entityPath);
|
|
@@ -205,53 +416,62 @@ export class BaseGraphManager {
|
|
|
205
416
|
return {
|
|
206
417
|
id: entityId,
|
|
207
418
|
path: entityPath,
|
|
419
|
+
folder_path: folderPathFromFile(entityPath, this.repoPath),
|
|
208
420
|
entityType,
|
|
209
421
|
data: validatedData,
|
|
210
422
|
document,
|
|
211
423
|
};
|
|
212
424
|
}
|
|
213
425
|
/**
|
|
214
|
-
* Get an entity by ID only (type resolved from
|
|
426
|
+
* Get an entity by ID only (type resolved from the filesystem).
|
|
427
|
+
*
|
|
428
|
+
* Walks the file index to find any entry with this id, regardless of folder
|
|
429
|
+
* depth or type. Used by the agent's `get_entity` tool when the caller
|
|
430
|
+
* doesn't know the entity's type.
|
|
215
431
|
*/
|
|
216
432
|
getById(entityId) {
|
|
217
|
-
|
|
218
|
-
if (!existsSync(entityPath)) {
|
|
433
|
+
if (!existsSync(this.repoPath)) {
|
|
219
434
|
throw new Error(`Entity not found: ${entityId}`);
|
|
220
435
|
}
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
436
|
+
for (const [key] of this.getFileIndex()) {
|
|
437
|
+
const slash = key.indexOf('/');
|
|
438
|
+
if (slash === -1)
|
|
439
|
+
continue;
|
|
440
|
+
const id = key.slice(slash + 1);
|
|
441
|
+
if (id === entityId) {
|
|
442
|
+
const type = key.slice(0, slash);
|
|
443
|
+
return this.get(type, entityId);
|
|
444
|
+
}
|
|
229
445
|
}
|
|
230
|
-
|
|
231
|
-
id: entityId,
|
|
232
|
-
path: entityPath,
|
|
233
|
-
entityType,
|
|
234
|
-
data: validatedData,
|
|
235
|
-
document,
|
|
236
|
-
};
|
|
446
|
+
throw new Error(`Entity not found: ${entityId}`);
|
|
237
447
|
}
|
|
238
448
|
/**
|
|
239
449
|
* Update an existing entity
|
|
240
450
|
*/
|
|
241
451
|
async update(options) {
|
|
242
|
-
const { entityType, entityId, data, content, commitMessage, skipCommit, user } = options;
|
|
452
|
+
const { entityType, entityId, data, content, commitMessage, skipCommit, user, skipYjsSync } = options;
|
|
243
453
|
// Get existing entity
|
|
244
454
|
const existing = this.get(entityType, entityId);
|
|
245
|
-
// Merge data (if provided) and
|
|
455
|
+
// Merge data (if provided) and always stamp update metadata. Even
|
|
456
|
+
// content-only edits (editor autosave) must update `updated_by` /
|
|
457
|
+
// `updated_at` — otherwise the audit trail freezes at whatever the
|
|
458
|
+
// last data-change user was, which for scaffolded files is
|
|
459
|
+
// "studiograph".
|
|
460
|
+
const updater = (user ?? this.gitUser).id;
|
|
461
|
+
const now = new Date().toISOString();
|
|
246
462
|
const mergedData = data
|
|
247
463
|
? {
|
|
248
464
|
...existing.data,
|
|
249
465
|
...data,
|
|
250
466
|
entity_type: entityType,
|
|
251
|
-
updated_at:
|
|
252
|
-
updated_by:
|
|
467
|
+
updated_at: now,
|
|
468
|
+
updated_by: updater,
|
|
253
469
|
}
|
|
254
|
-
:
|
|
470
|
+
: {
|
|
471
|
+
...existing.data,
|
|
472
|
+
updated_at: now,
|
|
473
|
+
updated_by: updater,
|
|
474
|
+
};
|
|
255
475
|
// Validate merged data — use lenient validation when no new data is provided,
|
|
256
476
|
// since the existing entity may have been stored without all required fields.
|
|
257
477
|
// Raw fallback coerces Date objects to YYYY-MM-DD so gray-matter doesn't
|
|
@@ -281,13 +501,30 @@ export class BaseGraphManager {
|
|
|
281
501
|
}
|
|
282
502
|
// Merge content (if provided)
|
|
283
503
|
const mergedContent = content !== undefined ? content : existing.document.content;
|
|
284
|
-
//
|
|
285
|
-
|
|
504
|
+
// Update in place — write to wherever the entry already lives, not
|
|
505
|
+
// wherever a fresh getEntityPath() computation would put it. Otherwise
|
|
506
|
+
// an entry in a nested folder gets duplicated to the legacy plural-type
|
|
507
|
+
// dir on first update.
|
|
508
|
+
const entityPath = existing.path;
|
|
286
509
|
this.markdownService.writeFile(entityPath, {
|
|
287
510
|
frontmatter: validatedData,
|
|
288
511
|
content: mergedContent,
|
|
289
512
|
wikilinks: this.markdownService.extractWikilinks(mergedContent, validatedData),
|
|
290
513
|
});
|
|
514
|
+
// Fire onContentUpdate SYNCHRONOUSLY before the git commit's await yield.
|
|
515
|
+
// This guarantees the Y.Doc is reconciled with the new .md content before
|
|
516
|
+
// any concurrent autosave PUT can land and overwrite it. Without the
|
|
517
|
+
// strict ordering, an autosave that arrived during the git commit would
|
|
518
|
+
// write stale Y.Text content over the just-written .md, silently losing
|
|
519
|
+
// server-bypass writes (MCP, agent, event processor).
|
|
520
|
+
if (!skipYjsSync && this.mutationCallbacks?.onContentUpdate) {
|
|
521
|
+
try {
|
|
522
|
+
this.mutationCallbacks.onContentUpdate(this.repoName, entityType, entityId);
|
|
523
|
+
}
|
|
524
|
+
catch (err) {
|
|
525
|
+
console.warn(`[graph] onContentUpdate callback failed for ${entityType}/${entityId}:`, err);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
291
528
|
if (this.gitService && !skipCommit) {
|
|
292
529
|
const transaction = this.gitService.createTransaction();
|
|
293
530
|
transaction.add({
|
|
@@ -301,19 +538,150 @@ export class BaseGraphManager {
|
|
|
301
538
|
}
|
|
302
539
|
// Return updated entity
|
|
303
540
|
const updated = this.get(entityType, entityId);
|
|
304
|
-
// Update vector index (fire-and-forget, non-fatal)
|
|
305
|
-
|
|
541
|
+
// Update vector index (fire-and-forget, non-fatal). Tracked on the
|
|
542
|
+
// service so /api/health surfaces a steady failure stream instead of
|
|
543
|
+
// burying it in stdout warnings.
|
|
544
|
+
if (this.vectorService) {
|
|
545
|
+
const v = this.vectorService;
|
|
546
|
+
v.beginOp();
|
|
547
|
+
v.upsert(this.repoName, updated)
|
|
548
|
+
.then(() => v.recordSuccess())
|
|
549
|
+
.catch(err => {
|
|
550
|
+
v.recordFailure('upsert', `${entityType}/${entityId}`, err);
|
|
551
|
+
console.warn(`[vector] index failed for ${entityType}/${entityId}:`, err.message);
|
|
552
|
+
});
|
|
553
|
+
}
|
|
306
554
|
return updated;
|
|
307
555
|
}
|
|
308
556
|
/**
|
|
309
557
|
* Delete an entity
|
|
310
558
|
*/
|
|
559
|
+
/**
|
|
560
|
+
* Delete multiple entities in a single git transaction. Each entity's
|
|
561
|
+
* file is removed from disk; folder markers (.gitkeep) are added where
|
|
562
|
+
* the deletion left a folder empty. Then everything commits as one
|
|
563
|
+
* atomic operation.
|
|
564
|
+
*
|
|
565
|
+
* Compared to looping `delete()` per entity, this is a real performance
|
|
566
|
+
* primitive: one fork of git, one index update, one commit, one ref
|
|
567
|
+
* advance — instead of N. Bulk delete of a few hundred entities goes
|
|
568
|
+
* from minutes to seconds.
|
|
569
|
+
*
|
|
570
|
+
* Errors per entity (e.g. "not found") don't abort the batch — they're
|
|
571
|
+
* collected in `errors` and the rest still commit. Returns both the
|
|
572
|
+
* successfully-deleted set (for vector cleanup + WS broadcast) and the
|
|
573
|
+
* errors so the caller can surface partial failures.
|
|
574
|
+
*/
|
|
575
|
+
async bulkDelete(specs, options) {
|
|
576
|
+
const deleted = [];
|
|
577
|
+
const errors = [];
|
|
578
|
+
const transaction = this.gitService?.createTransaction();
|
|
579
|
+
for (const { entityType, entityId } of specs) {
|
|
580
|
+
try {
|
|
581
|
+
const existing = this.get(entityType, entityId);
|
|
582
|
+
rmSync(existing.path, { force: true });
|
|
583
|
+
// Folder marker if the deletion emptied the folder — same lifecycle
|
|
584
|
+
// semantics as single delete().
|
|
585
|
+
const addedMarker = this.ensureFolderMarker(dirname(existing.path));
|
|
586
|
+
if (transaction) {
|
|
587
|
+
transaction.add({ operation: 'delete', entityType, entityId, filePath: existing.path });
|
|
588
|
+
if (addedMarker) {
|
|
589
|
+
transaction.add({ operation: 'create', entityType, entityId, filePath: addedMarker });
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
deleted.push({ entityType, entityId, path: existing.path });
|
|
593
|
+
}
|
|
594
|
+
catch (err) {
|
|
595
|
+
errors.push({
|
|
596
|
+
entityType,
|
|
597
|
+
entityId,
|
|
598
|
+
error: err instanceof Error ? err.message : String(err),
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
this.invalidateIndex();
|
|
603
|
+
if (transaction && deleted.length > 0) {
|
|
604
|
+
const message = options?.commitMessage
|
|
605
|
+
?? `Delete ${deleted.length} ${deleted.length === 1 ? 'entry' : 'entries'} from web UI`;
|
|
606
|
+
await transaction.commit(options?.user ?? this.gitUser, message);
|
|
607
|
+
}
|
|
608
|
+
// Vector index cleanup, fire-and-forget (matches single delete() behavior).
|
|
609
|
+
// Failures recorded on VectorService for /api/health visibility.
|
|
610
|
+
for (const { entityType, entityId } of deleted) {
|
|
611
|
+
if (!this.vectorService)
|
|
612
|
+
continue;
|
|
613
|
+
const v = this.vectorService;
|
|
614
|
+
v.beginOp();
|
|
615
|
+
v.delete(this.repoName, entityType, entityId)
|
|
616
|
+
.then(() => v.recordSuccess())
|
|
617
|
+
.catch(err => {
|
|
618
|
+
v.recordFailure('delete', `${entityType}/${entityId}`, err);
|
|
619
|
+
console.warn(`[vector] delete failed for ${entityType}/${entityId}:`, err.message);
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
return { deleted, errors };
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Commit a set of already-on-disk file changes as a single git
|
|
626
|
+
* operation. Used by callers (e.g. the import route) that perform many
|
|
627
|
+
* `skipCommit: true` writes and want one transaction at the end instead
|
|
628
|
+
* of per-file commits.
|
|
629
|
+
*
|
|
630
|
+
* `entries` describes what each file represents so the transaction can
|
|
631
|
+
* be staged correctly. No-op when nothing is staged or git isn't
|
|
632
|
+
* available.
|
|
633
|
+
*/
|
|
634
|
+
async commitPending(entries, options) {
|
|
635
|
+
if (!this.gitService)
|
|
636
|
+
return { committed: false };
|
|
637
|
+
if (entries.length === 0 && !options.extraPaths?.length)
|
|
638
|
+
return { committed: false };
|
|
639
|
+
const transaction = this.gitService.createTransaction();
|
|
640
|
+
for (const e of entries)
|
|
641
|
+
transaction.add(e);
|
|
642
|
+
// Auxiliary paths (folder-display sidecars from the import flow) are
|
|
643
|
+
// staged alongside the entity files so they land in the same commit.
|
|
644
|
+
// The transaction only uses `filePath` for staging — the other fields
|
|
645
|
+
// are metadata, so we fill them with sentinel values.
|
|
646
|
+
if (options.extraPaths?.length) {
|
|
647
|
+
for (const filePath of options.extraPaths) {
|
|
648
|
+
transaction.add({
|
|
649
|
+
operation: 'create',
|
|
650
|
+
entityType: '_sidecar',
|
|
651
|
+
entityId: '_sidecar',
|
|
652
|
+
filePath,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
await transaction.commit(options.user ?? this.gitUser, options.commitMessage);
|
|
657
|
+
return { committed: true };
|
|
658
|
+
}
|
|
311
659
|
async delete(entityType, entityId, commitMessage, user) {
|
|
312
660
|
// Verify entity exists
|
|
313
661
|
const existing = this.get(entityType, entityId);
|
|
314
662
|
// Delete the flat entity file
|
|
315
|
-
const { rmSync } = await import('fs');
|
|
316
663
|
rmSync(existing.path, { force: true });
|
|
664
|
+
// Asset entities own a binary blob (and optionally a PDF text sidecar).
|
|
665
|
+
// Both must be cleaned up — leaving them orphans the storage layer and
|
|
666
|
+
// poisons future entity_id reuse with a stale text dump. Fire-and-forget:
|
|
667
|
+
// the markdown is the system of record, so a stuck binary shouldn't
|
|
668
|
+
// block the delete commit. (Same approach as vector-index cleanup below.)
|
|
669
|
+
if (entityType === 'asset' && this.assetService) {
|
|
670
|
+
this.assetService.deleteAll('asset', entityId).catch(err => console.warn(`[asset] deleteAll failed for asset/${entityId}: ${err?.message ?? err}`));
|
|
671
|
+
if (this.workspaceRoot) {
|
|
672
|
+
const sidecarPath = join(this.workspaceRoot, '.studiograph', 'assets-text', 'asset', `${entityId}.txt`);
|
|
673
|
+
try {
|
|
674
|
+
rmSync(sidecarPath, { force: true });
|
|
675
|
+
}
|
|
676
|
+
catch { /* sidecar cleanup is best-effort */ }
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
// Folder lifecycle: deleting an entry never deletes its containing folder.
|
|
680
|
+
// If this was the last .md in the folder, write a .gitkeep so it stays
|
|
681
|
+
// visible to listFolders() (and therefore to the sidebar). A user who
|
|
682
|
+
// wants the folder gone calls deleteFolder() explicitly.
|
|
683
|
+
const addedMarker = this.ensureFolderMarker(dirname(existing.path));
|
|
684
|
+
this.invalidateIndex();
|
|
317
685
|
if (this.gitService) {
|
|
318
686
|
const transaction = this.gitService.createTransaction();
|
|
319
687
|
transaction.add({
|
|
@@ -322,11 +690,42 @@ export class BaseGraphManager {
|
|
|
322
690
|
entityId,
|
|
323
691
|
filePath: existing.path,
|
|
324
692
|
});
|
|
693
|
+
if (addedMarker) {
|
|
694
|
+
transaction.add({
|
|
695
|
+
operation: 'create',
|
|
696
|
+
entityType,
|
|
697
|
+
entityId,
|
|
698
|
+
filePath: addedMarker,
|
|
699
|
+
});
|
|
700
|
+
}
|
|
325
701
|
const message = commitMessage || `Delete ${entityType}: ${entityId}`;
|
|
326
702
|
await transaction.commit(user ?? this.gitUser, message);
|
|
327
703
|
}
|
|
328
|
-
// Remove from vector index (fire-and-forget, non-fatal)
|
|
329
|
-
|
|
704
|
+
// Remove from vector index (fire-and-forget, non-fatal). Tracked on
|
|
705
|
+
// the service so a leaking error stream is visible at /api/health.
|
|
706
|
+
if (this.vectorService) {
|
|
707
|
+
const v = this.vectorService;
|
|
708
|
+
v.beginOp();
|
|
709
|
+
v.delete(this.repoName, entityType, entityId)
|
|
710
|
+
.then(() => v.recordSuccess())
|
|
711
|
+
.catch(err => {
|
|
712
|
+
v.recordFailure('delete', `${entityType}/${entityId}`, err);
|
|
713
|
+
console.warn(`[vector] delete failed for ${entityType}/${entityId}:`, err.message);
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
// Notify Yjs persistence. Awaited (not fire-and-forget) because docName
|
|
717
|
+
// reuse is a real scenario: delete X then create a new entity at X. If the
|
|
718
|
+
// delete callback fired async, it could race the new entity's snapshot and
|
|
719
|
+
// unlink it after creation. Failures are logged but don't throw — orphaned
|
|
720
|
+
// snapshot files are a leak, not a correctness issue.
|
|
721
|
+
if (this.mutationCallbacks?.onDelete) {
|
|
722
|
+
try {
|
|
723
|
+
await this.mutationCallbacks.onDelete(this.repoName, entityType, entityId);
|
|
724
|
+
}
|
|
725
|
+
catch (err) {
|
|
726
|
+
console.warn(`[graph] onDelete callback failed for ${entityType}/${entityId}:`, err);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
330
729
|
}
|
|
331
730
|
/**
|
|
332
731
|
* Rename an entity (change its ID). Preserves all data, content, and source_ref.
|
|
@@ -334,7 +733,8 @@ export class BaseGraphManager {
|
|
|
334
733
|
*/
|
|
335
734
|
async rename(entityType, oldId, newId, commitMessage, user) {
|
|
336
735
|
const existing = this.get(entityType, oldId);
|
|
337
|
-
|
|
736
|
+
// Rename keeps the entry in its current folder — only the filename changes.
|
|
737
|
+
const newPath = join(dirname(existing.path), `${newId}.md`);
|
|
338
738
|
if (existsSync(newPath)) {
|
|
339
739
|
throw new Error(`Entity ${entityType}/${newId} already exists`);
|
|
340
740
|
}
|
|
@@ -348,27 +748,61 @@ export class BaseGraphManager {
|
|
|
348
748
|
// Remove old file
|
|
349
749
|
const { rmSync } = await import('fs');
|
|
350
750
|
rmSync(existing.path, { force: true });
|
|
351
|
-
|
|
751
|
+
this.invalidateIndex();
|
|
352
752
|
if (this.gitService) {
|
|
353
|
-
const { spawnSync } = await import('child_process');
|
|
354
|
-
spawnSync('git', ['add', newPath], { cwd: this.repoPath, stdio: 'ignore' });
|
|
355
|
-
spawnSync('git', ['add', existing.path], { cwd: this.repoPath, stdio: 'ignore' });
|
|
356
|
-
const transaction = this.gitService.createTransaction();
|
|
357
753
|
const message = commitMessage || `Rename ${entityType}: ${oldId} → ${newId}`;
|
|
358
|
-
|
|
754
|
+
this.gitService.commitFiles([existing.path, newPath], message, user ?? this.gitUser);
|
|
755
|
+
}
|
|
756
|
+
// Update vector index — failure tracked on the service so a steady
|
|
757
|
+
// leak surfaces at /api/health rather than getting swallowed entirely.
|
|
758
|
+
if (this.vectorService) {
|
|
759
|
+
const v = this.vectorService;
|
|
760
|
+
v.beginOp();
|
|
761
|
+
v.delete(this.repoName, entityType, oldId)
|
|
762
|
+
.then(() => v.recordSuccess())
|
|
763
|
+
.catch(err => v.recordFailure('delete', `${entityType}/${oldId}`, err));
|
|
764
|
+
}
|
|
765
|
+
// Notify Yjs persistence to rename the snapshot file alongside. Awaited
|
|
766
|
+
// because rename oldId→newId followed by creating a new entity at oldId
|
|
767
|
+
// would otherwise let the async rename clobber the new oldId snapshot.
|
|
768
|
+
if (this.mutationCallbacks?.onRename) {
|
|
769
|
+
try {
|
|
770
|
+
await this.mutationCallbacks.onRename(this.repoName, entityType, oldId, newId);
|
|
771
|
+
}
|
|
772
|
+
catch (err) {
|
|
773
|
+
console.warn(`[graph] onRename callback failed for ${entityType}/${oldId}→${newId}:`, err);
|
|
774
|
+
}
|
|
359
775
|
}
|
|
360
|
-
// Update vector index
|
|
361
|
-
this.vectorService?.delete(this.repoName, entityType, oldId).catch(() => { });
|
|
362
776
|
return this.get(entityType, newId);
|
|
363
777
|
}
|
|
364
778
|
/**
|
|
365
|
-
* Change an entity's type.
|
|
366
|
-
*
|
|
779
|
+
* Change an entity's type. Updates frontmatter only — the file stays in
|
|
780
|
+
* its current folder. In the type-agnostic folder model, type is a
|
|
781
|
+
* frontmatter property independent of location. (Legacy behavior moved
|
|
782
|
+
* the file between {plural-type}/ dirs; that's no longer correct because
|
|
783
|
+
* those dirs are now ordinary user-visible folders, not type markers.)
|
|
784
|
+
*
|
|
785
|
+
* If the user wants the file to also move (e.g. out of a "meetings/"
|
|
786
|
+
* folder when changed to a note), they invoke move() separately.
|
|
367
787
|
*/
|
|
368
788
|
async changeType(entityId, fromType, toType, commitMessage, user) {
|
|
789
|
+
// Asset entities are bound to a binary in the storage backend plus
|
|
790
|
+
// (for PDFs) a text sidecar. Changing TO asset would leave the entity
|
|
791
|
+
// without an asset_url / asset_kind / blob; changing FROM asset would
|
|
792
|
+
// orphan the binary and sidecar. Both directions must be a
|
|
793
|
+
// delete-and-recreate, not a type swap.
|
|
794
|
+
if (toType === 'asset' || fromType === 'asset') {
|
|
795
|
+
const direction = toType === 'asset' ? 'to' : 'from';
|
|
796
|
+
throw new Error(`Cannot change_type ${direction} 'asset' — assets are bound to a binary in storage. ` +
|
|
797
|
+
(toType === 'asset'
|
|
798
|
+
? 'Delete this entry and re-upload the file via the asset upload route to convert it to an asset.'
|
|
799
|
+
: 'Delete this asset entry (which removes the binary) and create a fresh entry of the target type.'));
|
|
800
|
+
}
|
|
369
801
|
const existing = this.get(fromType, entityId);
|
|
370
|
-
|
|
371
|
-
|
|
802
|
+
// (type, id) uniqueness check — if an entry with the new type already
|
|
803
|
+
// exists with this id, refuse rather than silently colliding in the index.
|
|
804
|
+
const collision = this.findEntityFile(toType, entityId);
|
|
805
|
+
if (collision && collision !== existing.path) {
|
|
372
806
|
throw new Error(`Entity ${toType}/${entityId} already exists`);
|
|
373
807
|
}
|
|
374
808
|
// Read current content
|
|
@@ -408,40 +842,330 @@ export class BaseGraphManager {
|
|
|
408
842
|
}
|
|
409
843
|
}
|
|
410
844
|
}
|
|
411
|
-
//
|
|
412
|
-
|
|
413
|
-
this.
|
|
414
|
-
// Remove old file
|
|
415
|
-
const { rmSync } = await import('fs');
|
|
416
|
-
rmSync(existing.path, { force: true });
|
|
417
|
-
// Git: stage the move and commit directly
|
|
845
|
+
// Write back to the same path — file stays in its current folder.
|
|
846
|
+
this.markdownService.writeFile(existing.path, doc);
|
|
847
|
+
this.invalidateIndex();
|
|
418
848
|
if (this.gitService) {
|
|
419
|
-
const { spawnSync } = await import('child_process');
|
|
420
|
-
const gitUser = user ?? this.gitUser;
|
|
421
849
|
const message = commitMessage || `Change type: ${entityId} (${fromType} → ${toType})`;
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
'commit', '-m', message,
|
|
428
|
-
], { cwd: this.repoPath, stdio: 'ignore' });
|
|
429
|
-
}
|
|
430
|
-
// Update vector index: remove old entry, re-index at new type
|
|
850
|
+
this.gitService.commitFiles([existing.path], message, user ?? this.gitUser);
|
|
851
|
+
}
|
|
852
|
+
// Update vector index: remove old entry, re-index at new type. Both ops
|
|
853
|
+
// tracked on the service so /api/health surfaces a leaking failure
|
|
854
|
+
// stream — previous .catch(() => {}) form was completely silent.
|
|
431
855
|
const updated = this.get(toType, entityId);
|
|
432
|
-
this.vectorService
|
|
433
|
-
|
|
856
|
+
if (this.vectorService) {
|
|
857
|
+
const v = this.vectorService;
|
|
858
|
+
v.beginOp();
|
|
859
|
+
v.delete(this.repoName, fromType, entityId)
|
|
860
|
+
.then(() => v.recordSuccess())
|
|
861
|
+
.catch(err => v.recordFailure('delete', `${fromType}/${entityId}`, err));
|
|
862
|
+
v.beginOp();
|
|
863
|
+
v.upsert(this.repoName, updated)
|
|
864
|
+
.then(() => v.recordSuccess())
|
|
865
|
+
.catch(err => v.recordFailure('upsert', `${toType}/${entityId}`, err));
|
|
866
|
+
}
|
|
867
|
+
// Notify Yjs persistence to move the snapshot from the old type's path to
|
|
868
|
+
// the new one (the docName changes, so the snapshot path changes). Awaited
|
|
869
|
+
// for the same reason as rename — preserves ordering when types are
|
|
870
|
+
// changed back and forth quickly.
|
|
871
|
+
if (this.mutationCallbacks?.onChangeType) {
|
|
872
|
+
try {
|
|
873
|
+
await this.mutationCallbacks.onChangeType(this.repoName, entityId, fromType, toType);
|
|
874
|
+
}
|
|
875
|
+
catch (err) {
|
|
876
|
+
console.warn(`[graph] onChangeType callback failed for ${entityId} (${fromType}→${toType}):`, err);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
434
879
|
return updated;
|
|
435
880
|
}
|
|
436
881
|
/**
|
|
437
|
-
*
|
|
882
|
+
* Move an entry to a different folder within the same repo. The filename
|
|
883
|
+
* (and therefore the entity id) and type are unchanged. Auto-creates parent
|
|
884
|
+
* folders. Refuses if the target path already has an entry with this id.
|
|
885
|
+
*/
|
|
886
|
+
async move(entityType, entityId, targetFolderPath, commitMessage, user) {
|
|
887
|
+
const existing = this.get(entityType, entityId);
|
|
888
|
+
const normalized = normalizeFolderPath(targetFolderPath);
|
|
889
|
+
const newPath = join(this.repoPath, normalized, `${entityId}.md`);
|
|
890
|
+
if (newPath === existing.path)
|
|
891
|
+
return existing; // no-op
|
|
892
|
+
if (existsSync(newPath)) {
|
|
893
|
+
throw new Error(`Path already occupied: ${normalized}/${entityId}.md`);
|
|
894
|
+
}
|
|
895
|
+
mkdirSync(dirname(newPath), { recursive: true });
|
|
896
|
+
const { renameSync } = await import('fs');
|
|
897
|
+
renameSync(existing.path, newPath);
|
|
898
|
+
// Folder lifecycle: source stays addressable via .gitkeep if it lost its
|
|
899
|
+
// last .md; destination drops any redundant .gitkeep now that it has one.
|
|
900
|
+
const filesToCommit = [existing.path, newPath];
|
|
901
|
+
const addedSourceMarker = this.ensureFolderMarker(dirname(existing.path));
|
|
902
|
+
if (addedSourceMarker)
|
|
903
|
+
filesToCommit.push(addedSourceMarker);
|
|
904
|
+
const removedDestMarker = this.removeFolderMarker(dirname(newPath));
|
|
905
|
+
if (removedDestMarker)
|
|
906
|
+
filesToCommit.push(removedDestMarker);
|
|
907
|
+
this.invalidateIndex();
|
|
908
|
+
if (this.gitService) {
|
|
909
|
+
const message = commitMessage || `Move ${entityType}: ${entityId} → ${normalized || '(root)'}`;
|
|
910
|
+
this.gitService.commitFiles(filesToCommit, message, user ?? this.gitUser);
|
|
911
|
+
}
|
|
912
|
+
return this.get(entityType, entityId);
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Restore an entity to its content at a specific commit. Single-entity
|
|
916
|
+
* scope only — multi-entity rollback (e.g. "undo this commit's effects
|
|
917
|
+
* across all touched files") is a separate, more cautious flow that
|
|
918
|
+
* isn't built yet.
|
|
919
|
+
*
|
|
920
|
+
* Two paths through the method:
|
|
921
|
+
* - **Overwrite**: the entity exists today at path `P`. Read content
|
|
922
|
+
* from `git show <hash>:<P>` and write it back to `P`.
|
|
923
|
+
* - **Recreate**: the entity is gone today. Walk the historical
|
|
924
|
+
* commit's tree to find the .md file whose basename matches
|
|
925
|
+
* `entityId` AND whose frontmatter `entity_type` matches
|
|
926
|
+
* `entityType`. That path is the restore target. The walk only
|
|
927
|
+
* fires when the entity is truly absent — overwrites stay cheap.
|
|
928
|
+
*
|
|
929
|
+
* Throws a `PathConflictError` if the recreate target path is now
|
|
930
|
+
* occupied by a DIFFERENT entity (different type+id) — recreating
|
|
931
|
+
* over it would silently overwrite unrelated work. Caller surfaces
|
|
932
|
+
* the conflict to the user; resolution is "move or delete the
|
|
933
|
+
* occupant first."
|
|
934
|
+
*/
|
|
935
|
+
async restoreFile(entityType, entityId, commitHash, user) {
|
|
936
|
+
if (!this.gitService) {
|
|
937
|
+
throw new Error('Cannot restore — git service is not available for this repo');
|
|
938
|
+
}
|
|
939
|
+
const existingPath = this.findEntityFile(entityType, entityId);
|
|
940
|
+
let targetPath;
|
|
941
|
+
let action;
|
|
942
|
+
let content;
|
|
943
|
+
if (existingPath) {
|
|
944
|
+
// Overwrite path. Read content at the historical commit using the
|
|
945
|
+
// CURRENT path (git tracks files by content + path; if the entity
|
|
946
|
+
// was renamed or moved across the file's lifetime, --follow would
|
|
947
|
+
// be needed — out of scope for v1, the user can resolve by
|
|
948
|
+
// restoring at a commit where the path matched).
|
|
949
|
+
const relPath = relative(this.repoPath, existingPath);
|
|
950
|
+
content = this.gitService.getFileAtCommit(relPath, commitHash);
|
|
951
|
+
if (content === null) {
|
|
952
|
+
throw new Error(`File ${relPath} did not exist at commit ${commitHash}`);
|
|
953
|
+
}
|
|
954
|
+
targetPath = existingPath;
|
|
955
|
+
action = 'overwrite';
|
|
956
|
+
}
|
|
957
|
+
else {
|
|
958
|
+
// Recreate path. Walk the historical commit's tree to find the
|
|
959
|
+
// file whose basename matches entityId and frontmatter type
|
|
960
|
+
// matches entityType. This is the only place we do a full
|
|
961
|
+
// tree-walk — it only runs when the entity is gone today.
|
|
962
|
+
const found = this.findEntityInCommit(entityType, entityId, commitHash);
|
|
963
|
+
if (!found) {
|
|
964
|
+
throw new Error(`Entity ${entityType}/${entityId} not found at commit ${commitHash}`);
|
|
965
|
+
}
|
|
966
|
+
targetPath = join(this.repoPath, found.path);
|
|
967
|
+
content = found.content;
|
|
968
|
+
action = 'recreate';
|
|
969
|
+
// Path-conflict check: nothing else may occupy targetPath today.
|
|
970
|
+
if (existsSync(targetPath)) {
|
|
971
|
+
const occupant = this.identifyEntityAtPath(targetPath);
|
|
972
|
+
if (occupant && (occupant.entityType !== entityType || occupant.entityId !== entityId)) {
|
|
973
|
+
throw new PathConflictError(found.path, occupant);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
978
|
+
writeFileSync(targetPath, content);
|
|
979
|
+
// Folder lifecycle: if recreating dropped a file into a directory
|
|
980
|
+
// that had been emptied (and now carries .gitkeep), drop the marker
|
|
981
|
+
// so the directory exists via the .md file alone.
|
|
982
|
+
const filesToCommit = [targetPath];
|
|
983
|
+
if (action === 'recreate') {
|
|
984
|
+
const removedDestMarker = this.removeFolderMarker(dirname(targetPath));
|
|
985
|
+
if (removedDestMarker)
|
|
986
|
+
filesToCommit.push(removedDestMarker);
|
|
987
|
+
}
|
|
988
|
+
this.invalidateIndex();
|
|
989
|
+
const shortHash = commitHash.length > 8 ? commitHash.slice(0, 8) : commitHash;
|
|
990
|
+
const message = `Restore ${entityType}/${entityId} from ${shortHash}`;
|
|
991
|
+
this.gitService.commitFiles(filesToCommit, message, user ?? this.gitUser);
|
|
992
|
+
return { action, path: targetPath };
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Walk a commit's tree looking for a .md file whose basename matches
|
|
996
|
+
* `entityId` AND whose frontmatter `entity_type` matches `entityType`.
|
|
997
|
+
* Returns repo-relative path + content when found, otherwise null.
|
|
998
|
+
* Used by `restoreFile` for the recreate-from-deletion case.
|
|
999
|
+
*/
|
|
1000
|
+
findEntityInCommit(entityType, entityId, commitHash) {
|
|
1001
|
+
if (!this.gitService)
|
|
1002
|
+
return null;
|
|
1003
|
+
const filenameLower = `${entityId}.md`.toLowerCase();
|
|
1004
|
+
const tree = this.gitService.listFilesAtCommit(commitHash);
|
|
1005
|
+
for (const path of tree) {
|
|
1006
|
+
if (!path.toLowerCase().endsWith(filenameLower))
|
|
1007
|
+
continue;
|
|
1008
|
+
// basename match — read content and check frontmatter type.
|
|
1009
|
+
const content = this.gitService.getFileAtCommit(path, commitHash);
|
|
1010
|
+
if (content === null)
|
|
1011
|
+
continue;
|
|
1012
|
+
try {
|
|
1013
|
+
const doc = this.markdownService.parseContent(content);
|
|
1014
|
+
const fmType = doc.frontmatter.entity_type;
|
|
1015
|
+
const inferred = this.inferLegacyType(join(this.repoPath, path));
|
|
1016
|
+
const type = fmType || inferred;
|
|
1017
|
+
if (type === entityType)
|
|
1018
|
+
return { path, content };
|
|
1019
|
+
}
|
|
1020
|
+
catch {
|
|
1021
|
+
// unparsable file — skip
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
return null;
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Identify the entity (if any) currently occupying `absPath`. Returns
|
|
1028
|
+
* null when the path holds something that isn't an entity (README, etc).
|
|
1029
|
+
* Mirrors the inference rules used in `getFileIndex`.
|
|
1030
|
+
*/
|
|
1031
|
+
identifyEntityAtPath(absPath) {
|
|
1032
|
+
try {
|
|
1033
|
+
const doc = this.markdownService.parseFile(absPath);
|
|
1034
|
+
const fmType = doc.frontmatter.entity_type;
|
|
1035
|
+
const inferred = this.inferLegacyType(absPath);
|
|
1036
|
+
const type = fmType || inferred;
|
|
1037
|
+
if (!type)
|
|
1038
|
+
return null;
|
|
1039
|
+
const id = absPath.split('/').pop().replace(/\.md$/, '');
|
|
1040
|
+
return { entityType: type, entityId: id };
|
|
1041
|
+
}
|
|
1042
|
+
catch {
|
|
1043
|
+
return null;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Enumerate the distinct entity types present in this repo. Used by
|
|
1048
|
+
* /api/repos to let the sidebar render the Views list without a full
|
|
1049
|
+
* entity fetch per folder.
|
|
1050
|
+
*
|
|
1051
|
+
* Derived from the file index (which reads frontmatter), so it works for
|
|
1052
|
+
* entries in any folder — not just the legacy {plural-type}/ dirs.
|
|
1053
|
+
*/
|
|
1054
|
+
listEntityTypes() {
|
|
1055
|
+
if (!existsSync(this.repoPath))
|
|
1056
|
+
return [];
|
|
1057
|
+
const types = new Set();
|
|
1058
|
+
for (const key of this.getFileIndex().keys()) {
|
|
1059
|
+
const slash = key.indexOf('/');
|
|
1060
|
+
if (slash > 0)
|
|
1061
|
+
types.add(key.slice(0, slash));
|
|
1062
|
+
}
|
|
1063
|
+
return [...types].sort();
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Folder lifecycle: a directory is treated as a first-class persistent
|
|
1067
|
+
* object. It exists from the moment it's created (createFolder, the destination
|
|
1068
|
+
* of a moved entry, etc.) until it's explicitly removed via a folder-level
|
|
1069
|
+
* operation (deleteFolder, renameFolder, moveFolderToRepo). Entry-level
|
|
1070
|
+
* operations (create, delete, move, transfer) never decide whether a folder
|
|
1071
|
+
* lives or dies.
|
|
1072
|
+
*
|
|
1073
|
+
* `listFolders()` requires either a .md child or a .gitkeep marker for a
|
|
1074
|
+
* directory to surface in the API, so the two helpers below are the single
|
|
1075
|
+
* place that adds and removes the marker:
|
|
1076
|
+
*
|
|
1077
|
+
* - `ensureFolderMarker` — call after an op that may have left `dir`
|
|
1078
|
+
* without .md children. Writes `.gitkeep` and returns its path so the
|
|
1079
|
+
* caller can stage it in the same commit.
|
|
1080
|
+
* - `removeFolderMarker` — call after an op that placed an .md into `dir`.
|
|
1081
|
+
* Drops a now-redundant `.gitkeep` so the repo stays tidy.
|
|
1082
|
+
*
|
|
1083
|
+
* Both no-op for the repo root (always implicit) and for paths that don't
|
|
1084
|
+
* exist. Both return `null` when nothing changed.
|
|
1085
|
+
*/
|
|
1086
|
+
ensureFolderMarker(dir) {
|
|
1087
|
+
if (dir === this.repoPath)
|
|
1088
|
+
return null;
|
|
1089
|
+
if (!existsSync(dir))
|
|
1090
|
+
return null;
|
|
1091
|
+
const entries = readdirSync(dir);
|
|
1092
|
+
const hasMd = entries.some(e => e.endsWith('.md') && !e.startsWith('.'));
|
|
1093
|
+
if (hasMd)
|
|
1094
|
+
return null;
|
|
1095
|
+
const keepPath = join(dir, '.gitkeep');
|
|
1096
|
+
if (existsSync(keepPath))
|
|
1097
|
+
return null;
|
|
1098
|
+
writeFileSync(keepPath, '');
|
|
1099
|
+
return keepPath;
|
|
1100
|
+
}
|
|
1101
|
+
removeFolderMarker(dir) {
|
|
1102
|
+
if (dir === this.repoPath)
|
|
1103
|
+
return null;
|
|
1104
|
+
if (!existsSync(dir))
|
|
1105
|
+
return null;
|
|
1106
|
+
const keepPath = join(dir, '.gitkeep');
|
|
1107
|
+
if (!existsSync(keepPath))
|
|
1108
|
+
return null;
|
|
1109
|
+
const entries = readdirSync(dir);
|
|
1110
|
+
const hasMd = entries.some(e => e.endsWith('.md') && !e.startsWith('.'));
|
|
1111
|
+
if (!hasMd)
|
|
1112
|
+
return null; // marker still load-bearing
|
|
1113
|
+
rmSync(keepPath, { force: true });
|
|
1114
|
+
return keepPath;
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Walk up from `dir`, removing each directory that became empty after a
|
|
1118
|
+
* folder-level removal (deleteFolder, renameFolder, moveFolderToRepo).
|
|
1119
|
+
* Stops at the repo root. Silently no-ops if a directory is missing,
|
|
1120
|
+
* non-empty, or can't be removed.
|
|
1121
|
+
*
|
|
1122
|
+
* NOT called by entry-level operations (create/delete/move/transfer) —
|
|
1123
|
+
* they preserve folder structure regardless of whether they emptied a
|
|
1124
|
+
* directory. Use `ensureFolderMarker` instead for those paths.
|
|
1125
|
+
*/
|
|
1126
|
+
async pruneEmptyAncestors(dir) {
|
|
1127
|
+
const { rmSync } = await import('fs');
|
|
1128
|
+
const repoRoot = this.repoPath.replace(/\/+$/, '');
|
|
1129
|
+
let current = dir;
|
|
1130
|
+
while (current.startsWith(repoRoot + '/') && current !== repoRoot) {
|
|
1131
|
+
if (!existsSync(current)) {
|
|
1132
|
+
current = dirname(current);
|
|
1133
|
+
continue;
|
|
1134
|
+
}
|
|
1135
|
+
let entries;
|
|
1136
|
+
try {
|
|
1137
|
+
entries = readdirSync(current);
|
|
1138
|
+
}
|
|
1139
|
+
catch {
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
// Treat .gitkeep as "this folder is intentionally kept" — don't auto-prune.
|
|
1143
|
+
if (entries.length > 0)
|
|
1144
|
+
return;
|
|
1145
|
+
try {
|
|
1146
|
+
rmSync(current, { recursive: true, force: true });
|
|
1147
|
+
}
|
|
1148
|
+
catch {
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
current = dirname(current);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* List entities of a given type (or all types if not specified).
|
|
1156
|
+
*
|
|
1157
|
+
* Walks the entire repo tree so entries in nested folders are discovered.
|
|
1158
|
+
* The type filter is applied against frontmatter `entity_type`; legacy
|
|
1159
|
+
* entries without explicit frontmatter type fall back to the depth-1
|
|
1160
|
+
* plural-type directory name (so `meetings/foo.md` still resolves as a
|
|
1161
|
+
* meeting). Files with no inferable type (README.md, CONTRIBUTING.md, etc.)
|
|
1162
|
+
* are skipped.
|
|
438
1163
|
*/
|
|
439
1164
|
list(entityType, limit) {
|
|
440
1165
|
if (!existsSync(this.repoPath))
|
|
441
1166
|
return [];
|
|
442
1167
|
const results = [];
|
|
443
|
-
|
|
444
|
-
// EntityBaseSchema, then raw data so app-defined types are still discovered.
|
|
1168
|
+
const seen = new Set(); // "type/id" — dedup if a file ends up indexed twice
|
|
445
1169
|
const validateLenient = (type, data) => {
|
|
446
1170
|
try {
|
|
447
1171
|
return this.validateEntity(type, data);
|
|
@@ -455,97 +1179,36 @@ export class BaseGraphManager {
|
|
|
455
1179
|
}
|
|
456
1180
|
}
|
|
457
1181
|
};
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
const tryAddFile = (filePath, id, inferredType) => {
|
|
461
|
-
if (seenIds.has(id))
|
|
462
|
-
return;
|
|
463
|
-
try {
|
|
464
|
-
const doc = this.markdownService.parseFile(filePath);
|
|
465
|
-
const fmType = doc.frontmatter.entity_type;
|
|
466
|
-
const resolvedType = entityType ?? fmType ?? inferredType ?? this.repoName;
|
|
467
|
-
if (entityType && fmType && fmType !== entityType)
|
|
468
|
-
return;
|
|
469
|
-
const validatedData = validateLenient(resolvedType, doc.frontmatter);
|
|
470
|
-
seenIds.add(id);
|
|
471
|
-
results.push({
|
|
472
|
-
id,
|
|
473
|
-
path: filePath,
|
|
474
|
-
entityType: resolvedType,
|
|
475
|
-
data: validatedData,
|
|
476
|
-
document: doc,
|
|
477
|
-
});
|
|
478
|
-
}
|
|
479
|
-
catch { /* skip invalid */ }
|
|
480
|
-
};
|
|
481
|
-
// Scan plural type subfolders for flat .md files
|
|
482
|
-
for (const entry of readdirSync(this.repoPath)) {
|
|
483
|
-
if (entry.startsWith('.'))
|
|
484
|
-
continue;
|
|
485
|
-
const fullPath = join(this.repoPath, entry);
|
|
1182
|
+
this.walkMarkdownFiles((filePath) => {
|
|
1183
|
+
let doc;
|
|
486
1184
|
try {
|
|
487
|
-
|
|
488
|
-
continue;
|
|
1185
|
+
doc = this.markdownService.parseFile(filePath);
|
|
489
1186
|
}
|
|
490
1187
|
catch {
|
|
491
|
-
|
|
492
|
-
}
|
|
493
|
-
const singularType = PLURAL_TO_SINGULAR[entry];
|
|
494
|
-
if (!singularType)
|
|
495
|
-
continue; // skip unknown directories
|
|
496
|
-
try {
|
|
497
|
-
for (const file of readdirSync(fullPath)) {
|
|
498
|
-
if (file.startsWith('.') || !file.endsWith('.md'))
|
|
499
|
-
continue;
|
|
500
|
-
const filePath = join(fullPath, file);
|
|
501
|
-
try {
|
|
502
|
-
if (!statSync(filePath).isFile())
|
|
503
|
-
continue;
|
|
504
|
-
}
|
|
505
|
-
catch {
|
|
506
|
-
continue;
|
|
507
|
-
}
|
|
508
|
-
const id = file.replace(/\.md$/, '');
|
|
509
|
-
tryAddFile(filePath, id, singularType);
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
catch { /* skip unreadable */ }
|
|
513
|
-
}
|
|
514
|
-
// Also discover flat .md files at repo root (README, etc.)
|
|
515
|
-
for (const entry of readdirSync(this.repoPath)) {
|
|
516
|
-
if (entry.startsWith('.'))
|
|
517
|
-
continue;
|
|
518
|
-
if (!entry.endsWith('.md'))
|
|
519
|
-
continue;
|
|
520
|
-
const fullPath = join(this.repoPath, entry);
|
|
521
|
-
try {
|
|
522
|
-
if (!statSync(fullPath).isFile())
|
|
523
|
-
continue;
|
|
524
|
-
}
|
|
525
|
-
catch {
|
|
526
|
-
continue;
|
|
527
|
-
}
|
|
528
|
-
const id = entry.replace(/\.md$/, '');
|
|
529
|
-
if (seenIds.has(id))
|
|
530
|
-
continue;
|
|
531
|
-
try {
|
|
532
|
-
const doc = this.markdownService.parseFile(fullPath);
|
|
533
|
-
const fmType = doc.frontmatter.entity_type;
|
|
534
|
-
const resolvedType = entityType ?? fmType ?? this.repoName;
|
|
535
|
-
if (entityType && fmType && fmType !== entityType)
|
|
536
|
-
continue;
|
|
537
|
-
const validatedData = validateLenient(resolvedType, doc.frontmatter);
|
|
538
|
-
seenIds.add(id);
|
|
539
|
-
results.push({
|
|
540
|
-
id,
|
|
541
|
-
path: fullPath,
|
|
542
|
-
entityType: resolvedType,
|
|
543
|
-
data: validatedData,
|
|
544
|
-
document: doc,
|
|
545
|
-
});
|
|
1188
|
+
return;
|
|
546
1189
|
}
|
|
547
|
-
|
|
548
|
-
|
|
1190
|
+
const fmType = doc.frontmatter.entity_type;
|
|
1191
|
+
const inferred = this.inferLegacyType(filePath);
|
|
1192
|
+
const resolvedType = (fmType ?? inferred);
|
|
1193
|
+
if (!resolvedType)
|
|
1194
|
+
return; // README, etc.
|
|
1195
|
+
if (entityType && resolvedType !== entityType)
|
|
1196
|
+
return;
|
|
1197
|
+
const id = filePath.split('/').pop().replace(/\.md$/, '');
|
|
1198
|
+
const key = `${resolvedType}/${id}`;
|
|
1199
|
+
if (seen.has(key))
|
|
1200
|
+
return;
|
|
1201
|
+
seen.add(key);
|
|
1202
|
+
const validatedData = validateLenient(resolvedType, doc.frontmatter);
|
|
1203
|
+
results.push({
|
|
1204
|
+
id,
|
|
1205
|
+
path: filePath,
|
|
1206
|
+
folder_path: folderPathFromFile(filePath, this.repoPath),
|
|
1207
|
+
entityType: resolvedType,
|
|
1208
|
+
data: validatedData,
|
|
1209
|
+
document: doc,
|
|
1210
|
+
});
|
|
1211
|
+
});
|
|
549
1212
|
if (limit && limit > 0)
|
|
550
1213
|
return results.slice(0, limit);
|
|
551
1214
|
return results;
|
|
@@ -709,31 +1372,43 @@ export class BaseGraphManager {
|
|
|
709
1372
|
}
|
|
710
1373
|
return entities;
|
|
711
1374
|
}
|
|
1375
|
+
/**
|
|
1376
|
+
* Resolve a markdown file (anywhere under the repo) to its (type, id),
|
|
1377
|
+
* tolerating both legacy {plural-type}/{id}.md and nested-folder layouts.
|
|
1378
|
+
* Returns undefined for files without a recognizable entity type.
|
|
1379
|
+
*/
|
|
1380
|
+
resolveFileToEntity(absPath) {
|
|
1381
|
+
const filename = absPath.split('/').pop();
|
|
1382
|
+
if (!filename || !filename.endsWith('.md'))
|
|
1383
|
+
return undefined;
|
|
1384
|
+
const id = filename.replace(/\.md$/, '');
|
|
1385
|
+
let fmType;
|
|
1386
|
+
try {
|
|
1387
|
+
fmType = this.markdownService.parseFile(absPath).frontmatter.entity_type;
|
|
1388
|
+
}
|
|
1389
|
+
catch { /* unparsable — fall back to inference */ }
|
|
1390
|
+
const type = fmType || this.inferLegacyType(absPath);
|
|
1391
|
+
if (!type)
|
|
1392
|
+
return undefined;
|
|
1393
|
+
return { type: type, id };
|
|
1394
|
+
}
|
|
712
1395
|
/**
|
|
713
1396
|
* Get entities related to a given entity (via wikilinks)
|
|
714
1397
|
*/
|
|
715
1398
|
getRelated(entityType, entityId) {
|
|
716
1399
|
const entity = this.get(entityType, entityId);
|
|
717
1400
|
const related = [];
|
|
718
|
-
// Find all wikilinked entities
|
|
719
1401
|
for (const link of entity.document.wikilinks) {
|
|
720
|
-
// Try to find the linked entity in all type subfolders
|
|
721
1402
|
const found = this.markdownService.findEntity(link, [this.repoPath]);
|
|
722
|
-
if (found)
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
const doc = this.markdownService.parseFile(found);
|
|
730
|
-
const linkedType = doc.frontmatter.entity_type ?? PLURAL_TO_SINGULAR[parts[0]] ?? this.repoName;
|
|
731
|
-
const linkedEntity = this.get(linkedType, linkedId);
|
|
732
|
-
related.push(linkedEntity);
|
|
733
|
-
}
|
|
734
|
-
catch { /* skip invalid */ }
|
|
735
|
-
}
|
|
1403
|
+
if (!found)
|
|
1404
|
+
continue;
|
|
1405
|
+
const resolved = this.resolveFileToEntity(found);
|
|
1406
|
+
if (!resolved)
|
|
1407
|
+
continue;
|
|
1408
|
+
try {
|
|
1409
|
+
related.push(this.get(resolved.type, resolved.id));
|
|
736
1410
|
}
|
|
1411
|
+
catch { /* skip invalid */ }
|
|
737
1412
|
}
|
|
738
1413
|
return related;
|
|
739
1414
|
}
|
|
@@ -744,19 +1419,13 @@ export class BaseGraphManager {
|
|
|
744
1419
|
const backlinks = this.markdownService.findBacklinks(entityId, [this.repoPath]);
|
|
745
1420
|
const results = [];
|
|
746
1421
|
for (const { file } of backlinks) {
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
try {
|
|
753
|
-
const doc = this.markdownService.parseFile(file);
|
|
754
|
-
const linkedType = doc.frontmatter.entity_type ?? PLURAL_TO_SINGULAR[parts[0]] ?? this.repoName;
|
|
755
|
-
const linkedEntity = this.get(linkedType, linkedId);
|
|
756
|
-
results.push(linkedEntity);
|
|
757
|
-
}
|
|
758
|
-
catch { /* skip invalid */ }
|
|
1422
|
+
const resolved = this.resolveFileToEntity(file);
|
|
1423
|
+
if (!resolved)
|
|
1424
|
+
continue;
|
|
1425
|
+
try {
|
|
1426
|
+
results.push(this.get(resolved.type, resolved.id));
|
|
759
1427
|
}
|
|
1428
|
+
catch { /* skip invalid */ }
|
|
760
1429
|
}
|
|
761
1430
|
return results;
|
|
762
1431
|
}
|
|
@@ -771,26 +1440,20 @@ export class BaseGraphManager {
|
|
|
771
1440
|
* Refactor a wikilink (rename entity and update all references)
|
|
772
1441
|
*/
|
|
773
1442
|
async refactorWikilink(oldId, newId, commitMessage) {
|
|
774
|
-
// Find all files that reference the old entity ID
|
|
775
1443
|
const backlinks = this.markdownService.findBacklinks(oldId, [this.repoPath]);
|
|
776
|
-
// Update each file
|
|
777
1444
|
if (this.gitService) {
|
|
778
1445
|
const transaction = this.gitService.createTransaction();
|
|
779
1446
|
for (const { file } of backlinks) {
|
|
780
1447
|
this.markdownService.updateWikilinks(file, oldId, newId);
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
entityId,
|
|
791
|
-
filePath: file,
|
|
792
|
-
});
|
|
793
|
-
}
|
|
1448
|
+
const resolved = this.resolveFileToEntity(file);
|
|
1449
|
+
if (!resolved)
|
|
1450
|
+
continue;
|
|
1451
|
+
transaction.add({
|
|
1452
|
+
operation: 'update',
|
|
1453
|
+
entityType: resolved.type,
|
|
1454
|
+
entityId: resolved.id,
|
|
1455
|
+
filePath: file,
|
|
1456
|
+
});
|
|
794
1457
|
}
|
|
795
1458
|
const message = commitMessage || `Refactor wikilink: ${oldId} → ${newId}`;
|
|
796
1459
|
await transaction.commit(this.gitUser, message);
|
|
@@ -802,6 +1465,491 @@ export class BaseGraphManager {
|
|
|
802
1465
|
}
|
|
803
1466
|
return { updated: backlinks.length };
|
|
804
1467
|
}
|
|
1468
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
1469
|
+
// Folder CRUD — operate on directories within the repo, not on entries.
|
|
1470
|
+
// Sharing remains a repo-level concept; folders carry no permissions or
|
|
1471
|
+
// metadata of their own. Empty folders are kept around via .gitkeep so git
|
|
1472
|
+
// tracks them.
|
|
1473
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
1474
|
+
/**
|
|
1475
|
+
* List every non-empty folder in this repo, plus folders explicitly kept
|
|
1476
|
+
* via .gitkeep. Returns repo-relative POSIX paths, sorted, deduplicated.
|
|
1477
|
+
* The repo root ("") is intentionally omitted — it's implicit.
|
|
1478
|
+
*/
|
|
1479
|
+
listFolders() {
|
|
1480
|
+
if (!existsSync(this.repoPath))
|
|
1481
|
+
return [];
|
|
1482
|
+
const folders = new Set();
|
|
1483
|
+
const sidecarByPath = new Map();
|
|
1484
|
+
const stack = [this.repoPath];
|
|
1485
|
+
const repoRoot = this.repoPath.replace(/\/+$/, '');
|
|
1486
|
+
while (stack.length > 0) {
|
|
1487
|
+
const dir = stack.pop();
|
|
1488
|
+
let entries;
|
|
1489
|
+
try {
|
|
1490
|
+
entries = readdirSync(dir);
|
|
1491
|
+
}
|
|
1492
|
+
catch {
|
|
1493
|
+
continue;
|
|
1494
|
+
}
|
|
1495
|
+
let kept = false;
|
|
1496
|
+
let hasSidecar = false;
|
|
1497
|
+
for (const entry of entries) {
|
|
1498
|
+
if (entry === '.gitkeep')
|
|
1499
|
+
kept = true;
|
|
1500
|
+
if (entry === FOLDER_SIDECAR)
|
|
1501
|
+
hasSidecar = true;
|
|
1502
|
+
if (entry.startsWith('.'))
|
|
1503
|
+
continue;
|
|
1504
|
+
if (WALK_IGNORE.has(entry))
|
|
1505
|
+
continue;
|
|
1506
|
+
const full = join(dir, entry);
|
|
1507
|
+
let st;
|
|
1508
|
+
try {
|
|
1509
|
+
st = statSync(full);
|
|
1510
|
+
}
|
|
1511
|
+
catch {
|
|
1512
|
+
continue;
|
|
1513
|
+
}
|
|
1514
|
+
if (st.isDirectory())
|
|
1515
|
+
stack.push(full);
|
|
1516
|
+
}
|
|
1517
|
+
if (dir === repoRoot)
|
|
1518
|
+
continue;
|
|
1519
|
+
// Include this directory if it has any .md files OR a .gitkeep marker
|
|
1520
|
+
// OR a sidecar (which carries display metadata even for empty folders).
|
|
1521
|
+
const hasMarkdown = entries.some(e => e.endsWith('.md') && !e.startsWith('.'));
|
|
1522
|
+
if (hasMarkdown || kept || hasSidecar) {
|
|
1523
|
+
const folderPath = folderPathFromFile(join(dir, 'placeholder.md'), this.repoPath);
|
|
1524
|
+
folders.add(folderPath);
|
|
1525
|
+
if (hasSidecar) {
|
|
1526
|
+
const display = readFolderSidecar(dir);
|
|
1527
|
+
if (display !== null)
|
|
1528
|
+
sidecarByPath.set(folderPath, display);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
return [...folders]
|
|
1533
|
+
.filter(p => p.length > 0)
|
|
1534
|
+
.sort()
|
|
1535
|
+
.map(p => {
|
|
1536
|
+
const entry = { path: p, name: p.slice(p.lastIndexOf('/') + 1) };
|
|
1537
|
+
const display = sidecarByPath.get(p);
|
|
1538
|
+
if (display)
|
|
1539
|
+
entry.display_name = display;
|
|
1540
|
+
return entry;
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
/**
|
|
1544
|
+
* Read the display_name override for a single folder, or null if no sidecar
|
|
1545
|
+
* exists / is unreadable. Used by mutation paths that need the current
|
|
1546
|
+
* override before deciding whether to rewrite it.
|
|
1547
|
+
*/
|
|
1548
|
+
readFolderDisplayName(folderPath) {
|
|
1549
|
+
const normalized = validateFolderPath(folderPath);
|
|
1550
|
+
if (!normalized)
|
|
1551
|
+
return null;
|
|
1552
|
+
return readFolderSidecar(join(this.repoPath, normalized));
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* Write or remove the per-folder display_name sidecar. Pass `null` /
|
|
1556
|
+
* empty-string to remove (returns to slug fallback). Returns the absolute
|
|
1557
|
+
* path of the sidecar (for git staging) when one was written or removed,
|
|
1558
|
+
* or null when no change was needed (idempotent).
|
|
1559
|
+
*
|
|
1560
|
+
* Caller is responsible for committing — the helper only touches the
|
|
1561
|
+
* filesystem. Folder must already exist; this never creates one.
|
|
1562
|
+
*/
|
|
1563
|
+
writeFolderDisplayName(folderPath, displayName) {
|
|
1564
|
+
const normalized = validateFolderPath(folderPath);
|
|
1565
|
+
if (!normalized) {
|
|
1566
|
+
throw new InvalidFolderPathError('Folder path cannot be empty');
|
|
1567
|
+
}
|
|
1568
|
+
const abs = join(this.repoPath, normalized);
|
|
1569
|
+
if (!existsSync(abs)) {
|
|
1570
|
+
throw new Error(`Folder not found: ${normalized}`);
|
|
1571
|
+
}
|
|
1572
|
+
const trimmed = displayName?.trim();
|
|
1573
|
+
if (!trimmed) {
|
|
1574
|
+
const removed = removeFolderSidecar(abs);
|
|
1575
|
+
if (!removed)
|
|
1576
|
+
return null;
|
|
1577
|
+
this.invalidateIndex();
|
|
1578
|
+
return removed;
|
|
1579
|
+
}
|
|
1580
|
+
const existing = readFolderSidecar(abs);
|
|
1581
|
+
if (existing === trimmed)
|
|
1582
|
+
return null;
|
|
1583
|
+
const sidecarPath = writeFolderSidecar(abs, trimmed);
|
|
1584
|
+
this.invalidateIndex();
|
|
1585
|
+
return sidecarPath;
|
|
1586
|
+
}
|
|
1587
|
+
/**
|
|
1588
|
+
* Bulk-write display_name sidecars for many folders at once. Used by the
|
|
1589
|
+
* import path to record original-casing overrides for every nested
|
|
1590
|
+
* directory it slugified. First-writer-wins: existing sidecars are NOT
|
|
1591
|
+
* overwritten, so a second import doesn't clobber a user's manual rename.
|
|
1592
|
+
*
|
|
1593
|
+
* Returns the list of sidecar paths actually written (for git staging).
|
|
1594
|
+
* Skipped paths (folder missing, slug already matches display, sidecar
|
|
1595
|
+
* already exists) are quietly omitted.
|
|
1596
|
+
*/
|
|
1597
|
+
recordFolderDisplays(entries) {
|
|
1598
|
+
const written = [];
|
|
1599
|
+
for (const entry of entries) {
|
|
1600
|
+
const normalized = validateFolderPath(entry.path);
|
|
1601
|
+
if (!normalized)
|
|
1602
|
+
continue;
|
|
1603
|
+
const abs = join(this.repoPath, normalized);
|
|
1604
|
+
if (!existsSync(abs))
|
|
1605
|
+
continue;
|
|
1606
|
+
const sidecarPath = folderSidecarPath(abs);
|
|
1607
|
+
if (existsSync(sidecarPath))
|
|
1608
|
+
continue; // first-writer-wins
|
|
1609
|
+
const display = entry.display_name.trim();
|
|
1610
|
+
if (!display)
|
|
1611
|
+
continue;
|
|
1612
|
+
written.push(writeFolderSidecar(abs, display));
|
|
1613
|
+
}
|
|
1614
|
+
if (written.length > 0)
|
|
1615
|
+
this.invalidateIndex();
|
|
1616
|
+
return written;
|
|
1617
|
+
}
|
|
1618
|
+
/**
|
|
1619
|
+
* Create a folder at the given path (mkdir -p semantics) and drop a
|
|
1620
|
+
* .gitkeep so git tracks it even when empty. Validates the path. No-op if
|
|
1621
|
+
* the folder already exists; .gitkeep is added if missing.
|
|
1622
|
+
*/
|
|
1623
|
+
async createFolder(folderPath, commitMessage, user, options) {
|
|
1624
|
+
const normalized = validateFolderPath(folderPath);
|
|
1625
|
+
if (!normalized) {
|
|
1626
|
+
throw new InvalidFolderPathError('Folder path cannot be empty (use the repo root directly)');
|
|
1627
|
+
}
|
|
1628
|
+
const abs = join(this.repoPath, normalized);
|
|
1629
|
+
mkdirSync(abs, { recursive: true });
|
|
1630
|
+
const keep = join(abs, '.gitkeep');
|
|
1631
|
+
if (!existsSync(keep)) {
|
|
1632
|
+
writeFileSync(keep, '');
|
|
1633
|
+
}
|
|
1634
|
+
// Sidecar is written here (not after the commit) so it joins the same
|
|
1635
|
+
// commit as .gitkeep — folder creation is atomic from git's perspective.
|
|
1636
|
+
const sidecarPath = options?.displayName
|
|
1637
|
+
? this.writeFolderDisplayName(normalized, options.displayName)
|
|
1638
|
+
: null;
|
|
1639
|
+
this.invalidateIndex();
|
|
1640
|
+
if (this.gitService) {
|
|
1641
|
+
const message = commitMessage || `Create folder: ${normalized}`;
|
|
1642
|
+
const files = sidecarPath ? [keep, sidecarPath] : [keep];
|
|
1643
|
+
this.gitService.commitFiles(files, message, user ?? this.gitUser);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* Rename / move a folder within the repo. Moves the entire subtree
|
|
1648
|
+
* atomically via filesystem rename. Refuses if the target path is already
|
|
1649
|
+
* occupied.
|
|
1650
|
+
*/
|
|
1651
|
+
async renameFolder(fromPath, toPath, commitMessage, user, options) {
|
|
1652
|
+
const fromNormalized = validateFolderPath(fromPath);
|
|
1653
|
+
const toNormalized = validateFolderPath(toPath);
|
|
1654
|
+
if (!fromNormalized)
|
|
1655
|
+
throw new InvalidFolderPathError('Source folder path required');
|
|
1656
|
+
if (!toNormalized)
|
|
1657
|
+
throw new InvalidFolderPathError('Target folder path required');
|
|
1658
|
+
// Display-only update: same path, just rewrite the sidecar.
|
|
1659
|
+
if (fromNormalized === toNormalized) {
|
|
1660
|
+
if (options?.displayName === undefined)
|
|
1661
|
+
return;
|
|
1662
|
+
const sidecarPath = this.writeFolderDisplayName(fromNormalized, options.displayName);
|
|
1663
|
+
if (sidecarPath && this.gitService) {
|
|
1664
|
+
const message = commitMessage || `Update folder display name: ${fromNormalized}`;
|
|
1665
|
+
this.gitService.commitFiles([sidecarPath], message, user ?? this.gitUser);
|
|
1666
|
+
}
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
const fromAbs = join(this.repoPath, fromNormalized);
|
|
1670
|
+
const toAbs = join(this.repoPath, toNormalized);
|
|
1671
|
+
if (!existsSync(fromAbs)) {
|
|
1672
|
+
throw new Error(`Folder not found: ${fromNormalized}`);
|
|
1673
|
+
}
|
|
1674
|
+
if (existsSync(toAbs)) {
|
|
1675
|
+
throw new Error(`Target folder already exists: ${toNormalized}`);
|
|
1676
|
+
}
|
|
1677
|
+
mkdirSync(dirname(toAbs), { recursive: true });
|
|
1678
|
+
const { renameSync } = await import('fs');
|
|
1679
|
+
renameSync(fromAbs, toAbs);
|
|
1680
|
+
// The sidecar moved with the folder via FS rename. Update it now if the
|
|
1681
|
+
// caller passed a new display name (or `null` to clear). Idempotent — no
|
|
1682
|
+
// sidecar write happens when display matches the existing override.
|
|
1683
|
+
let sidecarPath = null;
|
|
1684
|
+
if (options?.displayName !== undefined) {
|
|
1685
|
+
sidecarPath = this.writeFolderDisplayName(toNormalized, options.displayName);
|
|
1686
|
+
}
|
|
1687
|
+
this.invalidateIndex();
|
|
1688
|
+
if (this.gitService) {
|
|
1689
|
+
const message = commitMessage || `Rename folder: ${fromNormalized} → ${toNormalized}`;
|
|
1690
|
+
const files = sidecarPath ? [fromAbs, toAbs, sidecarPath] : [fromAbs, toAbs];
|
|
1691
|
+
this.gitService.commitFiles(files, message, user ?? this.gitUser);
|
|
1692
|
+
}
|
|
1693
|
+
await this.pruneEmptyAncestors(dirname(fromAbs));
|
|
1694
|
+
}
|
|
1695
|
+
/**
|
|
1696
|
+
* Move a folder (and its full subtree) into a different repo at `targetFolderPath`.
|
|
1697
|
+
*
|
|
1698
|
+
* The naive way to do this is to iterate the source's entries and call
|
|
1699
|
+
* `transfer` per entry — that's what the web UI used to do, and it costs
|
|
1700
|
+
* 2 git commits per entry (1600 commits for 800 files), each blocking the
|
|
1701
|
+
* Node event loop via `execSync`. This method performs the move as a
|
|
1702
|
+
* single FS-level rename + one commit per repo, regardless of how many
|
|
1703
|
+
* files are inside.
|
|
1704
|
+
*
|
|
1705
|
+
* Returns the count of entries + subfolders moved so the caller can
|
|
1706
|
+
* surface progress.
|
|
1707
|
+
*/
|
|
1708
|
+
async moveFolderToRepo(sourceFolderPath, target, targetFolderPath, options) {
|
|
1709
|
+
const sourceNormalized = validateFolderPath(sourceFolderPath);
|
|
1710
|
+
if (!sourceNormalized) {
|
|
1711
|
+
throw new InvalidFolderPathError('Source folder path required');
|
|
1712
|
+
}
|
|
1713
|
+
const targetNormalized = targetFolderPath ? validateFolderPath(targetFolderPath) : '';
|
|
1714
|
+
if (targetFolderPath && !targetNormalized) {
|
|
1715
|
+
throw new InvalidFolderPathError('Invalid target folder path');
|
|
1716
|
+
}
|
|
1717
|
+
if (target === this && sourceNormalized === targetNormalized) {
|
|
1718
|
+
return { entriesDeleted: 0, subfoldersDeleted: 0 };
|
|
1719
|
+
}
|
|
1720
|
+
const sourceAbs = join(this.repoPath, sourceNormalized);
|
|
1721
|
+
if (!existsSync(sourceAbs)) {
|
|
1722
|
+
throw new Error(`Source folder not found: ${sourceNormalized}`);
|
|
1723
|
+
}
|
|
1724
|
+
const targetAbs = targetNormalized
|
|
1725
|
+
? join(target.repoPath, targetNormalized)
|
|
1726
|
+
: target.repoPath;
|
|
1727
|
+
// Refuse if target already has visible content — caller must move into
|
|
1728
|
+
// an empty / nonexistent folder. (Hidden files like .studiograph are OK.)
|
|
1729
|
+
if (existsSync(targetAbs)) {
|
|
1730
|
+
const visible = readdirSync(targetAbs).filter(n => !n.startsWith('.'));
|
|
1731
|
+
if (visible.length > 0) {
|
|
1732
|
+
throw new Error(`Target folder is not empty: ${targetNormalized || '/'}`);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
const stats = this.countFolderContents(sourceAbs);
|
|
1736
|
+
// Atomic FS rename if possible. Falls back to copy + delete if the two
|
|
1737
|
+
// repos sit on different filesystems (rare, but possible on Docker
|
|
1738
|
+
// bind mounts).
|
|
1739
|
+
const { renameSync, cpSync, rmSync } = await import('fs');
|
|
1740
|
+
mkdirSync(dirname(targetAbs), { recursive: true });
|
|
1741
|
+
if (existsSync(targetAbs)) {
|
|
1742
|
+
// Empty pre-existing dir — drop it so renameSync can take its place.
|
|
1743
|
+
rmSync(targetAbs, { recursive: true, force: true });
|
|
1744
|
+
}
|
|
1745
|
+
try {
|
|
1746
|
+
renameSync(sourceAbs, targetAbs);
|
|
1747
|
+
}
|
|
1748
|
+
catch (err) {
|
|
1749
|
+
if (err?.code === 'EXDEV') {
|
|
1750
|
+
cpSync(sourceAbs, targetAbs, { recursive: true });
|
|
1751
|
+
rmSync(sourceAbs, { recursive: true, force: true });
|
|
1752
|
+
}
|
|
1753
|
+
else {
|
|
1754
|
+
throw err;
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
this.invalidateIndex();
|
|
1758
|
+
target.invalidateIndex();
|
|
1759
|
+
const user = options?.user ?? this.gitUser;
|
|
1760
|
+
const sourceMessage = options?.commitMessage
|
|
1761
|
+
?? `Move folder ${sourceNormalized} → ${target.repoName}:${targetNormalized || '/'}`;
|
|
1762
|
+
const targetMessage = options?.commitMessage
|
|
1763
|
+
?? `Move folder from ${this.repoName}:${sourceNormalized} → ${targetNormalized || '/'}`;
|
|
1764
|
+
if (this.gitService) {
|
|
1765
|
+
this.gitService.commitFiles([sourceAbs], sourceMessage, user);
|
|
1766
|
+
}
|
|
1767
|
+
if (target.gitService) {
|
|
1768
|
+
target.gitService.commitFiles([targetAbs], targetMessage, user);
|
|
1769
|
+
}
|
|
1770
|
+
await this.pruneEmptyAncestors(dirname(sourceAbs));
|
|
1771
|
+
return stats;
|
|
1772
|
+
}
|
|
1773
|
+
/**
|
|
1774
|
+
* Move a batch of entries (potentially across many source folders) into a
|
|
1775
|
+
* single target folder, optionally in a different repo. The web UI's
|
|
1776
|
+
* multi-select bulk move used to call /move or /transfer per entry — that
|
|
1777
|
+
* was 1-2 sync git commits per entry, blocking the event loop on each one.
|
|
1778
|
+
* This collapses the operation to a single FS rename per file plus one
|
|
1779
|
+
* commit on each repo touched.
|
|
1780
|
+
*/
|
|
1781
|
+
async moveEntitiesToRepo(entries, target, targetFolderPath, options) {
|
|
1782
|
+
if (entries.length === 0)
|
|
1783
|
+
return { moved: 0 };
|
|
1784
|
+
const targetNormalized = targetFolderPath ? validateFolderPath(targetFolderPath) : '';
|
|
1785
|
+
if (targetFolderPath && !targetNormalized) {
|
|
1786
|
+
throw new InvalidFolderPathError('Invalid target folder path');
|
|
1787
|
+
}
|
|
1788
|
+
const targetDir = targetNormalized
|
|
1789
|
+
? join(target.repoPath, targetNormalized)
|
|
1790
|
+
: target.repoPath;
|
|
1791
|
+
// Resolve all source paths up front and validate target conflicts before
|
|
1792
|
+
// any FS work — partial moves on conflict would leave the user with a
|
|
1793
|
+
// confusing half-state.
|
|
1794
|
+
const moves = [];
|
|
1795
|
+
for (const entry of entries) {
|
|
1796
|
+
const existing = this.get(entry.entityType, entry.entityId);
|
|
1797
|
+
const dst = join(targetDir, `${entry.entityId}.md`);
|
|
1798
|
+
if (existing.path === dst)
|
|
1799
|
+
continue; // already there — no-op
|
|
1800
|
+
if (existsSync(dst)) {
|
|
1801
|
+
throw new Error(`Path already occupied: ${target.repoName}:${targetNormalized ? targetNormalized + '/' : ''}${entry.entityId}.md`);
|
|
1802
|
+
}
|
|
1803
|
+
moves.push({ src: existing.path, dst });
|
|
1804
|
+
}
|
|
1805
|
+
if (moves.length === 0)
|
|
1806
|
+
return { moved: 0 };
|
|
1807
|
+
const { renameSync, cpSync } = await import('fs');
|
|
1808
|
+
mkdirSync(targetDir, { recursive: true });
|
|
1809
|
+
for (const m of moves) {
|
|
1810
|
+
try {
|
|
1811
|
+
renameSync(m.src, m.dst);
|
|
1812
|
+
}
|
|
1813
|
+
catch (err) {
|
|
1814
|
+
if (err?.code === 'EXDEV') {
|
|
1815
|
+
cpSync(m.src, m.dst);
|
|
1816
|
+
rmSync(m.src);
|
|
1817
|
+
}
|
|
1818
|
+
else {
|
|
1819
|
+
throw err;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
// Folder lifecycle: ensure markers in every source dir we may have
|
|
1824
|
+
// emptied, drop the marker in the (single) target dir now that it has
|
|
1825
|
+
// .md children. Helpers no-op for the repo root and for paths that
|
|
1826
|
+
// are already in the right state, so calling them per dir is cheap.
|
|
1827
|
+
const sourceMarkers = [];
|
|
1828
|
+
const visitedSourceDirs = new Set();
|
|
1829
|
+
for (const m of moves) {
|
|
1830
|
+
const sourceDir = dirname(m.src);
|
|
1831
|
+
if (visitedSourceDirs.has(sourceDir))
|
|
1832
|
+
continue;
|
|
1833
|
+
visitedSourceDirs.add(sourceDir);
|
|
1834
|
+
const added = this.ensureFolderMarker(sourceDir);
|
|
1835
|
+
if (added)
|
|
1836
|
+
sourceMarkers.push(added);
|
|
1837
|
+
}
|
|
1838
|
+
const removedTargetMarker = target.removeFolderMarker(targetDir);
|
|
1839
|
+
this.invalidateIndex();
|
|
1840
|
+
if (target !== this)
|
|
1841
|
+
target.invalidateIndex();
|
|
1842
|
+
const user = options?.user ?? this.gitUser;
|
|
1843
|
+
const where = target === this
|
|
1844
|
+
? (targetNormalized || '(root)')
|
|
1845
|
+
: `${target.repoName}:${targetNormalized || '(root)'}`;
|
|
1846
|
+
const sourceMessage = options?.commitMessage ?? `Move ${moves.length} entries → ${where}`;
|
|
1847
|
+
if (this.gitService) {
|
|
1848
|
+
const sourcePaths = [...moves.map(m => m.src), ...sourceMarkers];
|
|
1849
|
+
const dstStaging = target === this
|
|
1850
|
+
? [...moves.map(m => m.dst), ...(removedTargetMarker ? [removedTargetMarker] : [])]
|
|
1851
|
+
: [];
|
|
1852
|
+
this.gitService.commitFiles([...sourcePaths, ...dstStaging], sourceMessage, user);
|
|
1853
|
+
}
|
|
1854
|
+
if (target !== this && target.gitService) {
|
|
1855
|
+
const targetMessage = options?.commitMessage
|
|
1856
|
+
?? `Receive ${moves.length} entries from ${this.repoName}`;
|
|
1857
|
+
const targetPaths = [...moves.map(m => m.dst), ...(removedTargetMarker ? [removedTargetMarker] : [])];
|
|
1858
|
+
target.gitService.commitFiles(targetPaths, targetMessage, user);
|
|
1859
|
+
}
|
|
1860
|
+
return { moved: moves.length };
|
|
1861
|
+
}
|
|
1862
|
+
/**
|
|
1863
|
+
* Delete a folder. Refuses non-empty folders unless `recursive: true`.
|
|
1864
|
+
* Returns the count of entries + subfolders removed (for blast-radius UI).
|
|
1865
|
+
*/
|
|
1866
|
+
async deleteFolder(folderPath, options) {
|
|
1867
|
+
const normalized = validateFolderPath(folderPath);
|
|
1868
|
+
if (!normalized) {
|
|
1869
|
+
throw new InvalidFolderPathError('Cannot delete the repo root via deleteFolder');
|
|
1870
|
+
}
|
|
1871
|
+
const abs = join(this.repoPath, normalized);
|
|
1872
|
+
if (!existsSync(abs)) {
|
|
1873
|
+
throw new Error(`Folder not found: ${normalized}`);
|
|
1874
|
+
}
|
|
1875
|
+
const stats = this.countFolderContents(abs);
|
|
1876
|
+
if (!options?.recursive && (stats.entriesDeleted > 0 || stats.subfoldersDeleted > 0)) {
|
|
1877
|
+
const err = new Error(`Folder is not empty: ${normalized} (${stats.entriesDeleted} entries, ${stats.subfoldersDeleted} subfolders). Pass recursive: true to delete contents.`);
|
|
1878
|
+
err.code = 'FOLDER_NOT_EMPTY';
|
|
1879
|
+
err.blastRadius = stats;
|
|
1880
|
+
throw err;
|
|
1881
|
+
}
|
|
1882
|
+
const { rmSync } = await import('fs');
|
|
1883
|
+
rmSync(abs, { recursive: true, force: true });
|
|
1884
|
+
this.invalidateIndex();
|
|
1885
|
+
if (this.gitService) {
|
|
1886
|
+
const message = options?.commitMessage || `Delete folder: ${normalized}`;
|
|
1887
|
+
this.gitService.commitFiles([abs], message, options?.user ?? this.gitUser);
|
|
1888
|
+
}
|
|
1889
|
+
await this.pruneEmptyAncestors(dirname(abs));
|
|
1890
|
+
return stats;
|
|
1891
|
+
}
|
|
1892
|
+
/**
|
|
1893
|
+
* Count the entries (.md files with entity_type) and subfolders inside an
|
|
1894
|
+
* absolute directory, recursively. Used to surface blast radius in the
|
|
1895
|
+
* delete-folder approval UI.
|
|
1896
|
+
*/
|
|
1897
|
+
countFolderContents(absDir) {
|
|
1898
|
+
let entriesDeleted = 0;
|
|
1899
|
+
const subfolders = new Set();
|
|
1900
|
+
const stack = [absDir];
|
|
1901
|
+
while (stack.length > 0) {
|
|
1902
|
+
const dir = stack.pop();
|
|
1903
|
+
let entries;
|
|
1904
|
+
try {
|
|
1905
|
+
entries = readdirSync(dir);
|
|
1906
|
+
}
|
|
1907
|
+
catch {
|
|
1908
|
+
continue;
|
|
1909
|
+
}
|
|
1910
|
+
for (const entry of entries) {
|
|
1911
|
+
if (entry.startsWith('.'))
|
|
1912
|
+
continue;
|
|
1913
|
+
if (WALK_IGNORE.has(entry))
|
|
1914
|
+
continue;
|
|
1915
|
+
const full = join(dir, entry);
|
|
1916
|
+
let st;
|
|
1917
|
+
try {
|
|
1918
|
+
st = statSync(full);
|
|
1919
|
+
}
|
|
1920
|
+
catch {
|
|
1921
|
+
continue;
|
|
1922
|
+
}
|
|
1923
|
+
if (st.isDirectory()) {
|
|
1924
|
+
subfolders.add(full);
|
|
1925
|
+
stack.push(full);
|
|
1926
|
+
}
|
|
1927
|
+
else if (st.isFile() && entry.endsWith('.md')) {
|
|
1928
|
+
entriesDeleted += 1;
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
return { entriesDeleted, subfoldersDeleted: subfolders.size };
|
|
1933
|
+
}
|
|
1934
|
+
/**
|
|
1935
|
+
* Return the storage backend the graph is bound to, or `undefined` if
|
|
1936
|
+
* no asset service is configured. Used by routes that need to branch
|
|
1937
|
+
* between local-stream and R2-redirect behavior.
|
|
1938
|
+
*/
|
|
1939
|
+
getAssetStorage() {
|
|
1940
|
+
return this.assetService?.getStorage();
|
|
1941
|
+
}
|
|
1942
|
+
/**
|
|
1943
|
+
* Mint a short-lived pre-signed URL for an asset. Only supported by
|
|
1944
|
+
* remote backends (R2); the local backend throws. `method` selects
|
|
1945
|
+
* the verb the URL is signed against — HEAD callers need a HEAD URL.
|
|
1946
|
+
*/
|
|
1947
|
+
async getAssetSignedUrl(entityType, entityId, filename, keyPrefix, expiresIn, method = 'GET') {
|
|
1948
|
+
if (!this.assetService) {
|
|
1949
|
+
throw new Error('Asset service is not configured for this graph');
|
|
1950
|
+
}
|
|
1951
|
+
return await this.assetService.getSignedUrl(entityType, entityId, filename, keyPrefix, expiresIn, method);
|
|
1952
|
+
}
|
|
805
1953
|
/**
|
|
806
1954
|
* Upload asset for an entity
|
|
807
1955
|
*/
|
|
@@ -821,84 +1969,63 @@ export class BaseGraphManager {
|
|
|
821
1969
|
return await this.assetService.list(entityType, entityId, keyPrefix);
|
|
822
1970
|
}
|
|
823
1971
|
/**
|
|
824
|
-
*
|
|
1972
|
+
* List every asset in the backend in one round-trip, tagged with
|
|
1973
|
+
* (entityType, entityId) origin. Workspace-wide since the asset backend
|
|
1974
|
+
* is shared across repos — callers filter by repo via {@link listEntityIds}.
|
|
825
1975
|
*/
|
|
826
|
-
async
|
|
1976
|
+
async listAllAssets() {
|
|
827
1977
|
if (!this.assetService) {
|
|
828
1978
|
throw new Error('Asset service is not configured for this graph');
|
|
829
1979
|
}
|
|
830
|
-
return await this.assetService.
|
|
1980
|
+
return await this.assetService.listAll();
|
|
831
1981
|
}
|
|
832
1982
|
/**
|
|
833
|
-
*
|
|
1983
|
+
* Enumerate (type, id) pairs in this repo. Used by endpoints (like
|
|
1984
|
+
* /api/repos/:repo/assets) that need to answer "does entity X belong to
|
|
1985
|
+
* this repo?".
|
|
1986
|
+
*
|
|
1987
|
+
* Backed by the file index, which reads frontmatter — so entries in
|
|
1988
|
+
* nested folders are included alongside legacy {plural-type}/ entries.
|
|
834
1989
|
*/
|
|
835
|
-
|
|
836
|
-
if (!this.
|
|
837
|
-
|
|
1990
|
+
listEntityIds() {
|
|
1991
|
+
if (!existsSync(this.repoPath))
|
|
1992
|
+
return [];
|
|
1993
|
+
const results = [];
|
|
1994
|
+
for (const key of this.getFileIndex().keys()) {
|
|
1995
|
+
const slash = key.indexOf('/');
|
|
1996
|
+
if (slash <= 0)
|
|
1997
|
+
continue;
|
|
1998
|
+
results.push({ entityType: key.slice(0, slash), id: key.slice(slash + 1) });
|
|
838
1999
|
}
|
|
839
|
-
return
|
|
2000
|
+
return results;
|
|
840
2001
|
}
|
|
841
2002
|
/**
|
|
842
|
-
*
|
|
2003
|
+
* Resolve the absolute on-disk markdown path for an entity. Goes through
|
|
2004
|
+
* the file index so it works for both the flat repo-root layout and
|
|
2005
|
+
* arbitrary nested folders. Use this anywhere callers need a path —
|
|
2006
|
+
* never reconstruct one from `${plural-type}/${id}.md`, which assumes
|
|
2007
|
+
* the legacy typed-folder layout and silently misses everything else.
|
|
843
2008
|
*/
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
if (entity.data.entity_type !== 'dataset') {
|
|
847
|
-
throw new Error(`Entity ${entityId} is not a dataset`);
|
|
848
|
-
}
|
|
849
|
-
const schema = entity.data.schema;
|
|
850
|
-
const rows = this.csvService.parseRows(entity.document.content, schema);
|
|
851
|
-
if (filter && Object.keys(filter).length > 0) {
|
|
852
|
-
return this.csvService.filterRows(rows, filter);
|
|
853
|
-
}
|
|
854
|
-
return rows;
|
|
2009
|
+
getEntityFilePath(entityType, entityId) {
|
|
2010
|
+
return this.findEntityFile(entityType, entityId);
|
|
855
2011
|
}
|
|
856
2012
|
/**
|
|
857
|
-
*
|
|
2013
|
+
* Delete an asset
|
|
858
2014
|
*/
|
|
859
|
-
async
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
throw new Error(`Entity ${entityId} is not a dataset`);
|
|
2015
|
+
async deleteAsset(entityType, entityId, filename, keyPrefix) {
|
|
2016
|
+
if (!this.assetService) {
|
|
2017
|
+
throw new Error('Asset service is not configured for this graph');
|
|
863
2018
|
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
});
|
|
873
|
-
// Parse existing rows and append
|
|
874
|
-
const existingRows = this.csvService.parseRows(entity.document.content, schema);
|
|
875
|
-
const allRows = [...existingRows, ...validatedRows];
|
|
876
|
-
// Stringify back to CSV
|
|
877
|
-
const newContent = this.csvService.stringifyRows(allRows, schema);
|
|
878
|
-
// Update the entity file (update content, bump updated_at)
|
|
879
|
-
const entityPath = this.getEntityPath('dataset', entityId);
|
|
880
|
-
const updatedData = {
|
|
881
|
-
...entity.data,
|
|
882
|
-
updated_at: new Date().toISOString(),
|
|
883
|
-
updated_by: this.gitUser.id,
|
|
884
|
-
};
|
|
885
|
-
this.markdownService.writeFile(entityPath, {
|
|
886
|
-
frontmatter: updatedData,
|
|
887
|
-
content: newContent,
|
|
888
|
-
wikilinks: this.markdownService.extractWikilinks(newContent, updatedData),
|
|
889
|
-
});
|
|
890
|
-
if (this.gitService) {
|
|
891
|
-
const transaction = this.gitService.createTransaction();
|
|
892
|
-
transaction.add({
|
|
893
|
-
operation: 'update',
|
|
894
|
-
entityType: 'dataset',
|
|
895
|
-
entityId,
|
|
896
|
-
filePath: entityPath,
|
|
897
|
-
});
|
|
898
|
-
const message = commitMessage || `Append ${validatedRows.length} row(s) to dataset: ${entityId}`;
|
|
899
|
-
await transaction.commit(this.gitUser, message);
|
|
2019
|
+
return await this.assetService.delete(entityType, entityId, filename, keyPrefix);
|
|
2020
|
+
}
|
|
2021
|
+
/**
|
|
2022
|
+
* Delete all assets for an entity (when entity is deleted)
|
|
2023
|
+
*/
|
|
2024
|
+
async deleteAllAssets(entityType, entityId, keyPrefix) {
|
|
2025
|
+
if (!this.assetService) {
|
|
2026
|
+
throw new Error('Asset service is not configured for this graph');
|
|
900
2027
|
}
|
|
901
|
-
return this.
|
|
2028
|
+
return await this.assetService.deleteAll(entityType, entityId, keyPrefix);
|
|
902
2029
|
}
|
|
903
2030
|
/**
|
|
904
2031
|
* Get repository status
|