studiograph 1.3.48-next.13 → 1.3.48-next.131
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 +27 -1
- package/dist/agent/agent-pool.js +55 -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 +251 -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 +82 -23
- 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 +49 -0
- package/dist/core/secrets/index.js +74 -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 +531 -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 +385 -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 +308 -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 +180 -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/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/7R0Rp3Mz.js +1 -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/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/B7L4gmPR.js +2 -0
- package/dist/web/_app/immutable/chunks/B9qBGxh62.js +3 -0
- package/dist/web/_app/immutable/chunks/BDJYj9yi.js +2 -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/BKe5wPiN.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/BiUh0Sik.js +1 -0
- package/dist/web/_app/immutable/chunks/Biajc_oU.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/BmgyOoEp.js +1 -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/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/CIchr8Tk2.js +1 -0
- package/dist/web/_app/immutable/chunks/CK_yXDQI.js +1 -0
- package/dist/web/_app/immutable/chunks/CLPtMig0.js +1 -0
- package/dist/web/_app/immutable/chunks/CLSxJmTj.js +2 -0
- package/dist/web/_app/immutable/chunks/CPQNcHIB.js +3 -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/CSGIGrrK.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/CfqjDDMu.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/ClNEskRf.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/CsXIliss.js +1 -0
- package/dist/web/_app/immutable/chunks/Ct3quP0F.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/CxalqrMB.js +1 -0
- package/dist/web/_app/immutable/chunks/D1Ap3evT2.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/DCCaoKt42.js +1 -0
- package/dist/web/_app/immutable/chunks/DDKweOZr2.js +23 -0
- package/dist/web/_app/immutable/chunks/DHeU-hHI2.js +2 -0
- package/dist/web/_app/immutable/chunks/DHepUItL.js +1 -0
- package/dist/web/_app/immutable/chunks/DL_KbCKq.js +8 -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/DZetJu9k2.js +1 -0
- package/dist/web/_app/immutable/chunks/DaiJs3St.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/Dm25ot5x2.js +1 -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/Dt4HY2cT2.js +2 -0
- package/dist/web/_app/immutable/chunks/DtGUCMv8.js +6 -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/LAN53VbP2.js +5 -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/TuBvdkhV2.js +1 -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/dTXrX5Bw.js +2 -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/q6R6T-lK.js +1 -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/vwTMEIJQ.js +1 -0
- package/dist/web/_app/immutable/chunks/w1FCELam.js +64 -0
- package/dist/web/_app/immutable/chunks/zmjgin7A.js +1 -0
- package/dist/web/_app/immutable/chunks/zumDjcgJ.js +1 -0
- package/dist/web/_app/immutable/entry/app.B3JEO5sy.js +2 -0
- package/dist/web/_app/immutable/entry/start.Dk84vnrL.js +1 -0
- package/dist/web/_app/immutable/nodes/0.BKLFgYTV.js +2 -0
- package/dist/web/_app/immutable/nodes/1.Djf_1kN1.js +1 -0
- package/dist/web/_app/immutable/nodes/10.De6eGWLx.js +1 -0
- package/dist/web/_app/immutable/nodes/11.CDsfSRTw.js +1 -0
- package/dist/web/_app/immutable/nodes/12.BeKkKpkY.js +1 -0
- package/dist/web/_app/immutable/nodes/13.BcI8xwSy.js +1 -0
- package/dist/web/_app/immutable/nodes/2.ZMZ-QAZ7.js +106 -0
- package/dist/web/_app/immutable/nodes/3.C1ZiAfle.js +2 -0
- package/dist/web/_app/immutable/nodes/4.Chmqy9KF.js +2 -0
- package/dist/web/_app/immutable/nodes/5.Bpvr_Vcq.js +1 -0
- package/dist/web/_app/immutable/nodes/6.BHa_gOs3.js +1 -0
- package/dist/web/_app/immutable/nodes/7.D0hg4Eap.js +1 -0
- package/dist/web/_app/immutable/nodes/8.3ZUUxJqR.js +1 -0
- package/dist/web/_app/immutable/nodes/9.Ducy-oqR.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
|
@@ -2,132 +2,55 @@
|
|
|
2
2
|
* Graph API routes
|
|
3
3
|
*
|
|
4
4
|
* REST façade over WorkspaceManager.
|
|
5
|
-
* Enforces per-
|
|
5
|
+
* Enforces per-folder access when users are authenticated via JWT.
|
|
6
6
|
* API key / open mode → full access (no filtering).
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { isWorkspaceOwner } from '../../services/auth-service.js';
|
|
9
|
+
import { WorkspaceAccessService } from '../../services/workspace-access.js';
|
|
9
10
|
import { Workspace } from '../../core/workspace.js';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const user = req.user;
|
|
33
|
-
// Filter out private repos owned by other users (applies to all roles)
|
|
34
|
-
const visible = allRepos.filter(r => canSeePrivateRepo(r, user));
|
|
35
|
-
if (!user)
|
|
36
|
-
return visible; // API key / open mode → full access
|
|
37
|
-
if (isAdminOrOwner(user.role))
|
|
38
|
-
return visible;
|
|
39
|
-
const allowed = new Set(authService.getUserCollections(user.id));
|
|
40
|
-
return visible.filter(r => r.private || allowed.has(r.name));
|
|
41
|
-
}
|
|
42
|
-
/** Check if the current user can access a specific repo. */
|
|
43
|
-
function hasRepoAccess(req, repoName, workspaceManager, authService) {
|
|
44
|
-
const user = req.user;
|
|
45
|
-
const repoConfig = workspaceManager.getRepoConfig(repoName);
|
|
46
|
-
if (repoConfig && !canSeePrivateRepo(repoConfig, user))
|
|
47
|
-
return false;
|
|
48
|
-
if (!user)
|
|
49
|
-
return true; // API key / open mode
|
|
50
|
-
if (repoConfig?.private && repoConfig?.owner_id === user.id)
|
|
51
|
-
return true; // Owner of private repo
|
|
52
|
-
if (isAdminOrOwner(user.role))
|
|
53
|
-
return true;
|
|
54
|
-
return authService.getUserCollections(user.id).includes(repoName);
|
|
55
|
-
}
|
|
56
|
-
/** Check if the current user is an admin. */
|
|
57
|
-
function isAdmin(req) {
|
|
58
|
-
const user = req.user;
|
|
59
|
-
if (!user)
|
|
60
|
-
return true; // API key / open mode → admin-equivalent
|
|
61
|
-
return isAdminOrOwner(user.role);
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Build a set of all entity IDs visible to the current user.
|
|
65
|
-
* Used to filter wikilinks so users don't see links to entities
|
|
66
|
-
* in collections they can't access.
|
|
67
|
-
*/
|
|
68
|
-
function getAccessibleEntityIds(req, workspaceManager, authService) {
|
|
69
|
-
const user = req.user;
|
|
70
|
-
if (!user)
|
|
71
|
-
return null; // API key / open mode → no filtering
|
|
72
|
-
if (isAdminOrOwner(user.role)) {
|
|
73
|
-
// Admins see everything except other users' private repos
|
|
74
|
-
const allRepos = workspaceManager.getAllRepoConfigs();
|
|
75
|
-
const hasHiddenPrivate = allRepos.some(r => r.private && r.owner_id && r.owner_id !== user.id);
|
|
76
|
-
if (!hasHiddenPrivate)
|
|
77
|
-
return null; // No filtering needed
|
|
78
|
-
}
|
|
79
|
-
const repos = getAccessibleRepos(req, workspaceManager, authService);
|
|
80
|
-
const ids = new Set();
|
|
81
|
-
for (const repo of repos) {
|
|
82
|
-
try {
|
|
83
|
-
const graph = workspaceManager.getGraph(repo.name);
|
|
84
|
-
for (const e of graph.search({})) {
|
|
85
|
-
ids.add(e.id);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
catch { /* skip */ }
|
|
89
|
-
}
|
|
90
|
-
return ids;
|
|
91
|
-
}
|
|
92
|
-
/** Filter wikilinks to only those pointing to accessible entities. */
|
|
93
|
-
function filterWikilinks(wikilinks, accessibleIds) {
|
|
94
|
-
if (!accessibleIds)
|
|
95
|
-
return wikilinks;
|
|
96
|
-
return wikilinks.filter(id => accessibleIds.has(id));
|
|
97
|
-
}
|
|
98
|
-
export async function registerGraphApiRoutes(fastify, workspaceManager, authService, wsHub, sessionManager) {
|
|
99
|
-
// POST /api/repos — create a new collection (admin/owner only)
|
|
11
|
+
import { createSharedFolder, FolderCreationError } from '../../services/folder-creation.js';
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
|
13
|
+
import { dirname, join } from 'path';
|
|
14
|
+
import { checkEagerSkillPermission, filterWikilinks, getAccessibleEntityIds, getAccessibleRepos, graphForAdmin as resolveGraphForAdmin, graphForRead as resolveGraphForRead, graphForWrite as resolveGraphForWrite, isFolderAdminOrOwner, reqGitUser, reqUser, } from './graph-api-access.js';
|
|
15
|
+
import { log } from '../../utils/log.js';
|
|
16
|
+
export async function registerGraphApiRoutes(fastify, workspaceManager, authService, wsHub, sessionManager, yjsManager) {
|
|
17
|
+
// ARCHITECTURAL BOUNDARY: every per-route graph resolution flows through
|
|
18
|
+
// this service so the act of getting a graph IS the access check. Do not
|
|
19
|
+
// call `workspaceManager.getGraph(repo)` from a request-scoped handler —
|
|
20
|
+
// use `accessService.graphFor*(reqUser(req), repo)` instead. A security
|
|
21
|
+
// audit (2026-05) flagged the previous opt-in pattern (route had to
|
|
22
|
+
// remember `hasRepoAccess()` before `getGraph()`) as an architectural
|
|
23
|
+
// risk; if you find yourself reaching for the raw WM here, that's the
|
|
24
|
+
// smell the audit was about.
|
|
25
|
+
const accessService = new WorkspaceAccessService(workspaceManager, authService);
|
|
26
|
+
const graphForRead = (req, reply, repoName) => resolveGraphForRead(accessService, req, reply, repoName);
|
|
27
|
+
const graphForWrite = (req, reply, repoName) => resolveGraphForWrite(accessService, req, reply, repoName);
|
|
28
|
+
const graphForAdmin = (req, reply, repoName) => resolveGraphForAdmin(accessService, req, reply, repoName);
|
|
29
|
+
// POST /api/repos — any authenticated user can create a folder.
|
|
30
|
+
// The creator is granted folder-admin access automatically.
|
|
31
|
+
// Shared logic lives in services/folder-creation.ts so the MCP tool
|
|
32
|
+
// create_repo behaves identically.
|
|
100
33
|
fastify.post('/api/repos', async (req, reply) => {
|
|
101
|
-
|
|
102
|
-
return reply.status(403).send({ error: 'Admin access required' });
|
|
103
|
-
}
|
|
34
|
+
const user = req.user;
|
|
104
35
|
const { name, display_name, description } = req.body || {};
|
|
105
|
-
if (!name || !/^[a-z0-9][a-z0-9-]*$/.test(name)) {
|
|
106
|
-
return reply.status(400).send({ error: 'Collection name must be kebab-case' });
|
|
107
|
-
}
|
|
108
36
|
try {
|
|
109
|
-
const
|
|
110
|
-
const repo = await workspace.registerRepo({
|
|
111
|
-
name,
|
|
112
|
-
display_name: display_name || undefined,
|
|
113
|
-
description,
|
|
114
|
-
});
|
|
115
|
-
workspaceManager.reloadConfig();
|
|
116
|
-
wsHub?.broadcast({
|
|
117
|
-
type: 'collection_created', repo: name,
|
|
118
|
-
actor: req.user?.displayName ?? 'API',
|
|
119
|
-
timestamp: new Date().toISOString(),
|
|
120
|
-
});
|
|
37
|
+
const repo = await createSharedFolder(workspaceManager, authService, { name, display_name, description }, user, wsHub);
|
|
121
38
|
return reply.status(201).send(repo);
|
|
122
39
|
}
|
|
123
40
|
catch (err) {
|
|
124
|
-
|
|
41
|
+
if (err instanceof FolderCreationError) {
|
|
42
|
+
const status = err.code === 'invalid_name' ? 400
|
|
43
|
+
: err.code === 'duplicate' ? 409
|
|
44
|
+
: 500;
|
|
45
|
+
return reply.status(status).send({ error: err.message });
|
|
46
|
+
}
|
|
47
|
+
return reply.status(500).send({ error: err?.message ?? String(err) });
|
|
125
48
|
}
|
|
126
49
|
});
|
|
127
|
-
// DELETE /api/repos/:repo —
|
|
50
|
+
// DELETE /api/repos/:repo — folder admin or workspace owner only.
|
|
128
51
|
fastify.delete('/api/repos/:repo', async (req, reply) => {
|
|
129
|
-
if (!
|
|
130
|
-
return reply.status(403).send({ error: '
|
|
52
|
+
if (!isFolderAdminOrOwner(req, req.params.repo, workspaceManager, authService)) {
|
|
53
|
+
return reply.status(403).send({ error: 'Folder admin access required' });
|
|
131
54
|
}
|
|
132
55
|
const { repo } = req.params;
|
|
133
56
|
try {
|
|
@@ -135,14 +58,14 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
135
58
|
const config = await workspace.loadConfig();
|
|
136
59
|
const repoIndex = config.repos.findIndex(r => r.name === repo);
|
|
137
60
|
if (repoIndex === -1) {
|
|
138
|
-
return reply.status(404).send({ error: `
|
|
61
|
+
return reply.status(404).send({ error: `Folder \u201C${repo}\u201D not found` });
|
|
139
62
|
}
|
|
140
63
|
const repoConfig = config.repos[repoIndex];
|
|
141
64
|
const repoPath = join(workspaceManager['workspacePath'], repoConfig.path);
|
|
142
65
|
// Remove from config
|
|
143
66
|
config.repos.splice(repoIndex, 1);
|
|
144
67
|
workspace.writeConfig(config);
|
|
145
|
-
workspace.pushWorkspaceConfig(`Remove
|
|
68
|
+
workspace.pushWorkspaceConfig(`Remove folder: ${repo}`);
|
|
146
69
|
// Delete files
|
|
147
70
|
const { rmSync } = await import('fs');
|
|
148
71
|
if (existsSync(repoPath)) {
|
|
@@ -150,7 +73,7 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
150
73
|
}
|
|
151
74
|
workspaceManager.reloadConfig();
|
|
152
75
|
wsHub?.broadcast({
|
|
153
|
-
type: '
|
|
76
|
+
type: 'folder_deleted', repo,
|
|
154
77
|
actor: req.user?.displayName ?? 'API',
|
|
155
78
|
timestamp: new Date().toISOString(),
|
|
156
79
|
});
|
|
@@ -160,16 +83,16 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
160
83
|
return reply.status(500).send({ error: err.message });
|
|
161
84
|
}
|
|
162
85
|
});
|
|
163
|
-
// PATCH /api/repos/:repo — update
|
|
86
|
+
// PATCH /api/repos/:repo — rename/update sync rules. Folder admin or workspace owner only.
|
|
164
87
|
fastify.patch('/api/repos/:repo', async (req, reply) => {
|
|
165
|
-
if (!
|
|
166
|
-
return reply.status(403).send({ error: '
|
|
88
|
+
if (!isFolderAdminOrOwner(req, req.params.repo, workspaceManager, authService)) {
|
|
89
|
+
return reply.status(403).send({ error: 'Folder admin access required' });
|
|
167
90
|
}
|
|
168
91
|
const { repo } = req.params;
|
|
169
92
|
const body = req.body || {};
|
|
170
93
|
const { name: newName, display_name: newDisplayName, sync: newSync, ...rest } = body;
|
|
171
|
-
if ('
|
|
172
|
-
return reply.status(400).send({ error: 'The
|
|
94
|
+
if ('personal' in rest) {
|
|
95
|
+
return reply.status(400).send({ error: 'The personal flag is immutable after creation' });
|
|
173
96
|
}
|
|
174
97
|
if (newName === undefined && newSync === undefined && newDisplayName === undefined) {
|
|
175
98
|
return reply.status(400).send({ error: 'Nothing to update' });
|
|
@@ -182,12 +105,12 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
182
105
|
const config = await workspace.loadConfig();
|
|
183
106
|
const repoConfig = config.repos.find(r => r.name === repo);
|
|
184
107
|
if (!repoConfig) {
|
|
185
|
-
return reply.status(404).send({ error: `
|
|
108
|
+
return reply.status(404).send({ error: `Folder \u201C${repo}\u201D not found` });
|
|
186
109
|
}
|
|
187
110
|
// Handle rename
|
|
188
111
|
if (newName !== undefined) {
|
|
189
112
|
if (config.repos.some(r => r.name === newName)) {
|
|
190
|
-
return reply.status(409).send({ error: `
|
|
113
|
+
return reply.status(409).send({ error: `Folder \u201C${newName}\u201D already exists` });
|
|
191
114
|
}
|
|
192
115
|
const wsPath = workspaceManager['workspacePath'];
|
|
193
116
|
const oldPath = join(wsPath, repoConfig.path);
|
|
@@ -206,8 +129,8 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
206
129
|
if (existsSync(markerPath)) {
|
|
207
130
|
writeFS(markerPath, JSON.stringify({ repo_name: newName }, null, 2), 'utf-8');
|
|
208
131
|
}
|
|
209
|
-
// Update
|
|
210
|
-
authService.
|
|
132
|
+
// Update folder_access to follow the rename
|
|
133
|
+
authService.renameFolder(repo, newName);
|
|
211
134
|
}
|
|
212
135
|
// Handle display name update (without rename)
|
|
213
136
|
if (newDisplayName !== undefined && newName === undefined) {
|
|
@@ -218,7 +141,7 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
218
141
|
repoConfig.sync = newSync.length > 0 ? newSync : undefined;
|
|
219
142
|
}
|
|
220
143
|
workspace.writeConfig(config);
|
|
221
|
-
workspace.pushWorkspaceConfig(newName !== undefined ? `Rename
|
|
144
|
+
workspace.pushWorkspaceConfig(newName !== undefined ? `Rename folder: ${repo} → ${newName}` : `Update folder: ${repo}`);
|
|
222
145
|
workspaceManager.reloadConfig();
|
|
223
146
|
return reply.send(repoConfig);
|
|
224
147
|
}
|
|
@@ -226,50 +149,122 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
226
149
|
return reply.status(500).send({ error: err.message });
|
|
227
150
|
}
|
|
228
151
|
});
|
|
229
|
-
// GET /api/repos — list accessible repos (re-reads config to pick up CLI changes)
|
|
152
|
+
// GET /api/repos — list accessible repos (re-reads config to pick up CLI changes).
|
|
153
|
+
// Augments each repo with the caller's folder role ('admin' | 'editor'),
|
|
154
|
+
// member_count, entity_types (distinct types present), and folders (the
|
|
155
|
+
// flat list of nested-folder paths in the repo, used by the sidebar to
|
|
156
|
+
// render the active-path expansion).
|
|
230
157
|
fastify.get('/api/repos', async (req, reply) => {
|
|
231
158
|
workspaceManager.reloadConfig();
|
|
232
159
|
const repos = getAccessibleRepos(req, workspaceManager, authService);
|
|
233
|
-
|
|
160
|
+
const user = req.user;
|
|
161
|
+
const enriched = repos.map(r => {
|
|
162
|
+
const memberCount = authService.getFolderMembers(r.name).length;
|
|
163
|
+
const callerRole = user ? authService.getFolderRole(user.id, r.name) : null;
|
|
164
|
+
let entityTypes = [];
|
|
165
|
+
// `folders` carries the per-folder display_name override (when one
|
|
166
|
+
// exists in the folder's .studiograph-folder.json sidecar); the client
|
|
167
|
+
// falls back to toDisplayName(slug) when display_name is absent.
|
|
168
|
+
let folders = [];
|
|
169
|
+
try {
|
|
170
|
+
// Direct WM access OK here — `repos` came from getAccessibleRepos
|
|
171
|
+
// above, so every iteration is over a folder the caller can read.
|
|
172
|
+
const graph = workspaceManager.getGraph(r.name);
|
|
173
|
+
entityTypes = graph.listEntityTypes();
|
|
174
|
+
folders = graph.listFolders().map(f => {
|
|
175
|
+
const entry = { path: f.path };
|
|
176
|
+
if (f.display_name)
|
|
177
|
+
entry.display_name = f.display_name;
|
|
178
|
+
return entry;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
catch { /* repo not mounted */ }
|
|
182
|
+
return {
|
|
183
|
+
...r,
|
|
184
|
+
caller_role: callerRole,
|
|
185
|
+
member_count: memberCount,
|
|
186
|
+
entity_types: entityTypes,
|
|
187
|
+
folders,
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
return reply.send(enriched);
|
|
191
|
+
});
|
|
192
|
+
// GET /api/admin/folders — workspace-owner-only metadata list of every folder
|
|
193
|
+
// (name, display_name, creator, member count). No content access implied —
|
|
194
|
+
// owners still need to join a folder to read its entries.
|
|
195
|
+
fastify.get('/api/admin/folders', async (req, reply) => {
|
|
196
|
+
const user = req.user;
|
|
197
|
+
if (user && !isWorkspaceOwner(user.role)) {
|
|
198
|
+
return reply.status(403).send({ error: 'Workspace owner access required' });
|
|
199
|
+
}
|
|
200
|
+
workspaceManager.reloadConfig();
|
|
201
|
+
const all = workspaceManager.getAllRepoConfigs();
|
|
202
|
+
const result = all.map(r => {
|
|
203
|
+
const members = authService.getFolderMembers(r.name);
|
|
204
|
+
const admins = members.filter(m => m.role === 'admin');
|
|
205
|
+
const creator = r.owner_id
|
|
206
|
+
? members.find(m => m.user.id === r.owner_id)?.user
|
|
207
|
+
: admins[0]?.user;
|
|
208
|
+
let entryCount = 0;
|
|
209
|
+
// Direct WM access OK — this endpoint is owner-only (gated above) and
|
|
210
|
+
// returns metadata for every folder the workspace owner needs to see
|
|
211
|
+
// for admin purposes. No content is leaked: only entry counts.
|
|
212
|
+
try {
|
|
213
|
+
entryCount = workspaceManager.getGraph(r.name).search({}).length;
|
|
214
|
+
}
|
|
215
|
+
catch { /* skip */ }
|
|
216
|
+
return {
|
|
217
|
+
name: r.name,
|
|
218
|
+
display_name: r.display_name ?? r.name,
|
|
219
|
+
description: r.description,
|
|
220
|
+
member_count: members.length,
|
|
221
|
+
admin_count: admins.length,
|
|
222
|
+
entry_count: entryCount,
|
|
223
|
+
creator: creator ? { id: creator.id, displayName: creator.displayName, email: creator.email } : null,
|
|
224
|
+
caller_is_member: user ? members.some(m => m.user.id === user.id) : true,
|
|
225
|
+
};
|
|
226
|
+
});
|
|
227
|
+
return reply.send(result);
|
|
234
228
|
});
|
|
229
|
+
// Build the lightweight summary objects used by /entities?summary=true.
|
|
230
|
+
// Extracted so the per-repo and bulk endpoints share the same shape.
|
|
231
|
+
const EXCLUDED_SUMMARY_FIELDS = new Set([
|
|
232
|
+
'entity_type', 'entity_id', 'created_by', 'updated_by',
|
|
233
|
+
'description', 'content', 'context', 'rationale', 'alternatives', 'consequences',
|
|
234
|
+
'steps', 'guidelines', 'bio', 'agenda', 'summary', 'decision', 'action_items',
|
|
235
|
+
]);
|
|
236
|
+
function buildSummaries(entities, accessibleIds) {
|
|
237
|
+
return entities.map(e => {
|
|
238
|
+
const extra = Object.fromEntries(Object.entries(e.data ?? {}).filter(([k, v]) => !EXCLUDED_SUMMARY_FIELDS.has(k) && k !== 'name' && k !== 'created_at' && k !== 'updated_at' &&
|
|
239
|
+
(typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || Array.isArray(v) || v instanceof Date)));
|
|
240
|
+
return {
|
|
241
|
+
id: e.id,
|
|
242
|
+
entityType: e.entityType,
|
|
243
|
+
folder_path: e.folder_path ?? '',
|
|
244
|
+
data: {
|
|
245
|
+
name: e.data.name,
|
|
246
|
+
created_at: e.data.created_at,
|
|
247
|
+
updated_at: e.data.updated_at,
|
|
248
|
+
...extra,
|
|
249
|
+
},
|
|
250
|
+
document: { wikilinks: filterWikilinks(e.document?.wikilinks ?? [], accessibleIds) },
|
|
251
|
+
};
|
|
252
|
+
});
|
|
253
|
+
}
|
|
235
254
|
// GET /api/repos/:repo/entities — list all entities in a repo
|
|
236
255
|
// ?summary=true returns lightweight objects (no content, minimal data) for sidebar/graph
|
|
237
256
|
fastify.get('/api/repos/:repo/entities', async (req, reply) => {
|
|
238
257
|
const { repo } = req.params;
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
catch (err) {
|
|
247
|
-
return reply.status(404).send({ error: err.message });
|
|
248
|
-
}
|
|
258
|
+
// Access enforced by graphForRead \u2014 do not call workspaceManager.getGraph
|
|
259
|
+
// directly here. The audit (2026-05) flagged direct WM access from routes
|
|
260
|
+
// as the primary architectural risk; the service is the gate.
|
|
261
|
+
const graph = graphForRead(req, reply, repo);
|
|
262
|
+
if (!graph)
|
|
263
|
+
return;
|
|
249
264
|
const entities = graph.search({});
|
|
250
265
|
const accessibleIds = getAccessibleEntityIds(req, workspaceManager, authService);
|
|
251
266
|
if (req.query.summary === 'true') {
|
|
252
|
-
|
|
253
|
-
'entity_type', 'entity_id', 'created_by', 'updated_by',
|
|
254
|
-
'description', 'content', 'context', 'rationale', 'alternatives', 'consequences',
|
|
255
|
-
'steps', 'guidelines', 'bio', 'agenda', 'summary', 'decision', 'action_items',
|
|
256
|
-
]);
|
|
257
|
-
const summaries = entities.map(e => {
|
|
258
|
-
const extra = Object.fromEntries(Object.entries(e.data ?? {}).filter(([k, v]) => !EXCLUDED_SUMMARY_FIELDS.has(k) && k !== 'name' && k !== 'created_at' && k !== 'updated_at' &&
|
|
259
|
-
(typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || Array.isArray(v) || v instanceof Date)));
|
|
260
|
-
return {
|
|
261
|
-
id: e.id,
|
|
262
|
-
entityType: e.entityType,
|
|
263
|
-
data: {
|
|
264
|
-
name: e.data.name,
|
|
265
|
-
created_at: e.data.created_at,
|
|
266
|
-
updated_at: e.data.updated_at,
|
|
267
|
-
...extra,
|
|
268
|
-
},
|
|
269
|
-
document: { wikilinks: filterWikilinks(e.document?.wikilinks ?? [], accessibleIds) },
|
|
270
|
-
};
|
|
271
|
-
});
|
|
272
|
-
return reply.send(summaries);
|
|
267
|
+
return reply.send(buildSummaries(entities, accessibleIds));
|
|
273
268
|
}
|
|
274
269
|
// Full response — filter wikilinks in document
|
|
275
270
|
const filtered = entities.map(e => {
|
|
@@ -279,19 +274,57 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
279
274
|
});
|
|
280
275
|
return reply.send(filtered);
|
|
281
276
|
});
|
|
282
|
-
// GET /api/
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
277
|
+
// GET /api/workspace/entities?summary=true — bulk entity summaries for ALL
|
|
278
|
+
// accessible repos in a single round-trip. Replaces 15+ parallel
|
|
279
|
+
// /api/repos/:repo/entities?summary=true calls. Important on dev where each
|
|
280
|
+
// request has Vite middleware overhead and the browser's HTTP/1.1 6-slot
|
|
281
|
+
// limit serializes them, blocking other navigation fetches.
|
|
282
|
+
// Response shape: `{ [repoName]: EntitySummary[] }`.
|
|
283
|
+
fastify.get('/api/workspace/entities', async (req, reply) => {
|
|
284
|
+
// Mirror the per-repo endpoint's contract: only summary=true is supported
|
|
285
|
+
// here. Returning full bodies for the whole workspace would be huge and
|
|
286
|
+
// isn't a use case we have today.
|
|
287
|
+
if (req.query.summary !== 'true') {
|
|
288
|
+
return reply.status(400).send({ error: 'summary=true is required for the bulk entities endpoint' });
|
|
287
289
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
290
|
+
const repos = getAccessibleRepos(req, workspaceManager, authService);
|
|
291
|
+
// Single-pass: collect the per-repo entity arrays AND the accessible-ids
|
|
292
|
+
// set in one walk. The previous shape called graph.search({}) twice per
|
|
293
|
+
// repo (once via getAccessibleEntityIds, once in this loop), doubling
|
|
294
|
+
// disk I/O on the most-trafficked endpoint.
|
|
295
|
+
const user = reqUser(req);
|
|
296
|
+
const collectIds = user !== undefined; // unauthenticated → no wikilink filtering
|
|
297
|
+
const accessibleIds = collectIds ? new Set() : null;
|
|
298
|
+
const perRepoEntities = [];
|
|
299
|
+
for (const r of repos) {
|
|
300
|
+
let graph;
|
|
301
|
+
// Direct WM access OK — `repos` is the access-filtered list above.
|
|
302
|
+
try {
|
|
303
|
+
graph = workspaceManager.getGraph(r.name);
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
continue;
|
|
307
|
+
} // skip unmounted repos rather than failing the whole request
|
|
308
|
+
const entities = graph.search({});
|
|
309
|
+
perRepoEntities.push({ name: r.name, entities });
|
|
310
|
+
if (accessibleIds) {
|
|
311
|
+
for (const e of entities)
|
|
312
|
+
accessibleIds.add(e.id);
|
|
313
|
+
}
|
|
291
314
|
}
|
|
292
|
-
|
|
293
|
-
|
|
315
|
+
const result = {};
|
|
316
|
+
for (const { name, entities } of perRepoEntities) {
|
|
317
|
+
result[name] = buildSummaries(entities, accessibleIds);
|
|
294
318
|
}
|
|
319
|
+
return reply.send(result);
|
|
320
|
+
});
|
|
321
|
+
// GET /api/repos/:repo/entities/:type — list entities of a type
|
|
322
|
+
fastify.get('/api/repos/:repo/entities/:type', async (req, reply) => {
|
|
323
|
+
const { repo, type } = req.params;
|
|
324
|
+
// Access enforced by graphForRead (WorkspaceAccessService).
|
|
325
|
+
const graph = graphForRead(req, reply, repo);
|
|
326
|
+
if (!graph)
|
|
327
|
+
return;
|
|
295
328
|
const entities = graph.search({ entityType: type });
|
|
296
329
|
const accessibleIds = getAccessibleEntityIds(req, workspaceManager, authService);
|
|
297
330
|
const filtered = entities.map(e => {
|
|
@@ -304,17 +337,20 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
304
337
|
// GET /api/repos/:repo/entities/:type/:id — get single entity
|
|
305
338
|
fastify.get('/api/repos/:repo/entities/:type/:id', async (req, reply) => {
|
|
306
339
|
const { repo, type, id } = req.params;
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
340
|
+
// Access enforced by graphForRead (WorkspaceAccessService).
|
|
341
|
+
const graph = graphForRead(req, reply, repo);
|
|
342
|
+
if (!graph)
|
|
343
|
+
return;
|
|
344
|
+
let entity;
|
|
311
345
|
try {
|
|
312
|
-
|
|
346
|
+
entity = graph.get(type, id);
|
|
313
347
|
}
|
|
314
348
|
catch (err) {
|
|
315
|
-
return reply.status(404).send({ error: err.message });
|
|
349
|
+
return reply.status(404).send({ error: err.message ?? `Entity not found: ${type}/${id}` });
|
|
316
350
|
}
|
|
317
|
-
|
|
351
|
+
// graph.get may also return null for a missing entity without throwing
|
|
352
|
+
// (depends on the backing graph implementation). Treat that as 404 so
|
|
353
|
+
// we don't blow up on the wikilinks access below.
|
|
318
354
|
if (!entity) {
|
|
319
355
|
return reply.status(404).send({ error: `Entity not found: ${type}/${id}` });
|
|
320
356
|
}
|
|
@@ -330,25 +366,32 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
330
366
|
// GET /api/repos/:repo/entities/:type/:id/history — entity commit history
|
|
331
367
|
fastify.get('/api/repos/:repo/entities/:type/:id/history', async (req, reply) => {
|
|
332
368
|
const { repo, type, id } = req.params;
|
|
333
|
-
|
|
334
|
-
|
|
369
|
+
// Access enforced by graphForRead (WorkspaceAccessService).
|
|
370
|
+
const graph = graphForRead(req, reply, repo);
|
|
371
|
+
if (!graph)
|
|
372
|
+
return;
|
|
373
|
+
const gitService = graph.getGitService();
|
|
374
|
+
if (!gitService) {
|
|
375
|
+
return reply.send([]);
|
|
335
376
|
}
|
|
336
|
-
|
|
377
|
+
const limit = Math.min(parseInt(req.query.limit ?? '20', 10) || 20, 50);
|
|
378
|
+
// Resolve the actual on-disk path via graph.get() — it walks nested
|
|
379
|
+
// folders to find the file. The previous hardcoded `${plural}/${id}.md`
|
|
380
|
+
// missed any entry not at the legacy flat path (entries created in a
|
|
381
|
+
// sub-folder, MCP entries that landed elsewhere, etc.) and silently
|
|
382
|
+
// returned an empty history. Convert absolute path → repo-relative
|
|
383
|
+
// for git log.
|
|
384
|
+
let filePath;
|
|
337
385
|
try {
|
|
338
|
-
|
|
386
|
+
const entity = graph.get(type, id);
|
|
387
|
+
const repoRoot = graph.getRepoPath();
|
|
388
|
+
filePath = entity.path.startsWith(repoRoot + '/')
|
|
389
|
+
? entity.path.slice(repoRoot.length + 1)
|
|
390
|
+
: entity.path;
|
|
339
391
|
}
|
|
340
392
|
catch (err) {
|
|
341
393
|
return reply.status(404).send({ error: err.message });
|
|
342
394
|
}
|
|
343
|
-
const gitService = graph.getGitService();
|
|
344
|
-
if (!gitService) {
|
|
345
|
-
return reply.send([]);
|
|
346
|
-
}
|
|
347
|
-
const limit = Math.min(parseInt(req.query.limit ?? '20', 10) || 20, 50);
|
|
348
|
-
const { PLURAL_TO_SINGULAR } = await import('../../core/validation.js');
|
|
349
|
-
// Reverse lookup: singular type → plural folder name
|
|
350
|
-
const pluralDir = Object.entries(PLURAL_TO_SINGULAR).find(([, v]) => v === type)?.[0] ?? `${type}s`;
|
|
351
|
-
const filePath = `${pluralDir}/${id}.md`;
|
|
352
395
|
try {
|
|
353
396
|
const history = gitService.getFileHistory(filePath, limit);
|
|
354
397
|
return reply.send(history);
|
|
@@ -357,6 +400,59 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
357
400
|
return reply.send([]);
|
|
358
401
|
}
|
|
359
402
|
});
|
|
403
|
+
// POST /api/repos/:repo/entities/:type/:id/restore — restore an entry to
|
|
404
|
+
// its content at a past commit. Single-entity scope. Editor-or-admin role
|
|
405
|
+
// required (canWriteRepo). Returns 200 with the restore result, 404 if
|
|
406
|
+
// the entry doesn't exist at that commit, 409 PATH_CONFLICT if a different
|
|
407
|
+
// entry now occupies the target path, 403 if the caller lacks write access.
|
|
408
|
+
fastify.post('/api/repos/:repo/entities/:type/:id/restore', async (req, reply) => {
|
|
409
|
+
const { repo, type, id } = req.params;
|
|
410
|
+
const { commitHash } = req.body ?? {};
|
|
411
|
+
if (!commitHash || typeof commitHash !== 'string') {
|
|
412
|
+
return reply.status(400).send({ error: 'commitHash is required' });
|
|
413
|
+
}
|
|
414
|
+
// Access enforced by graphForWrite: 404 if no read access, 403 if no write.
|
|
415
|
+
const graph = graphForWrite(req, reply, repo);
|
|
416
|
+
if (!graph)
|
|
417
|
+
return;
|
|
418
|
+
try {
|
|
419
|
+
const result = await graph.restoreFile(type, id, commitHash, reqGitUser(req));
|
|
420
|
+
wsHub?.broadcast({
|
|
421
|
+
type: 'entity_change',
|
|
422
|
+
action: result.action === 'recreate' ? 'created' : 'updated',
|
|
423
|
+
repo,
|
|
424
|
+
entityType: type,
|
|
425
|
+
entityId: id,
|
|
426
|
+
actor: reqUser(req)?.displayName ?? 'API',
|
|
427
|
+
source: 'api',
|
|
428
|
+
timestamp: new Date().toISOString(),
|
|
429
|
+
});
|
|
430
|
+
return reply.send({
|
|
431
|
+
success: true,
|
|
432
|
+
action: result.action,
|
|
433
|
+
path: result.path,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
catch (err) {
|
|
437
|
+
// PathConflictError carries an error code distinct from generic
|
|
438
|
+
// failures so the UI can render an actionable conflict message.
|
|
439
|
+
if (err?.code === 'PATH_CONFLICT' && err?.occupiedBy) {
|
|
440
|
+
return reply.status(409).send({
|
|
441
|
+
error: 'PATH_CONFLICT',
|
|
442
|
+
path: err.path,
|
|
443
|
+
occupiedBy: err.occupiedBy,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
const message = err instanceof Error ? err.message : 'Restore failed';
|
|
447
|
+
// "not found at commit" / "did not exist at commit" → 404 so the
|
|
448
|
+
// client can show "that version isn't available" instead of a
|
|
449
|
+
// generic 500.
|
|
450
|
+
if (/not found at commit|did not exist at commit/i.test(message)) {
|
|
451
|
+
return reply.status(404).send({ error: message });
|
|
452
|
+
}
|
|
453
|
+
return reply.status(500).send({ error: message });
|
|
454
|
+
}
|
|
455
|
+
});
|
|
360
456
|
// GET /api/schema/:entityType/fields — field metadata for an entity type
|
|
361
457
|
fastify.get('/api/schema/:entityType/fields', async (req, reply) => {
|
|
362
458
|
const { entityType } = req.params;
|
|
@@ -367,17 +463,11 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
367
463
|
// GET /api/repos/:repo/search?q=&type=&tags= — search within a repo
|
|
368
464
|
fastify.get('/api/repos/:repo/search', async (req, reply) => {
|
|
369
465
|
const { repo } = req.params;
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
466
|
+
// Access enforced by graphForRead (WorkspaceAccessService).
|
|
467
|
+
const graph = graphForRead(req, reply, repo);
|
|
468
|
+
if (!graph)
|
|
469
|
+
return;
|
|
373
470
|
const { q, type, tags } = req.query;
|
|
374
|
-
let graph;
|
|
375
|
-
try {
|
|
376
|
-
graph = workspaceManager.getGraph(repo);
|
|
377
|
-
}
|
|
378
|
-
catch (err) {
|
|
379
|
-
return reply.status(404).send({ error: err.message });
|
|
380
|
-
}
|
|
381
471
|
const results = graph.search({
|
|
382
472
|
query: q,
|
|
383
473
|
entityType: type,
|
|
@@ -400,12 +490,11 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
400
490
|
const entityIds = new Set();
|
|
401
491
|
for (const repoConfig of repos) {
|
|
402
492
|
try {
|
|
493
|
+
// Direct WM access OK — `repos` came from getAccessibleRepos above.
|
|
403
494
|
const graph = workspaceManager.getGraph(repoConfig.name);
|
|
404
495
|
const entities = graph.search({});
|
|
405
496
|
for (const entity of entities) {
|
|
406
497
|
const nodeId = entity.id;
|
|
407
|
-
if (nodeId.toLowerCase().startsWith('readme'))
|
|
408
|
-
continue;
|
|
409
498
|
if (!entityIds.has(nodeId)) {
|
|
410
499
|
entityIds.add(nodeId);
|
|
411
500
|
nodes.push({
|
|
@@ -462,16 +551,13 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
462
551
|
// PUT /api/repos/:repo/entities/:type/:id — update entity (data and/or content)
|
|
463
552
|
fastify.put('/api/repos/:repo/entities/:type/:id', async (req, reply) => {
|
|
464
553
|
const { repo, type, id } = req.params;
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
catch (err) {
|
|
473
|
-
return reply.status(404).send({ error: err.message });
|
|
474
|
-
}
|
|
554
|
+
// Access enforced by graphForWrite \u2014 PUT updates an entity, write required.
|
|
555
|
+
const graph = graphForWrite(req, reply, repo);
|
|
556
|
+
if (!graph)
|
|
557
|
+
return;
|
|
558
|
+
const eagerError = checkEagerSkillPermission(req, type, req.body.data, repo, workspaceManager, authService);
|
|
559
|
+
if (eagerError)
|
|
560
|
+
return reply.status(403).send({ error: eagerError });
|
|
475
561
|
try {
|
|
476
562
|
// Optimistic concurrency: reject if entity was modified after client last fetched it
|
|
477
563
|
const ifUnmodifiedSince = req.headers['if-unmodified-since'];
|
|
@@ -491,6 +577,14 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
491
577
|
commitMessage: `Update ${type}/${id} from web UI`,
|
|
492
578
|
user: gitUser,
|
|
493
579
|
skipCommit: !!sessionManager, // defer commit if scheduler is active
|
|
580
|
+
// Skip the Yjs sync callback. Web-editor autosave PUTs carry
|
|
581
|
+
// content sourced from the live Y.Text — mirroring the just-
|
|
582
|
+
// written disk content back into the Y.Doc would race in-flight
|
|
583
|
+
// keystrokes typed during the round trip and revert them.
|
|
584
|
+
// Server-initiated writes that bypass Yjs (MCP, agent, meeting
|
|
585
|
+
// processor) leave skipYjsSync unset so onContentUpdate fires
|
|
586
|
+
// and the Y.Doc picks up their writes.
|
|
587
|
+
skipYjsSync: true,
|
|
494
588
|
});
|
|
495
589
|
if (sessionManager && gitUser) {
|
|
496
590
|
sessionManager.markDirty(repo, gitUser, type, id, updated.path);
|
|
@@ -513,16 +607,10 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
513
607
|
if (!type || !id || !newType) {
|
|
514
608
|
return reply.status(400).send({ error: 'entityType, entityId, and newType are required' });
|
|
515
609
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
try {
|
|
521
|
-
graph = workspaceManager.getGraph(repo);
|
|
522
|
-
}
|
|
523
|
-
catch (err) {
|
|
524
|
-
return reply.status(404).send({ error: err.message });
|
|
525
|
-
}
|
|
610
|
+
// Access enforced by graphForRead (WorkspaceAccessService).
|
|
611
|
+
const graph = graphForRead(req, reply, repo);
|
|
612
|
+
if (!graph)
|
|
613
|
+
return;
|
|
526
614
|
try {
|
|
527
615
|
// Flush any pending session saves before moving the file
|
|
528
616
|
if (sessionManager) {
|
|
@@ -547,45 +635,111 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
547
635
|
return reply.status(status).send({ error: err.message });
|
|
548
636
|
}
|
|
549
637
|
});
|
|
550
|
-
// POST /api/repos/:repo/
|
|
551
|
-
fastify.post('/api/repos/:repo/
|
|
552
|
-
const
|
|
553
|
-
const { entityType, entityId
|
|
554
|
-
if (!
|
|
555
|
-
return reply.status(400).send({ error: 'entityType, entityId,
|
|
638
|
+
// POST /api/repos/:repo/rename — rename an entity (changes its id/filename)
|
|
639
|
+
fastify.post('/api/repos/:repo/rename', async (req, reply) => {
|
|
640
|
+
const { repo } = req.params;
|
|
641
|
+
const { entityType: type, entityId: id, newId } = req.body ?? {};
|
|
642
|
+
if (!type || !id || !newId) {
|
|
643
|
+
return reply.status(400).send({ error: 'entityType, entityId, and newId are required' });
|
|
556
644
|
}
|
|
557
|
-
if (
|
|
558
|
-
return reply.status(400).send({ error: '
|
|
645
|
+
if (!/^[a-z0-9][a-z0-9_-]*$/.test(newId)) {
|
|
646
|
+
return reply.status(400).send({ error: 'New ID must be kebab-case (lowercase letters, numbers, hyphens, underscores)' });
|
|
559
647
|
}
|
|
560
|
-
if (
|
|
561
|
-
return reply.status(
|
|
648
|
+
if (id === newId) {
|
|
649
|
+
return reply.status(400).send({ error: 'New ID must differ from the current ID' });
|
|
562
650
|
}
|
|
563
|
-
|
|
564
|
-
|
|
651
|
+
// Access enforced by graphForWrite (WorkspaceAccessService).
|
|
652
|
+
const graph = graphForWrite(req, reply, repo);
|
|
653
|
+
if (!graph)
|
|
654
|
+
return;
|
|
655
|
+
// Workspace-wide uniqueness — ids must be unique across all folders
|
|
656
|
+
const conflict = workspaceManager.findEntityById(newId);
|
|
657
|
+
if (conflict) {
|
|
658
|
+
return reply.status(409).send({ error: `An entry with id "${newId}" already exists in folder "${conflict.repoName}".` });
|
|
565
659
|
}
|
|
566
|
-
let sourceGraph, targetGraph;
|
|
567
660
|
try {
|
|
568
|
-
|
|
569
|
-
|
|
661
|
+
// Flush pending session saves so we don't PUT to the old path after rename
|
|
662
|
+
if (sessionManager) {
|
|
663
|
+
await sessionManager.flush(repo);
|
|
664
|
+
}
|
|
665
|
+
const gitUser = reqGitUser(req);
|
|
666
|
+
const renamed = await graph.rename(type, id, newId, undefined, gitUser);
|
|
667
|
+
// Rewrite any [[wikilinks]] that still point at the old id, across
|
|
668
|
+
// every folder the caller can access. Per-repo — cross-folder links
|
|
669
|
+
// are rare but supported.
|
|
670
|
+
const refactorMsg = `Refactor wikilinks: ${id} \u2192 ${newId}`;
|
|
671
|
+
for (const other of getAccessibleRepos(req, workspaceManager, authService)) {
|
|
672
|
+
try {
|
|
673
|
+
// Direct WM access OK — iterating the access-filtered list.
|
|
674
|
+
const otherGraph = workspaceManager.getGraph(other.name);
|
|
675
|
+
await otherGraph.refactorWikilink(id, newId, refactorMsg);
|
|
676
|
+
}
|
|
677
|
+
catch { /* skip unloadable repo */ }
|
|
678
|
+
}
|
|
679
|
+
wsHub?.broadcast({
|
|
680
|
+
type: 'entity_change', action: 'deleted', repo, entityType: type, entityId: id,
|
|
681
|
+
actor: req.user?.displayName ?? 'API',
|
|
682
|
+
source: 'api', timestamp: new Date().toISOString(),
|
|
683
|
+
});
|
|
684
|
+
wsHub?.broadcast({
|
|
685
|
+
type: 'entity_change', action: 'created', repo, entityType: type, entityId: newId,
|
|
686
|
+
actor: req.user?.displayName ?? 'API',
|
|
687
|
+
source: 'api', timestamp: new Date().toISOString(),
|
|
688
|
+
});
|
|
689
|
+
return reply.send({ success: true, entity: renamed, newUrl: `/${repo}/${type}/${newId}` });
|
|
570
690
|
}
|
|
571
691
|
catch (err) {
|
|
572
|
-
|
|
692
|
+
const status = err.message?.includes('already exists') ? 409 : 400;
|
|
693
|
+
return reply.status(status).send({ error: err.message });
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
// POST /api/repos/:repo/transfer — move or copy entity to another folder
|
|
697
|
+
fastify.post('/api/repos/:repo/transfer', async (req, reply) => {
|
|
698
|
+
const sourceRepo = req.params.repo;
|
|
699
|
+
const { entityType, entityId, targetRepo, mode, targetFolderPath } = req.body ?? {};
|
|
700
|
+
if (!entityType || !entityId || !targetRepo || !mode) {
|
|
701
|
+
return reply.status(400).send({ error: 'entityType, entityId, targetRepo, and mode are required' });
|
|
573
702
|
}
|
|
703
|
+
if (sourceRepo === targetRepo) {
|
|
704
|
+
return reply.status(400).send({ error: 'Source and target folders must be different' });
|
|
705
|
+
}
|
|
706
|
+
// Access enforced by graphForWrite — both source (delete on move)
|
|
707
|
+
// and target (create) require write access. Resolved up-front so a
|
|
708
|
+
// target-side denial never surfaces only after source has been
|
|
709
|
+
// mutated. Audit (2026-05) flagged the previous opt-in pattern.
|
|
710
|
+
const sourceGraph = graphForWrite(req, reply, sourceRepo);
|
|
711
|
+
if (!sourceGraph)
|
|
712
|
+
return;
|
|
713
|
+
const targetGraph = graphForWrite(req, reply, targetRepo);
|
|
714
|
+
if (!targetGraph)
|
|
715
|
+
return;
|
|
574
716
|
try {
|
|
575
|
-
//
|
|
576
|
-
|
|
717
|
+
// Phase 0 — prevalidate. Audit (2026-05) required this: previously
|
|
718
|
+
// the route created in target, broadcast `created`, and only then
|
|
719
|
+
// tried to delete from source. A delete failure left a duplicate
|
|
720
|
+
// entry the client had already been told about.
|
|
721
|
+
if (sessionManager) {
|
|
577
722
|
await sessionManager.flush(sourceRepo);
|
|
578
|
-
|
|
723
|
+
if (targetRepo !== sourceRepo)
|
|
724
|
+
await sessionManager.flush(targetRepo);
|
|
725
|
+
}
|
|
726
|
+
// Source must exist; target must not collide. Both checks run BEFORE
|
|
727
|
+
// any mutation so we never enter phase 2 unless phase 1 will succeed.
|
|
579
728
|
const source = sourceGraph.get(entityType, entityId);
|
|
580
|
-
|
|
729
|
+
const sourcePath = source.path;
|
|
730
|
+
const sourceRawMarkdown = readFileSync(sourcePath, 'utf-8');
|
|
581
731
|
try {
|
|
582
732
|
targetGraph.get(entityType, entityId);
|
|
583
733
|
return reply.status(409).send({ error: `"${entityId}" already exists in ${targetRepo}` });
|
|
584
734
|
}
|
|
585
735
|
catch { /* not found = good */ }
|
|
586
|
-
// Create in target
|
|
587
736
|
const gitUser = reqGitUser(req);
|
|
588
737
|
const { entity_type, entity_id, ...dataFields } = source.data;
|
|
738
|
+
// Phase 1 — create in target. No broadcast yet: a half-completed
|
|
739
|
+
// move (created in target, source still present) must NOT be
|
|
740
|
+
// observable to clients, or they will display two copies until the
|
|
741
|
+
// next refresh. `targetFolderPath` lets the caller drop the entry
|
|
742
|
+
// straight into a subfolder of the target repo.
|
|
589
743
|
const created = await targetGraph.create({
|
|
590
744
|
entityType: entityType,
|
|
591
745
|
entityId,
|
|
@@ -593,21 +747,109 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
593
747
|
content: source.document?.content ?? '',
|
|
594
748
|
commitMessage: `${mode === 'move' ? 'Move' : 'Copy'} ${entityType}: ${entityId} from ${sourceRepo}`,
|
|
595
749
|
user: gitUser,
|
|
750
|
+
folderPath: targetFolderPath,
|
|
596
751
|
});
|
|
752
|
+
// Phase 2 — for `move` only: delete from source. If this fails, run
|
|
753
|
+
// a compensating delete on target so we don't leave a duplicate.
|
|
754
|
+
// The compensating delete is best-effort; if it also fails, surface
|
|
755
|
+
// a 500 with both error messages so the residue is visible (the
|
|
756
|
+
// previous behavior silently succeeded and left orphans).
|
|
757
|
+
if (mode === 'move') {
|
|
758
|
+
try {
|
|
759
|
+
await sourceGraph.delete(entityType, entityId, undefined, gitUser);
|
|
760
|
+
}
|
|
761
|
+
catch (deleteErr) {
|
|
762
|
+
const deleteMsg = deleteErr instanceof Error ? deleteErr.message : String(deleteErr);
|
|
763
|
+
let recoveryMsg = null;
|
|
764
|
+
try {
|
|
765
|
+
let restoreSource = !existsSync(sourcePath);
|
|
766
|
+
if (!restoreSource) {
|
|
767
|
+
try {
|
|
768
|
+
restoreSource = readFileSync(sourcePath, 'utf-8') !== sourceRawMarkdown;
|
|
769
|
+
}
|
|
770
|
+
catch {
|
|
771
|
+
restoreSource = true;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
if (restoreSource) {
|
|
775
|
+
mkdirSync(dirname(sourcePath), { recursive: true });
|
|
776
|
+
writeFileSync(sourcePath, sourceRawMarkdown, 'utf-8');
|
|
777
|
+
const recoveryPaths = [sourcePath];
|
|
778
|
+
const folderMarkerPath = join(dirname(sourcePath), '.gitkeep');
|
|
779
|
+
if (existsSync(folderMarkerPath)) {
|
|
780
|
+
rmSync(folderMarkerPath, { force: true });
|
|
781
|
+
recoveryPaths.push(folderMarkerPath);
|
|
782
|
+
}
|
|
783
|
+
const sourceAny = sourceGraph;
|
|
784
|
+
const recoveryGit = sourceAny.gitService;
|
|
785
|
+
const recoveryUser = gitUser ?? sourceAny.gitUser;
|
|
786
|
+
if (!recoveryGit || !recoveryUser) {
|
|
787
|
+
throw new Error('source graph git service or git user unavailable');
|
|
788
|
+
}
|
|
789
|
+
recoveryGit.commitFiles(recoveryPaths, `Recover ${entityType}: ${entityId} after failed move to ${targetRepo}`, recoveryUser);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
catch (recoveryErr) {
|
|
793
|
+
recoveryMsg = recoveryErr instanceof Error ? recoveryErr.message : String(recoveryErr);
|
|
794
|
+
log.error('[critical] cross-repo move source recovery failed', {
|
|
795
|
+
sourceRepo,
|
|
796
|
+
targetRepo,
|
|
797
|
+
entityType,
|
|
798
|
+
entityId,
|
|
799
|
+
sourcePath,
|
|
800
|
+
deleteError: deleteMsg,
|
|
801
|
+
recoveryError: recoveryMsg,
|
|
802
|
+
capturedContent: sourceRawMarkdown,
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
try {
|
|
806
|
+
await targetGraph.delete(entityType, entityId, undefined, gitUser);
|
|
807
|
+
if (recoveryMsg) {
|
|
808
|
+
return reply.status(500).send({
|
|
809
|
+
error: `Move failed: source delete of ${entityType}/${entityId} failed (${deleteMsg}). Target rolled back, but source recovery commit failed (${recoveryMsg}); manual source repo recovery is required.`,
|
|
810
|
+
code: 'MOVE_SOURCE_RECOVERY_FAILED',
|
|
811
|
+
sourceRepo,
|
|
812
|
+
targetRepo,
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
return reply.status(500).send({
|
|
816
|
+
error: `Move failed: source delete of ${entityType}/${entityId} failed (${deleteMsg}). Target rolled back; folders are back to their pre-move state.`,
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
catch (compErr) {
|
|
820
|
+
const compMsg = compErr instanceof Error ? compErr.message : String(compErr);
|
|
821
|
+
if (recoveryMsg) {
|
|
822
|
+
return reply.status(500).send({
|
|
823
|
+
error: `Move failed and could not be rolled back. Source delete failed (${deleteMsg}); source recovery commit failed (${recoveryMsg}); compensating target delete also failed (${compMsg}). Captured source content was logged for manual recovery.`,
|
|
824
|
+
code: 'MOVE_RECOVERY_AND_ROLLBACK_FAILED',
|
|
825
|
+
sourceRepo,
|
|
826
|
+
targetRepo,
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
return reply.status(500).send({
|
|
830
|
+
error: `Move failed and could not be rolled back. Source delete failed (${deleteMsg}); compensating target delete also failed (${compMsg}). Entry now exists in BOTH ${sourceRepo} and ${targetRepo} — manual cleanup required.`,
|
|
831
|
+
code: 'MOVE_DUPLICATE',
|
|
832
|
+
sourceRepo,
|
|
833
|
+
targetRepo,
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
// Phase 3 — both phases succeeded; only now broadcast. Order:
|
|
839
|
+
// source-delete first so subscribers don't briefly observe the
|
|
840
|
+
// entity in both folders during the network race.
|
|
597
841
|
const actor = req.user?.displayName ?? 'API';
|
|
598
842
|
const ts = new Date().toISOString();
|
|
599
|
-
wsHub?.broadcast({
|
|
600
|
-
type: 'entity_change', action: 'created', repo: targetRepo, entityType, entityId,
|
|
601
|
-
actor, source: 'api', timestamp: ts,
|
|
602
|
-
});
|
|
603
|
-
// Delete from source if moving
|
|
604
843
|
if (mode === 'move') {
|
|
605
|
-
await sourceGraph.delete(entityType, entityId, undefined, gitUser);
|
|
606
844
|
wsHub?.broadcast({
|
|
607
845
|
type: 'entity_change', action: 'deleted', repo: sourceRepo, entityType, entityId,
|
|
608
846
|
actor, source: 'api', timestamp: ts,
|
|
609
847
|
});
|
|
610
848
|
}
|
|
849
|
+
wsHub?.broadcast({
|
|
850
|
+
type: 'entity_change', action: 'created', repo: targetRepo, entityType, entityId,
|
|
851
|
+
actor, source: 'api', timestamp: ts,
|
|
852
|
+
});
|
|
611
853
|
return reply.send({
|
|
612
854
|
success: true,
|
|
613
855
|
entity: created,
|
|
@@ -618,19 +860,107 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
618
860
|
return reply.status(400).send({ error: err.message });
|
|
619
861
|
}
|
|
620
862
|
});
|
|
621
|
-
//
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
863
|
+
// POST /api/repos/:repo/transfer-entities — bulk move a list of entries
|
|
864
|
+
// into a single target folder (in this or another repo). The web UI's
|
|
865
|
+
// multi-select Move flow used to loop and POST per entry; this collapses
|
|
866
|
+
// to one round-trip per source repo and one commit per repo touched.
|
|
867
|
+
fastify.post('/api/repos/:repo/transfer-entities', async (req, reply) => {
|
|
868
|
+
const sourceRepo = req.params.repo;
|
|
869
|
+
const { entries, targetRepo, targetFolder } = req.body ?? {};
|
|
870
|
+
if (!Array.isArray(entries) || !targetRepo || targetFolder === undefined) {
|
|
871
|
+
return reply.status(400).send({ error: 'entries, targetRepo, and targetFolder are required' });
|
|
872
|
+
}
|
|
873
|
+
// Access enforced by graphForWrite — both source (mutate) and
|
|
874
|
+
// target (write) require write access. Resolved up-front so a
|
|
875
|
+
// target-side denial never surfaces only after source mutation.
|
|
876
|
+
const sourceGraph = graphForWrite(req, reply, sourceRepo);
|
|
877
|
+
if (!sourceGraph)
|
|
878
|
+
return;
|
|
879
|
+
const targetGraph = graphForWrite(req, reply, targetRepo);
|
|
880
|
+
if (!targetGraph)
|
|
881
|
+
return;
|
|
882
|
+
try {
|
|
883
|
+
if (sessionManager) {
|
|
884
|
+
await sessionManager.flush(sourceRepo);
|
|
885
|
+
if (targetRepo !== sourceRepo)
|
|
886
|
+
await sessionManager.flush(targetRepo);
|
|
887
|
+
}
|
|
888
|
+
const result = await sourceGraph.moveEntitiesToRepo(entries.map(e => ({ entityType: e.entityType, entityId: e.entityId })), targetGraph, targetFolder, { user: reqGitUser(req) });
|
|
889
|
+
const actor = req.user?.displayName ?? 'API';
|
|
890
|
+
const ts = new Date().toISOString();
|
|
891
|
+
wsHub?.broadcast({ type: 'repo_sync', repo: sourceRepo, source: 'api', actor, timestamp: ts });
|
|
892
|
+
if (targetRepo !== sourceRepo) {
|
|
893
|
+
wsHub?.broadcast({ type: 'repo_sync', repo: targetRepo, source: 'api', actor, timestamp: ts });
|
|
894
|
+
}
|
|
895
|
+
return reply.send({ success: true, ...result });
|
|
896
|
+
}
|
|
897
|
+
catch (err) {
|
|
898
|
+
const status = err?.name === 'InvalidFolderPathError' ? 400
|
|
899
|
+
: err.message?.includes('not found') ? 404
|
|
900
|
+
: err.message?.includes('already occupied') ? 409
|
|
901
|
+
: 400;
|
|
902
|
+
return reply.status(status).send({ error: err.message });
|
|
626
903
|
}
|
|
627
|
-
|
|
904
|
+
});
|
|
905
|
+
// POST /api/repos/:repo/transfer-folder — move an entire folder (with all
|
|
906
|
+
// descendants) into another repo in one shot. Replaces the per-entry loop
|
|
907
|
+
// the web UI used to run, which produced 2 git commits per entry; this
|
|
908
|
+
// route performs a single FS-level rename and one commit per repo.
|
|
909
|
+
fastify.post('/api/repos/:repo/transfer-folder', async (req, reply) => {
|
|
910
|
+
const sourceRepo = req.params.repo;
|
|
911
|
+
const { sourceFolder, targetRepo, targetFolder } = req.body ?? {};
|
|
912
|
+
if (!sourceFolder || !targetRepo || targetFolder === undefined) {
|
|
913
|
+
return reply.status(400).send({ error: 'sourceFolder, targetRepo, and targetFolder are required' });
|
|
914
|
+
}
|
|
915
|
+
if (sourceRepo === targetRepo && sourceFolder === targetFolder) {
|
|
916
|
+
return reply.status(400).send({ error: 'Source and target are the same' });
|
|
917
|
+
}
|
|
918
|
+
// Access enforced by graphForWrite — both source (mutate) and
|
|
919
|
+
// target (write) require write access. Resolved up-front so a
|
|
920
|
+
// target-side denial never surfaces only after source mutation.
|
|
921
|
+
const sourceGraph = graphForWrite(req, reply, sourceRepo);
|
|
922
|
+
if (!sourceGraph)
|
|
923
|
+
return;
|
|
924
|
+
const targetGraph = graphForWrite(req, reply, targetRepo);
|
|
925
|
+
if (!targetGraph)
|
|
926
|
+
return;
|
|
628
927
|
try {
|
|
629
|
-
|
|
928
|
+
// Flush any deferred saves on either side so we don't strand a
|
|
929
|
+
// pending edit in the source folder mid-move.
|
|
930
|
+
if (sessionManager) {
|
|
931
|
+
await sessionManager.flush(sourceRepo);
|
|
932
|
+
if (targetRepo !== sourceRepo)
|
|
933
|
+
await sessionManager.flush(targetRepo);
|
|
934
|
+
}
|
|
935
|
+
const stats = await sourceGraph.moveFolderToRepo(sourceFolder, targetGraph, targetFolder, { user: reqGitUser(req) });
|
|
936
|
+
const actor = req.user?.displayName ?? 'API';
|
|
937
|
+
const ts = new Date().toISOString();
|
|
938
|
+
// One refresh-trigger per repo. The receiving and sending lists
|
|
939
|
+
// both need to update; using `repo_sync` keeps the realtime
|
|
940
|
+
// handler simple — it already calls workspace.refreshEntities
|
|
941
|
+
// on this event type. Avoids emitting 2× entries-moved entity_change
|
|
942
|
+
// broadcasts (one created, one deleted) per file.
|
|
943
|
+
wsHub?.broadcast({ type: 'repo_sync', repo: sourceRepo, source: 'api', actor, timestamp: ts });
|
|
944
|
+
if (targetRepo !== sourceRepo) {
|
|
945
|
+
wsHub?.broadcast({ type: 'repo_sync', repo: targetRepo, source: 'api', actor, timestamp: ts });
|
|
946
|
+
}
|
|
947
|
+
return reply.send({ success: true, ...stats });
|
|
630
948
|
}
|
|
631
949
|
catch (err) {
|
|
632
|
-
|
|
950
|
+
const status = err?.name === 'InvalidFolderPathError' ? 400
|
|
951
|
+
: err.message?.includes('not found') ? 404
|
|
952
|
+
: err.message?.includes('not empty') ? 409
|
|
953
|
+
: 400;
|
|
954
|
+
return reply.status(status).send({ error: err.message });
|
|
633
955
|
}
|
|
956
|
+
});
|
|
957
|
+
// DELETE /api/repos/:repo/entities/:type/:id — delete an entity
|
|
958
|
+
fastify.delete('/api/repos/:repo/entities/:type/:id', async (req, reply) => {
|
|
959
|
+
const { repo, type, id } = req.params;
|
|
960
|
+
// Access enforced by graphForRead (WorkspaceAccessService).
|
|
961
|
+
const graph = graphForRead(req, reply, repo);
|
|
962
|
+
if (!graph)
|
|
963
|
+
return;
|
|
634
964
|
try {
|
|
635
965
|
await graph.delete(type, id, `Delete ${type}/${id} from web UI`, reqGitUser(req));
|
|
636
966
|
wsHub?.broadcast({
|
|
@@ -644,29 +974,63 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
644
974
|
return reply.status(400).send({ error: err.message });
|
|
645
975
|
}
|
|
646
976
|
});
|
|
647
|
-
// POST /api/repos/:repo/entities —
|
|
648
|
-
|
|
977
|
+
// POST /api/repos/:repo/entities/bulk-delete — delete many entities in
|
|
978
|
+
// one git transaction + one WS broadcast. Replaces N parallel
|
|
979
|
+
// DELETE /entities/:type/:id calls, which on a 500-entry bulk delete
|
|
980
|
+
// produced 500 git commits + 500 entity_change broadcasts and froze
|
|
981
|
+
// the UI for minutes.
|
|
982
|
+
fastify.post('/api/repos/:repo/entities/bulk-delete', async (req, reply) => {
|
|
649
983
|
const { repo } = req.params;
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
984
|
+
// Access enforced by graphForWrite (WorkspaceAccessService).
|
|
985
|
+
const graph = graphForWrite(req, reply, repo);
|
|
986
|
+
if (!graph)
|
|
987
|
+
return;
|
|
988
|
+
const body = req.body;
|
|
989
|
+
if (!body?.entities || !Array.isArray(body.entities)) {
|
|
990
|
+
return reply.status(400).send({ error: 'Expected { entities: [{ entityType, entityId }] }' });
|
|
656
991
|
}
|
|
657
|
-
let graph;
|
|
658
992
|
try {
|
|
659
|
-
|
|
993
|
+
const result = await graph.bulkDelete(body.entities.map(e => ({ entityType: e.entityType, entityId: e.entityId })), {
|
|
994
|
+
user: reqGitUser(req),
|
|
995
|
+
commitMessage: `Bulk delete ${body.entities.length} ${body.entities.length === 1 ? 'entry' : 'entries'} from web UI`,
|
|
996
|
+
});
|
|
997
|
+
if (result.deleted.length > 0) {
|
|
998
|
+
wsHub?.broadcast({
|
|
999
|
+
type: 'repo_sync', repo,
|
|
1000
|
+
actor: req.user?.displayName ?? 'API',
|
|
1001
|
+
source: 'api', timestamp: new Date().toISOString(),
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
return reply.send({
|
|
1005
|
+
deleted: result.deleted.length,
|
|
1006
|
+
errors: result.errors,
|
|
1007
|
+
});
|
|
660
1008
|
}
|
|
661
1009
|
catch (err) {
|
|
662
|
-
return reply.status(
|
|
1010
|
+
return reply.status(400).send({ error: err.message });
|
|
663
1011
|
}
|
|
1012
|
+
});
|
|
1013
|
+
// POST /api/repos/:repo/entities — create a new entity
|
|
1014
|
+
fastify.post('/api/repos/:repo/entities', async (req, reply) => {
|
|
1015
|
+
const { repo } = req.params;
|
|
1016
|
+
// Access enforced by graphForWrite (WorkspaceAccessService).
|
|
1017
|
+
const graph = graphForWrite(req, reply, repo);
|
|
1018
|
+
if (!graph)
|
|
1019
|
+
return;
|
|
1020
|
+
const { entityType, entityId, data = {}, content = '', folder_path } = req.body;
|
|
1021
|
+
if (!entityType || !entityId) {
|
|
1022
|
+
return reply.status(400).send({ error: 'entityType and entityId are required' });
|
|
1023
|
+
}
|
|
1024
|
+
const eagerError = checkEagerSkillPermission(req, entityType, data, repo, workspaceManager, authService);
|
|
1025
|
+
if (eagerError)
|
|
1026
|
+
return reply.status(403).send({ error: eagerError });
|
|
664
1027
|
try {
|
|
665
1028
|
const entity = await graph.create({
|
|
666
1029
|
entityType: entityType,
|
|
667
1030
|
entityId,
|
|
668
1031
|
data: { entity_type: entityType, entity_id: entityId, ...data },
|
|
669
1032
|
content,
|
|
1033
|
+
folderPath: folder_path,
|
|
670
1034
|
commitMessage: `Create ${entityType}/${entityId} from web UI`,
|
|
671
1035
|
user: reqGitUser(req),
|
|
672
1036
|
});
|
|
@@ -681,44 +1045,266 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
681
1045
|
return reply.status(400).send({ error: err.message });
|
|
682
1046
|
}
|
|
683
1047
|
});
|
|
1048
|
+
// POST /api/repos/:repo/move — move entry to a different folder within the
|
|
1049
|
+
// same repo. Distinct from /transfer (cross-repo) and /rename (id change).
|
|
1050
|
+
// The entry's type and id are unchanged; only its folder_path moves.
|
|
1051
|
+
fastify.post('/api/repos/:repo/move', async (req, reply) => {
|
|
1052
|
+
const { repo } = req.params;
|
|
1053
|
+
const { entityType, entityId, folder_path } = req.body ?? {};
|
|
1054
|
+
if (!entityType || !entityId || folder_path === undefined) {
|
|
1055
|
+
return reply.status(400).send({ error: 'entityType, entityId, and folder_path are required' });
|
|
1056
|
+
}
|
|
1057
|
+
// Access enforced by graphForWrite (WorkspaceAccessService).
|
|
1058
|
+
const graph = graphForWrite(req, reply, repo);
|
|
1059
|
+
if (!graph)
|
|
1060
|
+
return;
|
|
1061
|
+
try {
|
|
1062
|
+
if (sessionManager)
|
|
1063
|
+
await sessionManager.flush(repo);
|
|
1064
|
+
const moved = await graph.move(entityType, entityId, folder_path, undefined, reqGitUser(req));
|
|
1065
|
+
wsHub?.broadcast({
|
|
1066
|
+
type: 'entity_change', action: 'updated', repo, entityType, entityId,
|
|
1067
|
+
actor: req.user?.displayName ?? 'API',
|
|
1068
|
+
source: 'api', timestamp: new Date().toISOString(),
|
|
1069
|
+
});
|
|
1070
|
+
return reply.send({ success: true, entity: moved });
|
|
1071
|
+
}
|
|
1072
|
+
catch (err) {
|
|
1073
|
+
const status = err?.name === 'InvalidFolderPathError' ? 400
|
|
1074
|
+
: err.message?.includes('already occupied') ? 409
|
|
1075
|
+
: err.message?.includes('not found') ? 404
|
|
1076
|
+
: 400;
|
|
1077
|
+
return reply.status(status).send({ error: err.message });
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1080
|
+
// POST /api/repos/:repo/folders — create a folder (with .gitkeep). Auto-creates parents.
|
|
1081
|
+
// Optional display_name is written to the folder's .studiograph-folder.json
|
|
1082
|
+
// sidecar when set; otherwise the client falls back to toDisplayName(slug).
|
|
1083
|
+
fastify.post('/api/repos/:repo/folders', async (req, reply) => {
|
|
1084
|
+
const { repo } = req.params;
|
|
1085
|
+
const { path, display_name } = req.body ?? {};
|
|
1086
|
+
if (!path || typeof path !== 'string') {
|
|
1087
|
+
return reply.status(400).send({ error: 'path is required' });
|
|
1088
|
+
}
|
|
1089
|
+
// Access enforced by graphForWrite (WorkspaceAccessService).
|
|
1090
|
+
const graph = graphForWrite(req, reply, repo);
|
|
1091
|
+
if (!graph)
|
|
1092
|
+
return;
|
|
1093
|
+
try {
|
|
1094
|
+
await graph.createFolder(path, undefined, reqGitUser(req), {
|
|
1095
|
+
displayName: display_name,
|
|
1096
|
+
});
|
|
1097
|
+
return reply.status(201).send({ success: true, path });
|
|
1098
|
+
}
|
|
1099
|
+
catch (err) {
|
|
1100
|
+
return reply.status(400).send({ error: err.message });
|
|
1101
|
+
}
|
|
1102
|
+
});
|
|
1103
|
+
// PATCH /api/repos/:repo/folders — rename / move a folder within the repo,
|
|
1104
|
+
// optionally updating its display_name in the same commit. Three shapes:
|
|
1105
|
+
// { from, to } — slug-only rename / move
|
|
1106
|
+
// { from, to, display_name } — slug change + display update
|
|
1107
|
+
// { from, to: from, display_name } — display-only update (no FS move)
|
|
1108
|
+
// `display_name: ""` (or null) clears the override and reverts to the slug
|
|
1109
|
+
// fallback.
|
|
1110
|
+
fastify.patch('/api/repos/:repo/folders', async (req, reply) => {
|
|
1111
|
+
const { repo } = req.params;
|
|
1112
|
+
const { from, to, display_name } = req.body ?? {};
|
|
1113
|
+
if (!from || !to) {
|
|
1114
|
+
return reply.status(400).send({ error: 'from and to are required' });
|
|
1115
|
+
}
|
|
1116
|
+
// Access enforced by graphForWrite (WorkspaceAccessService).
|
|
1117
|
+
const graph = graphForWrite(req, reply, repo);
|
|
1118
|
+
if (!graph)
|
|
1119
|
+
return;
|
|
1120
|
+
try {
|
|
1121
|
+
if (sessionManager)
|
|
1122
|
+
await sessionManager.flush(repo);
|
|
1123
|
+
await graph.renameFolder(from, to, undefined, reqGitUser(req), {
|
|
1124
|
+
displayName: display_name === undefined ? undefined : display_name,
|
|
1125
|
+
});
|
|
1126
|
+
return reply.send({ success: true, from, to });
|
|
1127
|
+
}
|
|
1128
|
+
catch (err) {
|
|
1129
|
+
const status = err?.name === 'InvalidFolderPathError' ? 400
|
|
1130
|
+
: err.message?.includes('not found') ? 404
|
|
1131
|
+
: err.message?.includes('already exists') ? 409
|
|
1132
|
+
: 400;
|
|
1133
|
+
return reply.status(status).send({ error: err.message });
|
|
1134
|
+
}
|
|
1135
|
+
});
|
|
1136
|
+
// DELETE /api/repos/:repo/folders?path=X&recursive=true — delete a folder.
|
|
1137
|
+
// Without recursive=true, non-empty folders return 409 with blast-radius
|
|
1138
|
+
// counts so the UI can confirm before retrying.
|
|
1139
|
+
fastify.delete('/api/repos/:repo/folders', async (req, reply) => {
|
|
1140
|
+
const { repo } = req.params;
|
|
1141
|
+
const { path, recursive } = req.query;
|
|
1142
|
+
if (!path) {
|
|
1143
|
+
return reply.status(400).send({ error: 'path query parameter is required' });
|
|
1144
|
+
}
|
|
1145
|
+
// Access enforced by graphForWrite (WorkspaceAccessService).
|
|
1146
|
+
const graph = graphForWrite(req, reply, repo);
|
|
1147
|
+
if (!graph)
|
|
1148
|
+
return;
|
|
1149
|
+
try {
|
|
1150
|
+
if (sessionManager)
|
|
1151
|
+
await sessionManager.flush(repo);
|
|
1152
|
+
const result = await graph.deleteFolder(path, {
|
|
1153
|
+
recursive: recursive === 'true',
|
|
1154
|
+
user: reqGitUser(req),
|
|
1155
|
+
});
|
|
1156
|
+
return reply.send({ success: true, ...result });
|
|
1157
|
+
}
|
|
1158
|
+
catch (err) {
|
|
1159
|
+
if (err?.code === 'FOLDER_NOT_EMPTY') {
|
|
1160
|
+
return reply.status(409).send({
|
|
1161
|
+
error: err.message,
|
|
1162
|
+
code: 'FOLDER_NOT_EMPTY',
|
|
1163
|
+
blast_radius: err.blastRadius,
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
const status = err?.name === 'InvalidFolderPathError' ? 400
|
|
1167
|
+
: err.message?.includes('not found') ? 404
|
|
1168
|
+
: 400;
|
|
1169
|
+
return reply.status(status).send({ error: err.message });
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
684
1172
|
// POST /api/repos/:repo/commit — flush deferred commits (navigate-away signal)
|
|
685
1173
|
fastify.post('/api/repos/:repo/commit', async (req, reply) => {
|
|
686
1174
|
if (!sessionManager) {
|
|
687
1175
|
return reply.send({ committed: false });
|
|
688
1176
|
}
|
|
689
1177
|
const { repo } = req.params;
|
|
690
|
-
|
|
691
|
-
|
|
1178
|
+
// Access enforced via accessService.canRead \u2014 flush operates on the
|
|
1179
|
+
// SessionManager (no graph instance needed), so a boolean check is
|
|
1180
|
+
// sufficient. Pending commits were already authored by an authenticated
|
|
1181
|
+
// writer; this endpoint just flushes them on navigate-away.
|
|
1182
|
+
if (!accessService.canRead(reqUser(req), repo)) {
|
|
1183
|
+
return reply.status(404).send({ error: `Folder \u201C${repo}\u201D not found` });
|
|
692
1184
|
}
|
|
693
1185
|
const committed = await sessionManager.flush(repo);
|
|
694
1186
|
return reply.send({ committed });
|
|
695
1187
|
});
|
|
696
|
-
// POST /api/repos/:repo/import — import files
|
|
1188
|
+
// POST /api/repos/:repo/import — import files into a folder.
|
|
1189
|
+
//
|
|
1190
|
+
// Routing by extension:
|
|
1191
|
+
// .md / .txt / .csv → direct write via importFile() — no LLM
|
|
1192
|
+
// round-trip, preserves the source faithfully. Markdown frontmatter
|
|
1193
|
+
// (entity_type, entity_id) is honored when present; otherwise
|
|
1194
|
+
// sensible defaults.
|
|
1195
|
+
// .pdf + opaque binaries → asset wrapper via uploadAsset() —
|
|
1196
|
+
// creates an `asset` entity, stores the binary in R2 or the local
|
|
1197
|
+
// volume, writes a PDF text sidecar for vector indexing.
|
|
1198
|
+
// .docx → text extraction + agent capture
|
|
1199
|
+
// pipeline (until the mammoth → markdown workstream lands).
|
|
697
1200
|
fastify.post('/api/repos/:repo/import', async (req, reply) => {
|
|
698
1201
|
const { repo } = req.params;
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
1202
|
+
// Access enforced by graphForWrite — import creates entities, so write
|
|
1203
|
+
// is required. Graph captured here is reused by the inner closures so
|
|
1204
|
+
// they don't need to re-resolve (which would also bypass the gate).
|
|
1205
|
+
const repoGraph = graphForWrite(req, reply, repo);
|
|
1206
|
+
if (!repoGraph)
|
|
1207
|
+
return;
|
|
702
1208
|
const body = req.body;
|
|
703
1209
|
if (!body?.files || !Array.isArray(body.files)) {
|
|
704
1210
|
return reply.status(400).send({ error: 'Expected { files: [{ filename, content }] }' });
|
|
705
1211
|
}
|
|
1212
|
+
// Reject paths with traversal segments; importFile slugifies the rest.
|
|
1213
|
+
// Empty / undefined = repo root (existing behavior).
|
|
1214
|
+
const targetFolderPath = (body.targetFolderPath ?? '').trim();
|
|
1215
|
+
if (targetFolderPath.split('/').some(seg => seg === '..' || seg === '.')) {
|
|
1216
|
+
return reply.status(400).send({ error: 'Invalid targetFolderPath: contains traversal segments' });
|
|
1217
|
+
}
|
|
706
1218
|
const captureRepo = body.agentDecides ? undefined : repo;
|
|
1219
|
+
const gitUser = reqGitUser(req);
|
|
707
1220
|
try {
|
|
708
|
-
const { extractText, isSupportedFile } = await import('../../services/import-service.js');
|
|
1221
|
+
const { extractText, isSupportedFile, importFile } = await import('../../services/import-service.js');
|
|
1222
|
+
const { isAssetUploadExtension } = await import('../../services/assets/base.js');
|
|
1223
|
+
const { uploadAsset } = await import('../../services/asset-upload-service.js');
|
|
1224
|
+
const { extname } = await import('path');
|
|
709
1225
|
const results = [];
|
|
1226
|
+
// Direct-write extensions never need an LLM. Verbatim preservation —
|
|
1227
|
+
// markdown stays markdown, CSV stays CSV.
|
|
1228
|
+
const DIRECT_WRITE_EXTS = new Set(['.md', '.txt', '.csv']);
|
|
710
1229
|
const CONCURRENCY = 3;
|
|
711
|
-
async function
|
|
712
|
-
|
|
713
|
-
|
|
1230
|
+
async function processDirect(file) {
|
|
1231
|
+
// Reuse the access-checked graph captured in the outer scope. Do
|
|
1232
|
+
// NOT re-resolve via workspaceManager.getGraph here — the audit's
|
|
1233
|
+
// boundary is the outer graphForWrite call.
|
|
1234
|
+
const graph = repoGraph;
|
|
1235
|
+
const result = await importFile(graph, file.filename, file.content, {
|
|
1236
|
+
user: gitUser,
|
|
1237
|
+
// Caller-chosen destination subfolder; importFile combines it
|
|
1238
|
+
// with each file's own relative directory.
|
|
1239
|
+
folderPath: targetFolderPath || undefined,
|
|
1240
|
+
// Defer commits — we batch them into one transaction at the
|
|
1241
|
+
// end of the request so a 500-file import is one git commit
|
|
1242
|
+
// instead of 500. Per-file commits dominated wall time
|
|
1243
|
+
// (~50-200ms each) before this; batching collapses that to
|
|
1244
|
+
// one commit for the whole batch.
|
|
1245
|
+
skipCommit: true,
|
|
1246
|
+
});
|
|
1247
|
+
if (result.status === 'created' && result.path) {
|
|
1248
|
+
return {
|
|
1249
|
+
filename: file.filename,
|
|
1250
|
+
status: 'created',
|
|
1251
|
+
entities: [{ entityId: result.entityId, entityType: result.entityType }],
|
|
1252
|
+
entry: {
|
|
1253
|
+
operation: 'create',
|
|
1254
|
+
entityType: result.entityType,
|
|
1255
|
+
entityId: result.entityId,
|
|
1256
|
+
filePath: result.path,
|
|
1257
|
+
},
|
|
1258
|
+
sidecarPaths: result.sidecarPaths,
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
return { filename: file.filename, status: result.status, error: result.error };
|
|
1262
|
+
}
|
|
1263
|
+
async function processAsset(file) {
|
|
1264
|
+
// Reuse the outer access-checked graph; re-resolving would bypass
|
|
1265
|
+
// the architectural boundary even though the closure captures repo.
|
|
1266
|
+
const graph = repoGraph;
|
|
1267
|
+
try {
|
|
1268
|
+
const buffer = Buffer.from(file.content, 'base64');
|
|
1269
|
+
const result = await uploadAsset({
|
|
1270
|
+
graph,
|
|
1271
|
+
filename: file.filename,
|
|
1272
|
+
buffer,
|
|
1273
|
+
folderPath: targetFolderPath || undefined,
|
|
1274
|
+
user: gitUser,
|
|
1275
|
+
workspacePath: graph.getWorkspaceRoot(),
|
|
1276
|
+
// Defer commits — the dispatcher's batched commitPending
|
|
1277
|
+
// call at the bottom of the route covers every entry in
|
|
1278
|
+
// the same transaction.
|
|
1279
|
+
skipCommit: true,
|
|
1280
|
+
});
|
|
1281
|
+
return {
|
|
1282
|
+
filename: file.filename,
|
|
1283
|
+
status: 'created',
|
|
1284
|
+
entities: [{ entityId: result.entityId, entityType: result.entityType }],
|
|
1285
|
+
entry: {
|
|
1286
|
+
operation: 'create',
|
|
1287
|
+
entityType: 'asset',
|
|
1288
|
+
entityId: result.entityId,
|
|
1289
|
+
filePath: result.filePath,
|
|
1290
|
+
},
|
|
1291
|
+
// PDF text sidecars: include in extraPaths so they land in
|
|
1292
|
+
// the same commit as the entity files.
|
|
1293
|
+
sidecarPaths: result.sidecarPath ? [result.sidecarPath] : undefined,
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
catch (err) {
|
|
1297
|
+
return { filename: file.filename, status: 'error', error: err.message ?? 'Asset upload failed' };
|
|
714
1298
|
}
|
|
1299
|
+
}
|
|
1300
|
+
async function processViaAgent(file) {
|
|
715
1301
|
try {
|
|
716
1302
|
const text = await extractText(file.filename, file.content);
|
|
717
1303
|
if (!text) {
|
|
718
|
-
|
|
1304
|
+
log.warn(`[import] No text extracted from ${file.filename}`);
|
|
719
1305
|
return { filename: file.filename, status: 'error', error: 'No text content extracted' };
|
|
720
1306
|
}
|
|
721
|
-
|
|
1307
|
+
log.info(`[import] Extracted ${text.length} chars from ${file.filename}`);
|
|
722
1308
|
const captureRes = await fastify.inject({
|
|
723
1309
|
method: 'POST',
|
|
724
1310
|
url: '/api/capture',
|
|
@@ -731,7 +1317,7 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
731
1317
|
});
|
|
732
1318
|
const captureBody = JSON.parse(captureRes.body);
|
|
733
1319
|
if (captureRes.statusCode !== 200 || !captureBody.success) {
|
|
734
|
-
|
|
1320
|
+
log.warn(`[import] Capture failed for ${file.filename}: ${captureRes.statusCode} ${captureBody.error ?? JSON.stringify(captureBody).slice(0, 200)}`);
|
|
735
1321
|
return { filename: file.filename, status: 'error', error: captureBody.error ?? 'Agent processing failed' };
|
|
736
1322
|
}
|
|
737
1323
|
return { filename: file.filename, status: 'created', entities: captureBody.entities };
|
|
@@ -740,20 +1326,101 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
740
1326
|
return { filename: file.filename, status: 'error', error: err.message ?? 'Processing failed' };
|
|
741
1327
|
}
|
|
742
1328
|
}
|
|
1329
|
+
async function processFile(file) {
|
|
1330
|
+
if (!file.filename) {
|
|
1331
|
+
return { filename: file.filename, status: 'error', error: 'Missing filename' };
|
|
1332
|
+
}
|
|
1333
|
+
const ext = extname(file.filename).toLowerCase();
|
|
1334
|
+
// Direct-write text formats first — never collide with asset
|
|
1335
|
+
// detection. Then asset extensions (images, PDF, design files,
|
|
1336
|
+
// archives, …). Then the agent-extraction fallback for .docx
|
|
1337
|
+
// until the mammoth workstream lands. Anything else: reject.
|
|
1338
|
+
if (DIRECT_WRITE_EXTS.has(ext))
|
|
1339
|
+
return processDirect(file);
|
|
1340
|
+
if (isAssetUploadExtension(file.filename))
|
|
1341
|
+
return processAsset(file);
|
|
1342
|
+
if (isSupportedFile(file.filename))
|
|
1343
|
+
return processViaAgent(file);
|
|
1344
|
+
return { filename: file.filename, status: 'error', error: `Unsupported file: ${file.filename}` };
|
|
1345
|
+
}
|
|
743
1346
|
// Process files in batches of CONCURRENCY
|
|
744
1347
|
for (let i = 0; i < body.files.length; i += CONCURRENCY) {
|
|
745
1348
|
const batch = body.files.slice(i, i + CONCURRENCY);
|
|
746
1349
|
const batchResults = await Promise.all(batch.map(processFile));
|
|
747
1350
|
results.push(...batchResults);
|
|
748
1351
|
}
|
|
1352
|
+
// Direct-write path deferred git commits — flush them all in one
|
|
1353
|
+
// transaction now. Collapses N per-file commits into one batch
|
|
1354
|
+
// commit (~50ms vs N×50ms). Agent-path files committed inline via
|
|
1355
|
+
// /api/capture so they're not in this set.
|
|
1356
|
+
const pendingEntries = results
|
|
1357
|
+
.map(r => r.entry)
|
|
1358
|
+
.filter((e) => Boolean(e));
|
|
1359
|
+
// Collect every sidecar path produced by the import — these stage in
|
|
1360
|
+
// the same commit as the entity files, so the folder display name
|
|
1361
|
+
// and the entries it labels land atomically. De-dup because two
|
|
1362
|
+
// imported files inside the same Schema_OS/widgets/ would otherwise
|
|
1363
|
+
// produce duplicate sidecar entries.
|
|
1364
|
+
const sidecarPaths = Array.from(new Set(results.flatMap(r => r.sidecarPaths ?? [])));
|
|
1365
|
+
// commitPending failure used to be swallowed and the route returned
|
|
1366
|
+
// success=true even though the files were never committed. The audit
|
|
1367
|
+
// (2026-05) flagged this: a 500-file import that failed mid-commit
|
|
1368
|
+
// looked successful to the client but left N untracked files. Now
|
|
1369
|
+
// the failure propagates as 500 with a partial-failure shape so the
|
|
1370
|
+
// caller knows which entries are stranded.
|
|
1371
|
+
let commitError = null;
|
|
1372
|
+
if (pendingEntries.length > 0) {
|
|
1373
|
+
try {
|
|
1374
|
+
await repoGraph.commitPending(pendingEntries, {
|
|
1375
|
+
commitMessage: `Import ${pendingEntries.length} ${pendingEntries.length === 1 ? 'entry' : 'entries'} from web UI`,
|
|
1376
|
+
user: gitUser,
|
|
1377
|
+
extraPaths: sidecarPaths.length > 0 ? sidecarPaths : undefined,
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
catch (err) {
|
|
1381
|
+
commitError = err instanceof Error ? err.message : String(err);
|
|
1382
|
+
log.warn(`[import] batch commit failed for ${repo}: ${commitError}`);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
749
1385
|
const created = results.filter(r => r.status === 'created').length;
|
|
750
1386
|
const entityCount = results.reduce((sum, r) => sum + (r.entities?.length ?? 0), 0);
|
|
1387
|
+
// Single repo_sync broadcast for the whole import. The realtime
|
|
1388
|
+
// store's repo_sync handler refreshes the repo's entity list once
|
|
1389
|
+
// — vs. one entity_change per file, which would do N re-renders
|
|
1390
|
+
// on every connected client and freeze the UI for large imports.
|
|
1391
|
+
// Skip when commit failed: there's nothing for clients to refresh
|
|
1392
|
+
// beyond stranded uncommitted files, and the broadcast would lie
|
|
1393
|
+
// about persistence.
|
|
1394
|
+
if (created > 0 && !commitError) {
|
|
1395
|
+
wsHub?.broadcast({
|
|
1396
|
+
type: 'repo_sync', repo,
|
|
1397
|
+
actor: req.user?.displayName ?? 'API',
|
|
1398
|
+
source: 'api', timestamp: new Date().toISOString(),
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
const errors = results.filter(r => r.status === 'error').map(r => r.error);
|
|
1402
|
+
if (commitError) {
|
|
1403
|
+
// Files are on disk but uncommitted — partial failure. Surface as
|
|
1404
|
+
// 500 with details so the client doesn't show "imported N entries"
|
|
1405
|
+
// for entries that won't survive a server restart.
|
|
1406
|
+
return reply.status(500).send({
|
|
1407
|
+
success: false,
|
|
1408
|
+
error: `Import wrote ${pendingEntries.length} entries to disk but the git commit failed: ${commitError}`,
|
|
1409
|
+
commitError,
|
|
1410
|
+
uncommittedEntries: pendingEntries.length,
|
|
1411
|
+
imported: created,
|
|
1412
|
+
entityCount,
|
|
1413
|
+
skipped: errors.length,
|
|
1414
|
+
errors,
|
|
1415
|
+
results,
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
751
1418
|
return {
|
|
752
1419
|
success: true,
|
|
753
1420
|
imported: created,
|
|
754
1421
|
entityCount,
|
|
755
|
-
skipped:
|
|
756
|
-
errors
|
|
1422
|
+
skipped: errors.length,
|
|
1423
|
+
errors,
|
|
757
1424
|
results,
|
|
758
1425
|
};
|
|
759
1426
|
}
|
|
@@ -767,9 +1434,10 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
767
1434
|
// POST /api/repos/:repo/import-bookmarks — import browser bookmark exports as reference entities
|
|
768
1435
|
fastify.post('/api/repos/:repo/import-bookmarks', async (req, reply) => {
|
|
769
1436
|
const { repo } = req.params;
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
1437
|
+
// Access enforced by graphForWrite — bookmark import creates entities.
|
|
1438
|
+
const graph = graphForWrite(req, reply, repo);
|
|
1439
|
+
if (!graph)
|
|
1440
|
+
return;
|
|
773
1441
|
const body = req.body;
|
|
774
1442
|
if (!body?.content || typeof body.content !== 'string') {
|
|
775
1443
|
return reply.status(400).send({ error: 'Expected { content: string }' });
|
|
@@ -781,7 +1449,6 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
781
1449
|
error: "This doesn't appear to be a browser bookmark export. Export bookmarks from your browser's bookmark manager to generate the correct format.",
|
|
782
1450
|
});
|
|
783
1451
|
}
|
|
784
|
-
const graph = workspaceManager.getGraph(repo);
|
|
785
1452
|
const user = reqGitUser(req);
|
|
786
1453
|
const result = await importBookmarks(graph, body.content, user);
|
|
787
1454
|
// Single git commit for all created entities
|
|
@@ -806,41 +1473,73 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
806
1473
|
});
|
|
807
1474
|
}
|
|
808
1475
|
});
|
|
809
|
-
// POST /api/repos/:repo/
|
|
810
|
-
|
|
1476
|
+
// POST /api/repos/:repo/assets — generic asset upload.
|
|
1477
|
+
//
|
|
1478
|
+
// Multipart `file` field. Creates a wrapper `asset` entity whose
|
|
1479
|
+
// frontmatter points at the stored binary (R2 or local volume) via
|
|
1480
|
+
// `asset_url`. PDFs additionally get a text sidecar at
|
|
1481
|
+
// `<workspace>/.studiograph/assets-text/asset/<id>.txt` so the vector
|
|
1482
|
+
// indexer can RAG them without bloating the markdown body. See
|
|
1483
|
+
// docs/asset-architecture.md.
|
|
1484
|
+
fastify.post('/api/repos/:repo/assets', async (req, reply) => {
|
|
811
1485
|
const { repo } = req.params;
|
|
812
|
-
|
|
813
|
-
|
|
1486
|
+
// Access enforced by graphForWrite (WorkspaceAccessService).
|
|
1487
|
+
const graph = graphForWrite(req, reply, repo);
|
|
1488
|
+
if (!graph)
|
|
1489
|
+
return;
|
|
1490
|
+
// Per-route fileSize override — the global multipart limit (10MB) is
|
|
1491
|
+
// tuned for inline image embeds; full asset uploads accept up to the
|
|
1492
|
+
// 100MB cap enforced inside AssetService.upload().
|
|
1493
|
+
let file;
|
|
1494
|
+
try {
|
|
1495
|
+
file = await req.file({ limits: { fileSize: 100 * 1024 * 1024 } });
|
|
814
1496
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
1497
|
+
catch (err) {
|
|
1498
|
+
return reply.status(400).send({ error: err?.message ?? 'Invalid multipart upload' });
|
|
1499
|
+
}
|
|
1500
|
+
if (!file)
|
|
1501
|
+
return reply.status(400).send({ error: 'No file uploaded' });
|
|
1502
|
+
const buffer = await file.toBuffer();
|
|
1503
|
+
const folderPath = (req.query.folder ?? '').trim();
|
|
1504
|
+
if (folderPath.split('/').some(seg => seg === '..' || seg === '.')) {
|
|
1505
|
+
return reply.status(400).send({ error: 'Invalid folder: contains traversal segments' });
|
|
818
1506
|
}
|
|
819
1507
|
try {
|
|
820
|
-
const {
|
|
821
|
-
const
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
1508
|
+
const { uploadAsset } = await import('../../services/asset-upload-service.js');
|
|
1509
|
+
const result = await uploadAsset({
|
|
1510
|
+
graph,
|
|
1511
|
+
filename: file.filename,
|
|
1512
|
+
buffer,
|
|
1513
|
+
folderPath: folderPath || undefined,
|
|
1514
|
+
name: req.query.name?.trim() || undefined,
|
|
1515
|
+
user: reqGitUser(req),
|
|
1516
|
+
workspacePath: graph.getWorkspaceRoot(),
|
|
1517
|
+
});
|
|
1518
|
+
wsHub?.broadcast({
|
|
1519
|
+
type: 'repo_sync',
|
|
1520
|
+
repo,
|
|
1521
|
+
actor: req.user?.displayName ?? 'API',
|
|
1522
|
+
source: 'api',
|
|
1523
|
+
timestamp: new Date().toISOString(),
|
|
1524
|
+
});
|
|
1525
|
+
return reply.send({
|
|
833
1526
|
success: true,
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
1527
|
+
entity: {
|
|
1528
|
+
entityId: result.entityId,
|
|
1529
|
+
entityType: result.entityType,
|
|
1530
|
+
asset_url: result.asset_url,
|
|
1531
|
+
asset_kind: result.asset_kind,
|
|
1532
|
+
asset_size: result.asset_size,
|
|
1533
|
+
pages: result.pages,
|
|
1534
|
+
},
|
|
1535
|
+
});
|
|
838
1536
|
}
|
|
839
1537
|
catch (err) {
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
1538
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1539
|
+
const status = msg.includes('not configured') ? 501
|
|
1540
|
+
: msg.includes('Unsupported asset extension') ? 415
|
|
1541
|
+
: 400;
|
|
1542
|
+
return reply.status(status).send({ success: false, error: msg });
|
|
844
1543
|
}
|
|
845
1544
|
});
|
|
846
1545
|
}
|