studiograph 1.3.47 → 1.3.48-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -16
- package/dist/agent/agent-pool.d.ts +117 -0
- package/dist/agent/agent-pool.js +287 -0
- package/dist/agent/agent-pool.js.map +1 -0
- 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 +113 -22
- package/dist/agent/orchestrator.js +573 -181
- package/dist/agent/orchestrator.js.map +1 -1
- package/dist/agent/prompts/entity-types-section.d.ts +36 -0
- package/dist/agent/prompts/entity-types-section.js +90 -0
- package/dist/agent/prompts/entity-types-section.js.map +1 -0
- package/dist/agent/prompts/system.md +148 -69
- 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 +77 -433
- package/dist/agent/skills/interview/entity-templates.md +193 -0
- package/dist/agent/skills/interview/question-domains.md +391 -0
- package/dist/agent/skills/interview/skill.md +204 -0
- package/dist/agent/skills/schema-rules.md +54 -0
- 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 +7 -6
- package/dist/agent/tools/fs-tools.js +5 -4
- package/dist/agent/tools/fs-tools.js.map +1 -1
- package/dist/agent/tools/graph-tools.d.ts +21 -58
- package/dist/agent/tools/graph-tools.js +635 -332
- 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/ops-tools.d.ts +5 -4
- package/dist/agent/tools/ops-tools.js +130 -236
- package/dist/agent/tools/ops-tools.js.map +1 -1
- package/dist/agent/tools/permission-tools.d.ts +87 -17
- package/dist/agent/tools/permission-tools.js +233 -38
- 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.d.ts +18 -0
- package/dist/agent/tools/web-tools.js +283 -0
- package/dist/agent/tools/web-tools.js.map +1 -0
- 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/admin/audit-workspace.d.ts +23 -0
- package/dist/cli/commands/admin/audit-workspace.js +133 -0
- package/dist/cli/commands/admin/audit-workspace.js.map +1 -0
- package/dist/cli/commands/admin/audit.d.ts +21 -0
- package/dist/cli/commands/admin/audit.js +174 -0
- package/dist/cli/commands/admin/audit.js.map +1 -0
- package/dist/cli/commands/admin/backup.d.ts +54 -0
- package/dist/cli/commands/admin/backup.js +207 -0
- package/dist/cli/commands/admin/backup.js.map +1 -0
- package/dist/cli/commands/admin/folder.d.ts +11 -0
- package/dist/cli/commands/admin/folder.js +186 -0
- package/dist/cli/commands/admin/folder.js.map +1 -0
- package/dist/cli/commands/admin/index.d.ts +16 -0
- package/dist/cli/commands/admin/index.js +46 -0
- package/dist/cli/commands/admin/index.js.map +1 -0
- package/dist/cli/commands/admin/migrate-assets.d.ts +53 -0
- package/dist/cli/commands/admin/migrate-assets.js +184 -0
- package/dist/cli/commands/admin/migrate-assets.js.map +1 -0
- package/dist/cli/commands/admin/migrate.d.ts +56 -0
- package/dist/cli/commands/admin/migrate.js +263 -0
- package/dist/cli/commands/admin/migrate.js.map +1 -0
- package/dist/cli/commands/admin/restore.d.ts +24 -0
- package/dist/cli/commands/admin/restore.js +228 -0
- package/dist/cli/commands/admin/restore.js.map +1 -0
- package/dist/cli/commands/admin/secrets.d.ts +23 -0
- package/dist/cli/commands/admin/secrets.js +256 -0
- package/dist/cli/commands/admin/secrets.js.map +1 -0
- package/dist/cli/commands/admin/settings.d.ts +28 -0
- package/dist/cli/commands/admin/settings.js +284 -0
- package/dist/cli/commands/admin/settings.js.map +1 -0
- package/dist/cli/commands/admin/token.d.ts +42 -0
- package/dist/cli/commands/admin/token.js +126 -0
- package/dist/cli/commands/admin/token.js.map +1 -0
- package/dist/cli/commands/admin/transfer-ownership.d.ts +15 -0
- package/dist/cli/commands/admin/transfer-ownership.js +106 -0
- package/dist/cli/commands/admin/transfer-ownership.js.map +1 -0
- package/dist/cli/commands/admin/user.d.ts +11 -0
- package/dist/cli/commands/admin/user.js +187 -0
- package/dist/cli/commands/admin/user.js.map +1 -0
- package/dist/cli/commands/clone.d.ts +25 -3
- package/dist/cli/commands/clone.js +142 -36
- package/dist/cli/commands/clone.js.map +1 -1
- package/dist/cli/commands/commit.d.ts +1 -1
- package/dist/cli/commands/commit.js +24 -11
- package/dist/cli/commands/commit.js.map +1 -1
- package/dist/cli/commands/config.d.ts +23 -107
- package/dist/cli/commands/config.js +52 -260
- package/dist/cli/commands/config.js.map +1 -1
- package/dist/cli/commands/connector.d.ts +10 -13
- package/dist/cli/commands/connector.js +16 -58
- package/dist/cli/commands/connector.js.map +1 -1
- package/dist/cli/commands/deploy.js +215 -68
- package/dist/cli/commands/deploy.js.map +1 -1
- package/dist/cli/commands/index.js +2 -2
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/init.js +10 -30
- package/dist/cli/commands/init.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/orphans.js +14 -30
- package/dist/cli/commands/orphans.js.map +1 -1
- package/dist/cli/commands/r2.d.ts +3 -0
- package/dist/cli/commands/r2.js +223 -204
- package/dist/cli/commands/r2.js.map +1 -1
- package/dist/cli/commands/redeploy.js +65 -12
- package/dist/cli/commands/redeploy.js.map +1 -1
- package/dist/cli/commands/reset.d.ts +22 -6
- package/dist/cli/commands/reset.js +142 -63
- package/dist/cli/commands/reset.js.map +1 -1
- package/dist/cli/commands/serve.js +209 -24
- package/dist/cli/commands/serve.js.map +1 -1
- package/dist/cli/commands/sync.d.ts +1 -1
- package/dist/cli/commands/sync.js +372 -202
- package/dist/cli/commands/sync.js.map +1 -1
- package/dist/cli/crypto-bundle.d.ts +39 -0
- package/dist/cli/crypto-bundle.js +94 -0
- package/dist/cli/crypto-bundle.js.map +1 -0
- package/dist/cli/index.js +24 -35
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/railway-pin.d.ts +68 -0
- package/dist/cli/railway-pin.js +96 -0
- package/dist/cli/railway-pin.js.map +1 -0
- 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/setup-wizard.d.ts +33 -52
- package/dist/cli/setup-wizard.js +43 -68
- 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/cell-anchor.d.ts +29 -0
- package/dist/core/cell-anchor.js +53 -0
- package/dist/core/cell-anchor.js.map +1 -0
- package/dist/core/comment-anchor.d.ts +41 -0
- package/dist/core/comment-anchor.js +147 -0
- package/dist/core/comment-anchor.js.map +1 -0
- package/dist/core/commit-messages.d.ts +57 -0
- package/dist/core/commit-messages.js +85 -0
- package/dist/core/commit-messages.js.map +1 -0
- package/dist/core/embeds.d.ts +56 -0
- package/dist/core/embeds.js +103 -0
- package/dist/core/embeds.js.map +1 -0
- package/dist/core/entity-body-templates.d.ts +21 -0
- package/dist/core/entity-body-templates.js +53 -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 +140 -0
- package/dist/core/entity-types-catalog.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 +36 -0
- package/dist/core/folder-sidecar.js +91 -0
- package/dist/core/folder-sidecar.js.map +1 -0
- package/dist/core/graph.d.ts +675 -14
- package/dist/core/graph.js +2118 -314
- package/dist/core/graph.js.map +1 -1
- package/dist/core/history-diff.d.ts +35 -0
- package/dist/core/history-diff.js +114 -0
- package/dist/core/history-diff.js.map +1 -0
- package/dist/core/schema-registry.d.ts +59 -0
- package/dist/core/schema-registry.js +198 -0
- 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 +90 -0
- package/dist/core/secrets/SqliteEncryptedStore.js +598 -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 +81 -0
- package/dist/core/secrets/dual-write.js +247 -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/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/r2-structural-migration.d.ts +39 -0
- package/dist/core/secrets/r2-structural-migration.js +109 -0
- package/dist/core/secrets/r2-structural-migration.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 +598 -0
- package/dist/core/secrets/registry.js.map +1 -0
- package/dist/core/types.d.ts +77 -455
- package/dist/core/types.js +17 -3
- package/dist/core/types.js.map +1 -1
- package/dist/core/user-config.d.ts +75 -7
- package/dist/core/user-config.js +155 -7
- package/dist/core/user-config.js.map +1 -1
- package/dist/core/validation.d.ts +717 -1791
- package/dist/core/validation.js +357 -228
- package/dist/core/validation.js.map +1 -1
- package/dist/core/workspace-manager.d.ts +79 -12
- package/dist/core/workspace-manager.js +213 -73
- package/dist/core/workspace-manager.js.map +1 -1
- package/dist/core/workspace.d.ts +19 -5
- package/dist/core/workspace.js +69 -35
- package/dist/core/workspace.js.map +1 -1
- package/dist/mcp/comment-virtual-entity.d.ts +36 -0
- package/dist/mcp/comment-virtual-entity.js +71 -0
- package/dist/mcp/comment-virtual-entity.js.map +1 -0
- package/dist/mcp/connector-manager.d.ts +94 -13
- package/dist/mcp/connector-manager.js +283 -62
- package/dist/mcp/connector-manager.js.map +1 -1
- package/dist/mcp/oauth-provider.d.ts +27 -33
- package/dist/mcp/oauth-provider.js +65 -134
- package/dist/mcp/oauth-provider.js.map +1 -1
- package/dist/mcp/oauth-registry.d.ts +62 -0
- package/dist/mcp/oauth-registry.js +85 -0
- package/dist/mcp/oauth-registry.js.map +1 -0
- 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/state-signer.d.ts +41 -0
- package/dist/mcp/state-signer.js +120 -0
- package/dist/mcp/state-signer.js.map +1 -0
- package/dist/mcp/tools.d.ts +35 -2
- package/dist/mcp/tools.js +1051 -117
- 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/body-write-coordinator.d.ts +151 -0
- package/dist/server/body-write-coordinator.js +161 -0
- package/dist/server/body-write-coordinator.js.map +1 -0
- package/dist/server/cloud-billing-flow.d.ts +32 -0
- package/dist/server/cloud-billing-flow.js +75 -0
- package/dist/server/cloud-billing-flow.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 +451 -193
- 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.d.ts +10 -0
- package/dist/server/routes/asset-api.js +294 -0
- package/dist/server/routes/asset-api.js.map +1 -0
- 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.d.ts +3 -2
- package/dist/server/routes/auth-api.js +519 -38
- package/dist/server/routes/auth-api.js.map +1 -1
- package/dist/server/routes/capture-api.d.ts +18 -0
- package/dist/server/routes/capture-api.js +257 -0
- package/dist/server/routes/capture-api.js.map +1 -0
- package/dist/server/routes/chat.d.ts +4 -1
- package/dist/server/routes/chat.js +298 -88
- package/dist/server/routes/chat.js.map +1 -1
- package/dist/server/routes/comments-api.d.ts +15 -0
- package/dist/server/routes/comments-api.js +444 -0
- package/dist/server/routes/comments-api.js.map +1 -0
- 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.d.ts +17 -0
- package/dist/server/routes/connectors-api.js +410 -0
- package/dist/server/routes/connectors-api.js.map +1 -0
- 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/git-http.d.ts +3 -3
- package/dist/server/routes/git-http.js +86 -38
- 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 +105 -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 +1321 -263
- 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.d.ts +1 -2
- package/dist/server/routes/insights-api.js +149 -41
- package/dist/server/routes/insights-api.js.map +1 -1
- package/dist/server/routes/mcp.d.ts +7 -3
- package/dist/server/routes/mcp.js +22 -6
- package/dist/server/routes/mcp.js.map +1 -1
- package/dist/server/routes/permissions-api.d.ts +9 -2
- package/dist/server/routes/permissions-api.js +87 -30
- package/dist/server/routes/permissions-api.js.map +1 -1
- package/dist/server/routes/r2-api.d.ts +25 -0
- package/dist/server/routes/r2-api.js +276 -0
- package/dist/server/routes/r2-api.js.map +1 -0
- 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/url-api.d.ts +7 -0
- package/dist/server/routes/url-api.js +74 -0
- package/dist/server/routes/url-api.js.map +1 -0
- package/dist/server/routes/views-api.js +81 -7
- package/dist/server/routes/views-api.js.map +1 -1
- package/dist/server/routes/workspace-api.d.ts +2 -1
- package/dist/server/routes/workspace-api.js +124 -35
- package/dist/server/routes/workspace-api.js.map +1 -1
- package/dist/server/routes/ws.d.ts +2 -1
- package/dist/server/routes/ws.js +64 -16
- package/dist/server/routes/ws.js.map +1 -1
- package/dist/server/session-manager.d.ts +48 -5
- package/dist/server/session-manager.js +182 -15
- package/dist/server/session-manager.js.map +1 -1
- package/dist/server/ws-hub.d.ts +138 -14
- package/dist/server/ws-hub.js +0 -0
- package/dist/server/ws-hub.js.map +1 -1
- package/dist/server/yjs-manager.d.ts +285 -7
- package/dist/server/yjs-manager.js +907 -36
- package/dist/server/yjs-manager.js.map +1 -1
- package/dist/server/yjs-persistence.d.ts +165 -0
- package/dist/server/yjs-persistence.js +557 -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 +73 -0
- package/dist/services/access-control.js +146 -0
- package/dist/services/access-control.js.map +1 -0
- package/dist/services/asset-listing.d.ts +39 -0
- package/dist/services/asset-listing.js +125 -0
- package/dist/services/asset-listing.js.map +1 -0
- package/dist/services/asset-upload-service.d.ts +53 -0
- package/dist/services/asset-upload-service.js +201 -0
- package/dist/services/asset-upload-service.js.map +1 -0
- package/dist/services/assets/asset-index-service.d.ts +82 -0
- package/dist/services/assets/asset-index-service.js +167 -0
- package/dist/services/assets/asset-index-service.js.map +1 -0
- package/dist/services/assets/base.d.ts +44 -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 +69 -1
- package/dist/services/assets/index.js +125 -0
- package/dist/services/assets/index.js.map +1 -1
- package/dist/services/assets/local.d.ts +12 -1
- package/dist/services/assets/local.js +100 -1
- package/dist/services/assets/local.js.map +1 -1
- package/dist/services/assets/r2-sync.d.ts +88 -0
- package/dist/services/assets/r2-sync.js +412 -0
- package/dist/services/assets/r2-sync.js.map +1 -0
- package/dist/services/assets/r2.d.ts +34 -1
- package/dist/services/assets/r2.js +93 -1
- package/dist/services/assets/r2.js.map +1 -1
- package/dist/services/auth-service.d.ts +314 -36
- package/dist/services/auth-service.js +1126 -139
- package/dist/services/auth-service.js.map +1 -1
- package/dist/services/bookmark-import-service.d.ts +49 -0
- package/dist/services/bookmark-import-service.js +250 -0
- package/dist/services/bookmark-import-service.js.map +1 -0
- package/dist/services/cloud-inference-billing.d.ts +64 -0
- package/dist/services/cloud-inference-billing.js +313 -0
- package/dist/services/cloud-inference-billing.js.map +1 -0
- package/dist/services/comment-service.d.ts +97 -0
- package/dist/services/comment-service.js +341 -0
- package/dist/services/comment-service.js.map +1 -0
- package/dist/services/csv-service.d.ts +22 -17
- package/dist/services/csv-service.js +55 -92
- package/dist/services/csv-service.js.map +1 -1
- package/dist/services/entity-mutations.d.ts +161 -0
- package/dist/services/entity-mutations.js +296 -0
- package/dist/services/entity-mutations.js.map +1 -0
- package/dist/services/event-processor.d.ts +104 -0
- package/dist/services/event-processor.js +441 -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 +54 -0
- package/dist/services/git.js +188 -54
- package/dist/services/git.js.map +1 -1
- package/dist/services/graph-maintenance.d.ts +42 -0
- package/dist/services/graph-maintenance.js +143 -0
- package/dist/services/graph-maintenance.js.map +1 -0
- package/dist/services/import-service.d.ts +45 -2
- package/dist/services/import-service.js +304 -100
- package/dist/services/import-service.js.map +1 -1
- package/dist/services/lint-service.js +9 -16
- package/dist/services/lint-service.js.map +1 -1
- package/dist/services/markdown.d.ts +12 -1
- package/dist/services/markdown.js +71 -6
- package/dist/services/markdown.js.map +1 -1
- package/dist/services/memory-service.d.ts +22 -14
- package/dist/services/memory-service.js +53 -35
- package/dist/services/memory-service.js.map +1 -1
- package/dist/services/mention-detector.d.ts +23 -0
- package/dist/services/mention-detector.js +47 -0
- package/dist/services/mention-detector.js.map +1 -0
- package/dist/services/notification-broadcast.d.ts +32 -0
- package/dist/services/notification-broadcast.js +38 -0
- package/dist/services/notification-broadcast.js.map +1 -0
- package/dist/services/notification-service.d.ts +41 -0
- package/dist/services/notification-service.js +100 -0
- package/dist/services/notification-service.js.map +1 -0
- 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/user-person-bridge.d.ts +118 -0
- package/dist/services/user-person-bridge.js +306 -0
- package/dist/services/user-person-bridge.js.map +1 -0
- package/dist/services/vector-service.d.ts +146 -6
- package/dist/services/vector-service.js +276 -13
- 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.d.ts +17 -2
- package/dist/utils/git.js +23 -7
- 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 +12 -19
- package/dist/utils/version-checker.js +14 -104
- package/dist/utils/version-checker.js.map +1 -1
- package/dist/web/_app/immutable/assets/0.DDqq11xY.css +2 -0
- package/dist/web/_app/immutable/assets/12.DlIf1mKh.css +1 -0
- package/dist/web/_app/immutable/assets/13.7d-Q37wc.css +1 -0
- package/dist/web/_app/immutable/assets/14.CmLrbzZp.css +1 -0
- package/dist/web/_app/immutable/assets/2.CGK2ky-K.css +1 -0
- package/dist/web/_app/immutable/assets/3.CV0abekd.css +1 -0
- package/dist/web/_app/immutable/assets/4.fGsEpo95.css +1 -0
- package/dist/web/_app/immutable/assets/AgentOverlay.DqzjAOm4.css +1 -0
- package/dist/web/_app/immutable/assets/AssetManagerModal.BKJfPAPk.css +1 -0
- package/dist/web/_app/immutable/assets/CalendarView.bNewSxkH.css +1 -0
- package/dist/web/_app/immutable/assets/FolderMembersPanel.DX1oBeF_.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.DgvSjS0I.css +1 -0
- package/dist/web/_app/immutable/assets/GraphView.BJtGL9py.css +1 -0
- package/dist/web/_app/immutable/assets/KanbanView.D0QbZFd6.css +1 -0
- package/dist/web/_app/immutable/assets/Markdown.B4lW7Llh.css +1 -0
- package/dist/web/_app/immutable/assets/SettingsDialog.DYWYMFSO.css +1 -0
- package/dist/web/_app/immutable/assets/Spinner.UBfl0pab.css +1 -0
- package/dist/web/_app/immutable/assets/StopFilledAlt.clodJdp5.css +1 -0
- package/dist/web/_app/immutable/assets/TimelineView.DA-H6dvv.css +1 -0
- package/dist/web/_app/immutable/assets/WorkspaceView.BS1ildu7.css +1 -0
- package/dist/web/_app/immutable/assets/button.BlLm4Dg1.css +1 -0
- package/dist/web/_app/immutable/assets/formatting.B912lpdG.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.cqga56xT.css +1 -0
- package/dist/web/_app/immutable/assets/list-columns.DCMVmy_Q.css +1 -0
- package/dist/web/_app/immutable/assets/realtime.DDCWxn3B.css +1 -0
- package/dist/web/_app/immutable/assets/sheet.DJPuqnTg.css +1 -0
- package/dist/web/_app/immutable/assets/speech-dictation.C5CYFhS1.css +1 -0
- package/dist/web/_app/immutable/chunks/0iDDDyaZ.js +1 -0
- package/dist/web/_app/immutable/chunks/2LgrBrpS2.js +2 -0
- package/dist/web/_app/immutable/chunks/3EfPAYzi2.js +1 -0
- package/dist/web/_app/immutable/chunks/7Bu427my2.js +184 -0
- package/dist/web/_app/immutable/chunks/96h3ktuk.js +1 -0
- package/dist/web/_app/immutable/chunks/A6FJA1AF.js +1 -0
- package/dist/web/_app/immutable/chunks/B-FAwCqe2.js +1 -0
- package/dist/web/_app/immutable/chunks/B-V5eMSw2.js +1 -0
- package/dist/web/_app/immutable/chunks/B-i9_YVM.js +1 -0
- package/dist/web/_app/immutable/chunks/B15-kvCe2.js +1 -0
- package/dist/web/_app/immutable/chunks/B1KZ7zY32.js +2 -0
- package/dist/web/_app/immutable/chunks/B4gpzeQ2.js +1 -0
- package/dist/web/_app/immutable/chunks/B6Lt-qaJ2.js +1 -0
- package/dist/web/_app/immutable/chunks/B7tbGyiU2.js +1 -0
- package/dist/web/_app/immutable/chunks/BBxt8lpa.js +1 -0
- package/dist/web/_app/immutable/chunks/BCaRi2AF.js +1 -0
- package/dist/web/_app/immutable/chunks/BJ6Z7eAM.js +2 -0
- package/dist/web/_app/immutable/chunks/BM_vIwVC.js +83 -0
- package/dist/web/_app/immutable/chunks/BQULA6er.js +4 -0
- package/dist/web/_app/immutable/chunks/BQW-EDgP2.js +224 -0
- package/dist/web/_app/immutable/chunks/BR6jMpLB.js +1 -0
- package/dist/web/_app/immutable/chunks/BVnuLlVg.js +1 -0
- package/dist/web/_app/immutable/chunks/BddNmQ8j.js +1 -0
- package/dist/web/_app/immutable/chunks/BeuunHPO.js +1 -0
- package/dist/web/_app/immutable/chunks/BhTmSo4n2.js +1 -0
- package/dist/web/_app/immutable/chunks/BikW7mty2.js +1 -0
- package/dist/web/_app/immutable/chunks/BkyFvJvz.js +1 -0
- package/dist/web/_app/immutable/chunks/BlyFGPFV.js +22 -0
- package/dist/web/_app/immutable/chunks/Bo5dghSu.js +3 -0
- package/dist/web/_app/immutable/chunks/BpPyho8Y.js +3 -0
- package/dist/web/_app/immutable/chunks/BsuvyNDN2.js +2 -0
- package/dist/web/_app/immutable/chunks/BuKi0vpu2.js +7 -0
- package/dist/web/_app/immutable/chunks/BuuYnIjE.js +1 -0
- package/dist/web/_app/immutable/chunks/By4lVKNC.js +2 -0
- package/dist/web/_app/immutable/chunks/ByQGCIwO.js +5 -0
- package/dist/web/_app/immutable/chunks/ByidpUhu.js +1 -0
- package/dist/web/_app/immutable/chunks/C35TIzaB.js +1 -0
- package/dist/web/_app/immutable/chunks/C5dM5op-.js +1 -0
- package/dist/web/_app/immutable/chunks/C6Nx0jpM.js +1 -0
- package/dist/web/_app/immutable/chunks/C6pVyOEl2.js +1 -0
- package/dist/web/_app/immutable/chunks/C80GKPZ9.js +1 -0
- package/dist/web/_app/immutable/chunks/C94TJqPI.js +1 -0
- package/dist/web/_app/immutable/chunks/C9PETsiT.js +1 -0
- package/dist/web/_app/immutable/chunks/CAuAQ81a2.js +1 -0
- package/dist/web/_app/immutable/chunks/CBnzSITi2.js +1 -0
- package/dist/web/_app/immutable/chunks/CC9p2p6w.js +1 -0
- package/dist/web/_app/immutable/chunks/CCLR-AJL2.js +185 -0
- package/dist/web/_app/immutable/chunks/CD0X7XPW.js +6 -0
- package/dist/web/_app/immutable/chunks/CDTjuBSm.js +1 -0
- package/dist/web/_app/immutable/chunks/CH-KYFIm.js +64 -0
- package/dist/web/_app/immutable/chunks/CHj8fObQ2.js +9 -0
- package/dist/web/_app/immutable/chunks/CK-UnH-n2.js +1 -0
- package/dist/web/_app/immutable/chunks/CKH5BEAh.js +1 -0
- package/dist/web/_app/immutable/chunks/CPSupDJp2.js +2 -0
- package/dist/web/_app/immutable/chunks/CSUrYOis.js +1 -0
- package/dist/web/_app/immutable/chunks/CYTTTma0.js +3 -0
- package/dist/web/_app/immutable/chunks/Cbaut_us2.js +1 -0
- package/dist/web/_app/immutable/chunks/CiIpSx5n2.js +1 -0
- package/dist/web/_app/immutable/chunks/CkLfUXwy.js +1 -0
- package/dist/web/_app/immutable/chunks/CokE4L8B2.js +1 -0
- package/dist/web/_app/immutable/chunks/CpLdnbh3.js +1 -0
- package/dist/web/_app/immutable/chunks/CvDrXJk9.js +1 -0
- package/dist/web/_app/immutable/chunks/D00MFB9_2.js +1 -0
- package/dist/web/_app/immutable/chunks/D41CbXmM.js +1 -0
- package/dist/web/_app/immutable/chunks/D5Hj-MPO2.js +1 -0
- package/dist/web/_app/immutable/chunks/D6nLEmZy2.js +1 -0
- package/dist/web/_app/immutable/chunks/D71ZlQBM2.js +2 -0
- package/dist/web/_app/immutable/chunks/D9ql9vXU.js +1 -0
- package/dist/web/_app/immutable/chunks/DAXW5iLp.js +1 -0
- package/dist/web/_app/immutable/chunks/DBzQ8LyQ2.js +2 -0
- package/dist/web/_app/immutable/chunks/DIb5rAPU2.js +1 -0
- package/dist/web/_app/immutable/chunks/DOmH4G9o.js +14 -0
- package/dist/web/_app/immutable/chunks/DQMWa2t72.js +1 -0
- package/dist/web/_app/immutable/chunks/DRsswX8S.js +1 -0
- package/dist/web/_app/immutable/chunks/DTCHhxD5.js +1 -0
- package/dist/web/_app/immutable/chunks/DUwgGBxa.js +1 -0
- package/dist/web/_app/immutable/chunks/DVcy5Pp82.js +1 -0
- package/dist/web/_app/immutable/chunks/DWygwzcO.js +1 -0
- package/dist/web/_app/immutable/chunks/DYXDRmoo.js +1 -0
- package/dist/web/_app/immutable/chunks/DZ-YyRVM.js +1 -0
- package/dist/web/_app/immutable/chunks/D_HVOuHV.js +1 -0
- package/dist/web/_app/immutable/chunks/DaoOQFPb.js +1 -0
- package/dist/web/_app/immutable/chunks/DdLkNvPf.js +23 -0
- package/dist/web/_app/immutable/chunks/DiMQob8p.js +1 -0
- package/dist/web/_app/immutable/chunks/DiQJ9DLy.js +1 -0
- package/dist/web/_app/immutable/chunks/DlORZqfE.js +1 -0
- package/dist/web/_app/immutable/chunks/DmeYl7eL.js +2 -0
- package/dist/web/_app/immutable/chunks/Dng3OcRd2.js +1 -0
- package/dist/web/_app/immutable/chunks/DoKMFfJd.js +1 -0
- package/dist/web/_app/immutable/chunks/DpzG2mVI2.js +1 -0
- package/dist/web/_app/immutable/chunks/DvJSCDuZ2.js +1 -0
- package/dist/web/_app/immutable/chunks/DvPuaTCl2.js +5 -0
- package/dist/web/_app/immutable/chunks/ItgU4ei5.js +1 -0
- package/dist/web/_app/immutable/chunks/JJDMrjj62.js +1 -0
- package/dist/web/_app/immutable/chunks/OTTMDMTW.js +1 -0
- package/dist/web/_app/immutable/chunks/RmiWpCin2.js +1 -0
- package/dist/web/_app/immutable/chunks/ZTiqmKJy2.js +1 -0
- package/dist/web/_app/immutable/chunks/ahC5WoXX2.js +1 -0
- package/dist/web/_app/immutable/chunks/e3SY5t2O2.js +2692 -0
- package/dist/web/_app/immutable/chunks/g09ZrevW.js +1 -0
- package/dist/web/_app/immutable/chunks/go8N1nWD2.js +3 -0
- package/dist/web/_app/immutable/chunks/nTAmRNKw2.js +1 -0
- package/dist/web/_app/immutable/chunks/oBQOivfH2.js +1 -0
- package/dist/web/_app/immutable/chunks/oUTRxd0f.js +1 -0
- package/dist/web/_app/immutable/chunks/s_7BJJYy.js +2 -0
- package/dist/web/_app/immutable/chunks/uuFM-ygL.js +1 -0
- package/dist/web/_app/immutable/chunks/v7me-ydg.js +2 -0
- package/dist/web/_app/immutable/chunks/xihTtKlq.js +1 -0
- package/dist/web/_app/immutable/chunks/zl16E4yc2.js +1 -0
- package/dist/web/_app/immutable/entry/app.CbuUtkoM.js +2 -0
- package/dist/web/_app/immutable/entry/start.DeZyKwOT.js +1 -0
- package/dist/web/_app/immutable/nodes/0.C4PMtdx9.js +2 -0
- package/dist/web/_app/immutable/nodes/1.BwVfPLMn.js +1 -0
- package/dist/web/_app/immutable/nodes/10.DPr_SYan.js +1 -0
- package/dist/web/_app/immutable/nodes/11.CJAmEchE.js +1 -0
- package/dist/web/_app/immutable/nodes/12.ChMWXPMk.js +1 -0
- package/dist/web/_app/immutable/nodes/13.8vOKq8ue.js +1 -0
- package/dist/web/_app/immutable/nodes/14.BOSNmj9h.js +1 -0
- package/dist/web/_app/immutable/nodes/2.7N4BLwhX.js +109 -0
- package/dist/web/_app/immutable/nodes/3.Cd-tx8Ej.js +2 -0
- package/dist/web/_app/immutable/nodes/4.DtuT-syy.js +14 -0
- package/dist/web/_app/immutable/nodes/5.YwNHOrkt.js +1 -0
- package/dist/web/_app/immutable/nodes/6.BSPlYQuq.js +1 -0
- package/dist/web/_app/immutable/nodes/7.DPXzwW0H.js +1 -0
- package/dist/web/_app/immutable/nodes/8.Dhpk7w3X.js +1 -0
- package/dist/web/_app/immutable/nodes/9.Dd4iAQN5.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 +54 -19
- package/dist/web/studiograph-logomark.svg +29 -0
- package/package.json +34 -14
- 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/clear.d.ts +0 -5
- package/dist/cli/commands/clear.js +0 -36
- package/dist/cli/commands/clear.js.map +0 -1
- package/dist/cli/commands/collection.d.ts +0 -10
- package/dist/cli/commands/collection.js +0 -187
- 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/join.d.ts +0 -9
- package/dist/cli/commands/join.js +0 -255
- package/dist/cli/commands/join.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/start.d.ts +0 -7
- package/dist/cli/commands/start.js +0 -589
- package/dist/cli/commands/start.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/commands/update.d.ts +0 -8
- package/dist/cli/commands/update.js +0 -155
- package/dist/cli/commands/update.js.map +0 -1
- package/dist/cli/commands/user.d.ts +0 -7
- package/dist/cli/commands/user.js +0 -153
- package/dist/cli/commands/user.js.map +0 -1
- package/dist/cli/scaffolding.d.ts +0 -12
- package/dist/cli/scaffolding.js +0 -397
- package/dist/cli/scaffolding.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 -15
- package/dist/integrations/registry.js +0 -27
- package/dist/integrations/registry.js.map +0 -1
- package/dist/integrations/types.d.ts +0 -38
- package/dist/integrations/types.js +0 -9
- 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/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/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 -15
- package/dist/server/routes/meeting-ingest-api.js +0 -323
- 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 -814
- 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/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/insights.d.ts +0 -38
- package/dist/services/insights.js +0 -246
- package/dist/services/insights.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.DdizXOKi.css +0 -1
- package/dist/web/_app/immutable/assets/10.BUrHkYry.css +0 -1
- package/dist/web/_app/immutable/assets/2.n7-yW_Fs.css +0 -1
- package/dist/web/_app/immutable/assets/3.BtaZagnv.css +0 -1
- package/dist/web/_app/immutable/assets/4.DT7X0x5S.css +0 -1
- package/dist/web/_app/immutable/assets/5.DFeGxLYf.css +0 -1
- package/dist/web/_app/immutable/assets/6.CquhUpOu.css +0 -1
- package/dist/web/_app/immutable/assets/7.CjbDRbuG.css +0 -1
- package/dist/web/_app/immutable/assets/8.S1QdUGNM.css +0 -1
- package/dist/web/_app/immutable/assets/9.CT4xL4K3.css +0 -1
- package/dist/web/_app/immutable/assets/Copy.FS8bdfxr.css +0 -1
- package/dist/web/_app/immutable/assets/Toaster.rN8r_Hzv.css +0 -1
- package/dist/web/_app/immutable/assets/ViewToolbar.D-QW2oKe.css +0 -1
- package/dist/web/_app/immutable/assets/index.D6tMDJWk.css +0 -1
- package/dist/web/_app/immutable/chunks/37NdyH6G.js +0 -32
- package/dist/web/_app/immutable/chunks/4Hhzb6pv.js +0 -1
- package/dist/web/_app/immutable/chunks/72bZS3G9.js +0 -1
- package/dist/web/_app/immutable/chunks/7S2LGqoP.js +0 -2
- package/dist/web/_app/immutable/chunks/B-7hHz7B.js +0 -1
- package/dist/web/_app/immutable/chunks/B340SUKR.js +0 -1
- package/dist/web/_app/immutable/chunks/BB_5th5W.js +0 -3383
- package/dist/web/_app/immutable/chunks/BDoWDZaG.js +0 -1
- package/dist/web/_app/immutable/chunks/BFyl1ZHO.js +0 -1
- package/dist/web/_app/immutable/chunks/BS2rFAVq.js +0 -1
- package/dist/web/_app/immutable/chunks/BSsDC_p4.js +0 -1
- package/dist/web/_app/immutable/chunks/BTX2Jr8_.js +0 -1
- package/dist/web/_app/immutable/chunks/BWNY8zw6.js +0 -2
- package/dist/web/_app/immutable/chunks/BdPUVvAi.js +0 -1
- package/dist/web/_app/immutable/chunks/BmsJVLyy.js +0 -1
- package/dist/web/_app/immutable/chunks/BoAosqzE.js +0 -1
- package/dist/web/_app/immutable/chunks/Bs-tL5OR.js +0 -1
- package/dist/web/_app/immutable/chunks/Bsacq2UX.js +0 -1
- package/dist/web/_app/immutable/chunks/BuewkNdS.js +0 -4
- package/dist/web/_app/immutable/chunks/ByunV8un.js +0 -1
- package/dist/web/_app/immutable/chunks/C7VoTosa.js +0 -2
- package/dist/web/_app/immutable/chunks/CDV5Q7RN.js +0 -1
- package/dist/web/_app/immutable/chunks/CJ8__CWt.js +0 -1
- package/dist/web/_app/immutable/chunks/CJUKmosC.js +0 -5
- package/dist/web/_app/immutable/chunks/CSVbGg_b.js +0 -5
- package/dist/web/_app/immutable/chunks/CT6oIU-C.js +0 -1
- package/dist/web/_app/immutable/chunks/CTRxNfZk.js +0 -1
- package/dist/web/_app/immutable/chunks/CWQRQXxy.js +0 -1
- package/dist/web/_app/immutable/chunks/CXH6iFbA.js +0 -1
- 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/CdeYcEuU.js +0 -1
- package/dist/web/_app/immutable/chunks/CmCtpZ1Y.js +0 -1
- package/dist/web/_app/immutable/chunks/CqkleIqs.js +0 -1
- package/dist/web/_app/immutable/chunks/CrdV0ttd.js +0 -1
- package/dist/web/_app/immutable/chunks/CscoF-YL.js +0 -18
- package/dist/web/_app/immutable/chunks/Cx4YdFMk.js +0 -1
- package/dist/web/_app/immutable/chunks/D7ZqAKi9.js +0 -3
- package/dist/web/_app/immutable/chunks/DGbA7IXh.js +0 -2
- package/dist/web/_app/immutable/chunks/DbbB6Up5.js +0 -1
- package/dist/web/_app/immutable/chunks/DnL9f0lc.js +0 -7
- package/dist/web/_app/immutable/chunks/DtM5cEDu.js +0 -1
- package/dist/web/_app/immutable/chunks/Eqahi7_S.js +0 -2
- package/dist/web/_app/immutable/chunks/FFlLQIiS.js +0 -1
- package/dist/web/_app/immutable/chunks/O9Eb9k00.js +0 -1
- package/dist/web/_app/immutable/chunks/PPVm8Dsz.js +0 -1
- package/dist/web/_app/immutable/chunks/QF8MAmnc.js +0 -1
- package/dist/web/_app/immutable/chunks/UjzxCpxn.js +0 -1
- package/dist/web/_app/immutable/chunks/WSUKABI_.js +0 -1
- package/dist/web/_app/immutable/chunks/Y90OgS-9.js +0 -1
- package/dist/web/_app/immutable/chunks/cLyZlkXv.js +0 -1
- package/dist/web/_app/immutable/chunks/iDtAARVW.js +0 -2
- package/dist/web/_app/immutable/chunks/oC_W7W7p.js +0 -1
- package/dist/web/_app/immutable/chunks/rJdWIgh-.js +0 -1
- package/dist/web/_app/immutable/chunks/tvOsokaa.js +0 -1
- package/dist/web/_app/immutable/chunks/uLEXaPwE.js +0 -1
- package/dist/web/_app/immutable/chunks/vSaooFHB.js +0 -1
- package/dist/web/_app/immutable/entry/app.DAR-mtfg.js +0 -2
- package/dist/web/_app/immutable/entry/start.xDnyRaR1.js +0 -1
- package/dist/web/_app/immutable/nodes/0.D0aV-Zro.js +0 -2
- package/dist/web/_app/immutable/nodes/1.C7QoM8qD.js +0 -1
- package/dist/web/_app/immutable/nodes/10.DrDOAjOD.js +0 -1
- package/dist/web/_app/immutable/nodes/2.B0OgNuKW.js +0 -168
- package/dist/web/_app/immutable/nodes/3.BZ4qHGZi.js +0 -1
- package/dist/web/_app/immutable/nodes/4.hm-mxuuj.js +0 -12
- package/dist/web/_app/immutable/nodes/5.Bdbt4fjx.js +0 -4
- package/dist/web/_app/immutable/nodes/6.BXrCI40c.js +0 -2
- package/dist/web/_app/immutable/nodes/7.Chd4i9bm.js +0 -2
- package/dist/web/_app/immutable/nodes/8.B8SF-aNs.js +0 -1
- package/dist/web/_app/immutable/nodes/9.YDeu-RnO.js +0 -1
|
@@ -2,135 +2,58 @@
|
|
|
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 { isWorkspaceOwner } from '../../services/auth-service.js';
|
|
9
|
+
import { WorkspaceAccessService } from '../../services/workspace-access.js';
|
|
8
10
|
import { Workspace } from '../../core/workspace.js';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
if (!user)
|
|
35
|
-
return visible; // API key / open mode → full access
|
|
36
|
-
if (user.role === 'admin')
|
|
37
|
-
return visible;
|
|
38
|
-
const allowed = new Set(authService.getUserCollections(user.id));
|
|
39
|
-
return visible.filter(r => r.private || allowed.has(r.name));
|
|
40
|
-
}
|
|
41
|
-
/** Check if the current user can access a specific repo. */
|
|
42
|
-
function hasRepoAccess(req, repoName, workspaceManager, authService) {
|
|
43
|
-
const user = req.user;
|
|
44
|
-
const repoConfig = workspaceManager.getRepoConfig(repoName);
|
|
45
|
-
if (repoConfig && !canSeePrivateRepo(repoConfig, user))
|
|
46
|
-
return false;
|
|
47
|
-
if (!user)
|
|
48
|
-
return true; // API key / open mode
|
|
49
|
-
if (repoConfig?.private && repoConfig?.owner_id === user.id)
|
|
50
|
-
return true; // Owner of private repo
|
|
51
|
-
if (user.role === 'admin')
|
|
52
|
-
return true;
|
|
53
|
-
return authService.getUserCollections(user.id).includes(repoName);
|
|
54
|
-
}
|
|
55
|
-
/** Check if the current user is an admin. */
|
|
56
|
-
function isAdmin(req) {
|
|
57
|
-
const user = req.user;
|
|
58
|
-
if (!user)
|
|
59
|
-
return true; // API key / open mode → admin-equivalent
|
|
60
|
-
return user.role === 'admin';
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Build a set of all entity IDs visible to the current user.
|
|
64
|
-
* Used to filter wikilinks so users don't see links to entities
|
|
65
|
-
* in collections they can't access.
|
|
66
|
-
*/
|
|
67
|
-
function getAccessibleEntityIds(req, workspaceManager, authService) {
|
|
68
|
-
const user = req.user;
|
|
69
|
-
if (!user)
|
|
70
|
-
return null; // API key / open mode → no filtering
|
|
71
|
-
if (user.role === 'admin') {
|
|
72
|
-
// Admins see everything except other users' private repos
|
|
73
|
-
const allRepos = workspaceManager.getAllRepoConfigs();
|
|
74
|
-
const hasHiddenPrivate = allRepos.some(r => r.private && r.owner_id && r.owner_id !== user.id);
|
|
75
|
-
if (!hasHiddenPrivate)
|
|
76
|
-
return null; // No filtering needed
|
|
77
|
-
}
|
|
78
|
-
const repos = getAccessibleRepos(req, workspaceManager, authService);
|
|
79
|
-
const ids = new Set();
|
|
80
|
-
for (const repo of repos) {
|
|
81
|
-
try {
|
|
82
|
-
const graph = workspaceManager.getGraph(repo.name);
|
|
83
|
-
for (const e of graph.search({})) {
|
|
84
|
-
ids.add(e.id);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
catch { /* skip */ }
|
|
88
|
-
}
|
|
89
|
-
return ids;
|
|
90
|
-
}
|
|
91
|
-
/** Filter wikilinks to only those pointing to accessible entities. */
|
|
92
|
-
function filterWikilinks(wikilinks, accessibleIds) {
|
|
93
|
-
if (!accessibleIds)
|
|
94
|
-
return wikilinks;
|
|
95
|
-
return wikilinks.filter(id => accessibleIds.has(id));
|
|
96
|
-
}
|
|
97
|
-
export async function registerGraphApiRoutes(fastify, workspaceManager, authService, wsHub, sessionManager) {
|
|
98
|
-
// POST /api/repos — create a new collection (admin only, or any user for private collections)
|
|
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 { validateFolderPath } from '../../core/folder-paths.js';
|
|
15
|
+
import { StaleBaseRevisionError } from '../../core/graph.js';
|
|
16
|
+
import { checkEagerSkillPermission, filterWikilinks, getAccessibleEntityIds, getAccessibleRepos, graphForAdmin as resolveGraphForAdmin, graphForRead as resolveGraphForRead, graphForWrite as resolveGraphForWrite, isFolderAdminOrOwner, reqGitUser, reqUser, } from './graph-api-access.js';
|
|
17
|
+
import { BridgeError, assertCanChangeUserHandle, validateUserHandleInPayload } from '../../services/user-person-bridge.js';
|
|
18
|
+
import { log } from '../../utils/log.js';
|
|
19
|
+
export async function registerGraphApiRoutes(fastify, workspaceManager, authService, wsHub, sessionManager, yjsManager) {
|
|
20
|
+
// ARCHITECTURAL BOUNDARY: every per-route graph resolution flows through
|
|
21
|
+
// this service so the act of getting a graph IS the access check. Do not
|
|
22
|
+
// call `workspaceManager.getGraph(repo)` from a request-scoped handler —
|
|
23
|
+
// use `accessService.graphFor*(reqUser(req), repo)` instead. A security
|
|
24
|
+
// audit (2026-05) flagged the previous opt-in pattern (route had to
|
|
25
|
+
// remember `hasRepoAccess()` before `getGraph()`) as an architectural
|
|
26
|
+
// risk; if you find yourself reaching for the raw WM here, that's the
|
|
27
|
+
// smell the audit was about.
|
|
28
|
+
const accessService = new WorkspaceAccessService(workspaceManager, authService);
|
|
29
|
+
const graphForRead = (req, reply, repoName) => resolveGraphForRead(accessService, req, reply, repoName);
|
|
30
|
+
const graphForWrite = (req, reply, repoName) => resolveGraphForWrite(accessService, req, reply, repoName);
|
|
31
|
+
const graphForAdmin = (req, reply, repoName) => resolveGraphForAdmin(accessService, req, reply, repoName);
|
|
32
|
+
// POST /api/repos — any authenticated user can create a folder.
|
|
33
|
+
// The creator is granted folder-admin access automatically.
|
|
34
|
+
// Shared logic lives in services/folder-creation.ts so the MCP tool
|
|
35
|
+
// create_repo behaves identically.
|
|
99
36
|
fastify.post('/api/repos', async (req, reply) => {
|
|
100
37
|
const user = req.user;
|
|
101
|
-
const { name, display_name, description
|
|
102
|
-
// Non-private collections require admin
|
|
103
|
-
if (!isPrivate && !isAdmin(req)) {
|
|
104
|
-
return reply.status(403).send({ error: 'Admin access required' });
|
|
105
|
-
}
|
|
106
|
-
if (!name || !/^[a-z0-9][a-z0-9-]*$/.test(name)) {
|
|
107
|
-
return reply.status(400).send({ error: 'Collection name must be kebab-case' });
|
|
108
|
-
}
|
|
38
|
+
const { name, display_name, description } = req.body || {};
|
|
109
39
|
try {
|
|
110
|
-
const
|
|
111
|
-
const repo = await workspace.registerRepo({
|
|
112
|
-
name,
|
|
113
|
-
display_name: display_name || undefined,
|
|
114
|
-
description,
|
|
115
|
-
private: isPrivate,
|
|
116
|
-
...(isPrivate && user ? { owner_id: user.id } : {}),
|
|
117
|
-
});
|
|
118
|
-
workspaceManager.reloadConfig();
|
|
119
|
-
wsHub?.broadcast({
|
|
120
|
-
type: 'collection_created', repo: name,
|
|
121
|
-
actor: req.user?.displayName ?? 'API',
|
|
122
|
-
timestamp: new Date().toISOString(),
|
|
123
|
-
});
|
|
40
|
+
const repo = await createSharedFolder(workspaceManager, authService, { name, display_name, description }, user, wsHub);
|
|
124
41
|
return reply.status(201).send(repo);
|
|
125
42
|
}
|
|
126
43
|
catch (err) {
|
|
127
|
-
|
|
44
|
+
if (err instanceof FolderCreationError) {
|
|
45
|
+
const status = err.code === 'invalid_name' ? 400
|
|
46
|
+
: err.code === 'duplicate' ? 409
|
|
47
|
+
: 500;
|
|
48
|
+
return reply.status(status).send({ error: err.message });
|
|
49
|
+
}
|
|
50
|
+
return reply.status(500).send({ error: err?.message ?? String(err) });
|
|
128
51
|
}
|
|
129
52
|
});
|
|
130
|
-
// DELETE /api/repos/:repo —
|
|
53
|
+
// DELETE /api/repos/:repo — folder admin or workspace owner only.
|
|
131
54
|
fastify.delete('/api/repos/:repo', async (req, reply) => {
|
|
132
|
-
if (!
|
|
133
|
-
return reply.status(403).send({ error: '
|
|
55
|
+
if (!isFolderAdminOrOwner(req, req.params.repo, workspaceManager, authService)) {
|
|
56
|
+
return reply.status(403).send({ error: 'Folder admin access required' });
|
|
134
57
|
}
|
|
135
58
|
const { repo } = req.params;
|
|
136
59
|
try {
|
|
@@ -138,14 +61,14 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
138
61
|
const config = await workspace.loadConfig();
|
|
139
62
|
const repoIndex = config.repos.findIndex(r => r.name === repo);
|
|
140
63
|
if (repoIndex === -1) {
|
|
141
|
-
return reply.status(404).send({ error: `
|
|
64
|
+
return reply.status(404).send({ error: `Folder \u201C${repo}\u201D not found` });
|
|
142
65
|
}
|
|
143
66
|
const repoConfig = config.repos[repoIndex];
|
|
144
67
|
const repoPath = join(workspaceManager['workspacePath'], repoConfig.path);
|
|
145
68
|
// Remove from config
|
|
146
69
|
config.repos.splice(repoIndex, 1);
|
|
147
70
|
workspace.writeConfig(config);
|
|
148
|
-
workspace.pushWorkspaceConfig(`Remove
|
|
71
|
+
workspace.pushWorkspaceConfig(`Remove folder: ${repo}`);
|
|
149
72
|
// Delete files
|
|
150
73
|
const { rmSync } = await import('fs');
|
|
151
74
|
if (existsSync(repoPath)) {
|
|
@@ -153,7 +76,7 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
153
76
|
}
|
|
154
77
|
workspaceManager.reloadConfig();
|
|
155
78
|
wsHub?.broadcast({
|
|
156
|
-
type: '
|
|
79
|
+
type: 'folder_deleted', repo,
|
|
157
80
|
actor: req.user?.displayName ?? 'API',
|
|
158
81
|
timestamp: new Date().toISOString(),
|
|
159
82
|
});
|
|
@@ -163,16 +86,16 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
163
86
|
return reply.status(500).send({ error: err.message });
|
|
164
87
|
}
|
|
165
88
|
});
|
|
166
|
-
// PATCH /api/repos/:repo — update
|
|
89
|
+
// PATCH /api/repos/:repo — rename/update sync rules. Folder admin or workspace owner only.
|
|
167
90
|
fastify.patch('/api/repos/:repo', async (req, reply) => {
|
|
168
|
-
if (!
|
|
169
|
-
return reply.status(403).send({ error: '
|
|
91
|
+
if (!isFolderAdminOrOwner(req, req.params.repo, workspaceManager, authService)) {
|
|
92
|
+
return reply.status(403).send({ error: 'Folder admin access required' });
|
|
170
93
|
}
|
|
171
94
|
const { repo } = req.params;
|
|
172
95
|
const body = req.body || {};
|
|
173
96
|
const { name: newName, display_name: newDisplayName, sync: newSync, ...rest } = body;
|
|
174
|
-
if ('
|
|
175
|
-
return reply.status(400).send({ error: 'The
|
|
97
|
+
if ('personal' in rest) {
|
|
98
|
+
return reply.status(400).send({ error: 'The personal flag is immutable after creation' });
|
|
176
99
|
}
|
|
177
100
|
if (newName === undefined && newSync === undefined && newDisplayName === undefined) {
|
|
178
101
|
return reply.status(400).send({ error: 'Nothing to update' });
|
|
@@ -185,12 +108,12 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
185
108
|
const config = await workspace.loadConfig();
|
|
186
109
|
const repoConfig = config.repos.find(r => r.name === repo);
|
|
187
110
|
if (!repoConfig) {
|
|
188
|
-
return reply.status(404).send({ error: `
|
|
111
|
+
return reply.status(404).send({ error: `Folder \u201C${repo}\u201D not found` });
|
|
189
112
|
}
|
|
190
113
|
// Handle rename
|
|
191
114
|
if (newName !== undefined) {
|
|
192
115
|
if (config.repos.some(r => r.name === newName)) {
|
|
193
|
-
return reply.status(409).send({ error: `
|
|
116
|
+
return reply.status(409).send({ error: `Folder \u201C${newName}\u201D already exists` });
|
|
194
117
|
}
|
|
195
118
|
const wsPath = workspaceManager['workspacePath'];
|
|
196
119
|
const oldPath = join(wsPath, repoConfig.path);
|
|
@@ -209,8 +132,8 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
209
132
|
if (existsSync(markerPath)) {
|
|
210
133
|
writeFS(markerPath, JSON.stringify({ repo_name: newName }, null, 2), 'utf-8');
|
|
211
134
|
}
|
|
212
|
-
// Update
|
|
213
|
-
authService.
|
|
135
|
+
// Update folder_access to follow the rename
|
|
136
|
+
authService.renameFolder(repo, newName);
|
|
214
137
|
}
|
|
215
138
|
// Handle display name update (without rename)
|
|
216
139
|
if (newDisplayName !== undefined && newName === undefined) {
|
|
@@ -221,7 +144,7 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
221
144
|
repoConfig.sync = newSync.length > 0 ? newSync : undefined;
|
|
222
145
|
}
|
|
223
146
|
workspace.writeConfig(config);
|
|
224
|
-
workspace.pushWorkspaceConfig(newName !== undefined ? `Rename
|
|
147
|
+
workspace.pushWorkspaceConfig(newName !== undefined ? `Rename folder: ${repo} → ${newName}` : `Update folder: ${repo}`);
|
|
225
148
|
workspaceManager.reloadConfig();
|
|
226
149
|
return reply.send(repoConfig);
|
|
227
150
|
}
|
|
@@ -229,50 +152,124 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
229
152
|
return reply.status(500).send({ error: err.message });
|
|
230
153
|
}
|
|
231
154
|
});
|
|
232
|
-
// GET /api/repos — list accessible repos (re-reads config to pick up CLI changes)
|
|
155
|
+
// GET /api/repos — list accessible repos (re-reads config to pick up CLI changes).
|
|
156
|
+
// Augments each repo with the caller's folder role ('admin' | 'editor'),
|
|
157
|
+
// member_count, entity_types (distinct types present), and folders (the
|
|
158
|
+
// flat list of nested-folder paths in the repo, used by the sidebar to
|
|
159
|
+
// render the active-path expansion).
|
|
233
160
|
fastify.get('/api/repos', async (req, reply) => {
|
|
234
161
|
workspaceManager.reloadConfig();
|
|
235
162
|
const repos = getAccessibleRepos(req, workspaceManager, authService);
|
|
236
|
-
|
|
163
|
+
const user = req.user;
|
|
164
|
+
const enriched = repos.map(r => {
|
|
165
|
+
const memberCount = authService.getFolderMembers(r.name).length;
|
|
166
|
+
const callerRole = user ? authService.getFolderRole(user.id, r.name) : null;
|
|
167
|
+
let entityTypes = [];
|
|
168
|
+
// `folders` carries the per-folder display_name override (when one
|
|
169
|
+
// exists in the folder's .studiograph-folder.json sidecar); the client
|
|
170
|
+
// falls back to toDisplayName(slug) when display_name is absent.
|
|
171
|
+
let folders = [];
|
|
172
|
+
try {
|
|
173
|
+
// Direct WM access OK here — `repos` came from getAccessibleRepos
|
|
174
|
+
// above, so every iteration is over a folder the caller can read.
|
|
175
|
+
const graph = workspaceManager.getGraph(r.name);
|
|
176
|
+
entityTypes = graph.listEntityTypes();
|
|
177
|
+
folders = graph.listFolders().map(f => {
|
|
178
|
+
const entry = { path: f.path };
|
|
179
|
+
if (f.display_name)
|
|
180
|
+
entry.display_name = f.display_name;
|
|
181
|
+
if (f.id)
|
|
182
|
+
entry.id = f.id;
|
|
183
|
+
return entry;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
catch { /* repo not mounted */ }
|
|
187
|
+
return {
|
|
188
|
+
...r,
|
|
189
|
+
caller_role: callerRole,
|
|
190
|
+
member_count: memberCount,
|
|
191
|
+
entity_types: entityTypes,
|
|
192
|
+
folders,
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
return reply.send(enriched);
|
|
196
|
+
});
|
|
197
|
+
// GET /api/admin/folders — workspace-owner-only metadata list of every folder
|
|
198
|
+
// (name, display_name, creator, member count). No content access implied —
|
|
199
|
+
// owners still need to join a folder to read its entries.
|
|
200
|
+
fastify.get('/api/admin/folders', async (req, reply) => {
|
|
201
|
+
const user = req.user;
|
|
202
|
+
if (user && !isWorkspaceOwner(user.role)) {
|
|
203
|
+
return reply.status(403).send({ error: 'Workspace owner access required' });
|
|
204
|
+
}
|
|
205
|
+
workspaceManager.reloadConfig();
|
|
206
|
+
const all = workspaceManager.getAllRepoConfigs();
|
|
207
|
+
const result = all.map(r => {
|
|
208
|
+
const members = authService.getFolderMembers(r.name);
|
|
209
|
+
const admins = members.filter(m => m.role === 'admin');
|
|
210
|
+
const creator = r.owner_id
|
|
211
|
+
? members.find(m => m.user.id === r.owner_id)?.user
|
|
212
|
+
: admins[0]?.user;
|
|
213
|
+
let entryCount = 0;
|
|
214
|
+
// Direct WM access OK — this endpoint is owner-only (gated above) and
|
|
215
|
+
// returns metadata for every folder the workspace owner needs to see
|
|
216
|
+
// for admin purposes. No content is leaked: only entry counts.
|
|
217
|
+
try {
|
|
218
|
+
entryCount = workspaceManager.getGraph(r.name).search({}).length;
|
|
219
|
+
}
|
|
220
|
+
catch { /* skip */ }
|
|
221
|
+
return {
|
|
222
|
+
name: r.name,
|
|
223
|
+
display_name: r.display_name ?? r.name,
|
|
224
|
+
description: r.description,
|
|
225
|
+
member_count: members.length,
|
|
226
|
+
admin_count: admins.length,
|
|
227
|
+
entry_count: entryCount,
|
|
228
|
+
creator: creator ? { id: creator.id, displayName: creator.displayName, email: creator.email } : null,
|
|
229
|
+
caller_is_member: user ? members.some(m => m.user.id === user.id) : true,
|
|
230
|
+
};
|
|
231
|
+
});
|
|
232
|
+
return reply.send(result);
|
|
237
233
|
});
|
|
234
|
+
// Build the lightweight summary objects used by /entities?summary=true.
|
|
235
|
+
// Extracted so the per-repo and bulk endpoints share the same shape.
|
|
236
|
+
const EXCLUDED_SUMMARY_FIELDS = new Set([
|
|
237
|
+
'entity_type', 'entity_id', 'created_by', 'updated_by',
|
|
238
|
+
'description', 'content', 'context', 'rationale', 'alternatives', 'consequences',
|
|
239
|
+
'steps', 'guidelines', 'bio', 'agenda', 'summary', 'decision', 'action_items',
|
|
240
|
+
]);
|
|
241
|
+
function buildSummaries(entities, accessibleIds) {
|
|
242
|
+
return entities.map(e => {
|
|
243
|
+
const extra = Object.fromEntries(Object.entries(e.data ?? {}).filter(([k, v]) => !EXCLUDED_SUMMARY_FIELDS.has(k) && k !== 'name' && k !== 'created_at' && k !== 'updated_at' &&
|
|
244
|
+
(typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || Array.isArray(v) || v instanceof Date)));
|
|
245
|
+
return {
|
|
246
|
+
id: e.id,
|
|
247
|
+
entityType: e.entityType,
|
|
248
|
+
folder_path: e.folder_path ?? '',
|
|
249
|
+
data: {
|
|
250
|
+
name: e.data.name,
|
|
251
|
+
created_at: e.data.created_at,
|
|
252
|
+
updated_at: e.data.updated_at,
|
|
253
|
+
...extra,
|
|
254
|
+
},
|
|
255
|
+
document: { wikilinks: filterWikilinks(e.document?.wikilinks ?? [], accessibleIds) },
|
|
256
|
+
};
|
|
257
|
+
});
|
|
258
|
+
}
|
|
238
259
|
// GET /api/repos/:repo/entities — list all entities in a repo
|
|
239
260
|
// ?summary=true returns lightweight objects (no content, minimal data) for sidebar/graph
|
|
240
261
|
fastify.get('/api/repos/:repo/entities', async (req, reply) => {
|
|
241
262
|
const { repo } = req.params;
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
catch (err) {
|
|
250
|
-
return reply.status(404).send({ error: err.message });
|
|
251
|
-
}
|
|
263
|
+
// Access enforced by graphForRead \u2014 do not call workspaceManager.getGraph
|
|
264
|
+
// directly here. The audit (2026-05) flagged direct WM access from routes
|
|
265
|
+
// as the primary architectural risk; the service is the gate.
|
|
266
|
+
const graph = graphForRead(req, reply, repo);
|
|
267
|
+
if (!graph)
|
|
268
|
+
return;
|
|
252
269
|
const entities = graph.search({});
|
|
253
270
|
const accessibleIds = getAccessibleEntityIds(req, workspaceManager, authService);
|
|
254
271
|
if (req.query.summary === 'true') {
|
|
255
|
-
|
|
256
|
-
'entity_type', 'entity_id', 'created_by', 'updated_by',
|
|
257
|
-
'description', 'content', 'context', 'rationale', 'alternatives', 'consequences',
|
|
258
|
-
'steps', 'guidelines', 'bio', 'agenda', 'summary', 'decision', 'action_items',
|
|
259
|
-
]);
|
|
260
|
-
const summaries = entities.map(e => {
|
|
261
|
-
const extra = Object.fromEntries(Object.entries(e.data ?? {}).filter(([k, v]) => !EXCLUDED_SUMMARY_FIELDS.has(k) && k !== 'name' && k !== 'created_at' && k !== 'updated_at' &&
|
|
262
|
-
(typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || Array.isArray(v))));
|
|
263
|
-
return {
|
|
264
|
-
id: e.id,
|
|
265
|
-
entityType: e.entityType,
|
|
266
|
-
data: {
|
|
267
|
-
name: e.data.name,
|
|
268
|
-
created_at: e.data.created_at,
|
|
269
|
-
updated_at: e.data.updated_at,
|
|
270
|
-
...extra,
|
|
271
|
-
},
|
|
272
|
-
document: { wikilinks: filterWikilinks(e.document?.wikilinks ?? [], accessibleIds) },
|
|
273
|
-
};
|
|
274
|
-
});
|
|
275
|
-
return reply.send(summaries);
|
|
272
|
+
return reply.send(buildSummaries(entities, accessibleIds));
|
|
276
273
|
}
|
|
277
274
|
// Full response — filter wikilinks in document
|
|
278
275
|
const filtered = entities.map(e => {
|
|
@@ -282,19 +279,57 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
282
279
|
});
|
|
283
280
|
return reply.send(filtered);
|
|
284
281
|
});
|
|
285
|
-
// GET /api/
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
282
|
+
// GET /api/workspace/entities?summary=true — bulk entity summaries for ALL
|
|
283
|
+
// accessible repos in a single round-trip. Replaces 15+ parallel
|
|
284
|
+
// /api/repos/:repo/entities?summary=true calls. Important on dev where each
|
|
285
|
+
// request has Vite middleware overhead and the browser's HTTP/1.1 6-slot
|
|
286
|
+
// limit serializes them, blocking other navigation fetches.
|
|
287
|
+
// Response shape: `{ [repoName]: EntitySummary[] }`.
|
|
288
|
+
fastify.get('/api/workspace/entities', async (req, reply) => {
|
|
289
|
+
// Mirror the per-repo endpoint's contract: only summary=true is supported
|
|
290
|
+
// here. Returning full bodies for the whole workspace would be huge and
|
|
291
|
+
// isn't a use case we have today.
|
|
292
|
+
if (req.query.summary !== 'true') {
|
|
293
|
+
return reply.status(400).send({ error: 'summary=true is required for the bulk entities endpoint' });
|
|
290
294
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
295
|
+
const repos = getAccessibleRepos(req, workspaceManager, authService);
|
|
296
|
+
// Single-pass: collect the per-repo entity arrays AND the accessible-ids
|
|
297
|
+
// set in one walk. The previous shape called graph.search({}) twice per
|
|
298
|
+
// repo (once via getAccessibleEntityIds, once in this loop), doubling
|
|
299
|
+
// disk I/O on the most-trafficked endpoint.
|
|
300
|
+
const user = reqUser(req);
|
|
301
|
+
const collectIds = user !== undefined; // unauthenticated → no wikilink filtering
|
|
302
|
+
const accessibleIds = collectIds ? new Set() : null;
|
|
303
|
+
const perRepoEntities = [];
|
|
304
|
+
for (const r of repos) {
|
|
305
|
+
let graph;
|
|
306
|
+
// Direct WM access OK — `repos` is the access-filtered list above.
|
|
307
|
+
try {
|
|
308
|
+
graph = workspaceManager.getGraph(r.name);
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
continue;
|
|
312
|
+
} // skip unmounted repos rather than failing the whole request
|
|
313
|
+
const entities = graph.search({});
|
|
314
|
+
perRepoEntities.push({ name: r.name, entities });
|
|
315
|
+
if (accessibleIds) {
|
|
316
|
+
for (const e of entities)
|
|
317
|
+
accessibleIds.add(e.id);
|
|
318
|
+
}
|
|
294
319
|
}
|
|
295
|
-
|
|
296
|
-
|
|
320
|
+
const result = {};
|
|
321
|
+
for (const { name, entities } of perRepoEntities) {
|
|
322
|
+
result[name] = buildSummaries(entities, accessibleIds);
|
|
297
323
|
}
|
|
324
|
+
return reply.send(result);
|
|
325
|
+
});
|
|
326
|
+
// GET /api/repos/:repo/entities/:type — list entities of a type
|
|
327
|
+
fastify.get('/api/repos/:repo/entities/:type', async (req, reply) => {
|
|
328
|
+
const { repo, type } = req.params;
|
|
329
|
+
// Access enforced by graphForRead (WorkspaceAccessService).
|
|
330
|
+
const graph = graphForRead(req, reply, repo);
|
|
331
|
+
if (!graph)
|
|
332
|
+
return;
|
|
298
333
|
const entities = graph.search({ entityType: type });
|
|
299
334
|
const accessibleIds = getAccessibleEntityIds(req, workspaceManager, authService);
|
|
300
335
|
const filtered = entities.map(e => {
|
|
@@ -307,17 +342,20 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
307
342
|
// GET /api/repos/:repo/entities/:type/:id — get single entity
|
|
308
343
|
fastify.get('/api/repos/:repo/entities/:type/:id', async (req, reply) => {
|
|
309
344
|
const { repo, type, id } = req.params;
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
345
|
+
// Access enforced by graphForRead (WorkspaceAccessService).
|
|
346
|
+
const graph = graphForRead(req, reply, repo);
|
|
347
|
+
if (!graph)
|
|
348
|
+
return;
|
|
349
|
+
let entity;
|
|
314
350
|
try {
|
|
315
|
-
|
|
351
|
+
entity = graph.get(type, id);
|
|
316
352
|
}
|
|
317
353
|
catch (err) {
|
|
318
|
-
return reply.status(404).send({ error: err.message });
|
|
354
|
+
return reply.status(404).send({ error: err.message ?? `Entity not found: ${type}/${id}` });
|
|
319
355
|
}
|
|
320
|
-
|
|
356
|
+
// graph.get may also return null for a missing entity without throwing
|
|
357
|
+
// (depends on the backing graph implementation). Treat that as 404 so
|
|
358
|
+
// we don't blow up on the wikilinks access below.
|
|
321
359
|
if (!entity) {
|
|
322
360
|
return reply.status(404).send({ error: `Entity not found: ${type}/${id}` });
|
|
323
361
|
}
|
|
@@ -330,20 +368,128 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
330
368
|
}
|
|
331
369
|
return reply.send(entity);
|
|
332
370
|
});
|
|
333
|
-
// GET /api/repos/:repo/
|
|
334
|
-
fastify.get('/api/repos/:repo/
|
|
335
|
-
const { repo } = req.params;
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
371
|
+
// GET /api/repos/:repo/entities/:type/:id/history — entity commit history
|
|
372
|
+
fastify.get('/api/repos/:repo/entities/:type/:id/history', async (req, reply) => {
|
|
373
|
+
const { repo, type, id } = req.params;
|
|
374
|
+
// Access enforced by graphForRead (WorkspaceAccessService).
|
|
375
|
+
const graph = graphForRead(req, reply, repo);
|
|
376
|
+
if (!graph)
|
|
377
|
+
return;
|
|
378
|
+
const gitService = graph.getGitService();
|
|
379
|
+
if (!gitService) {
|
|
380
|
+
return reply.send([]);
|
|
381
|
+
}
|
|
382
|
+
const limit = Math.min(parseInt(req.query.limit ?? '20', 10) || 20, 50);
|
|
383
|
+
// Resolve the actual on-disk path via graph.get() — it walks nested
|
|
384
|
+
// folders to find the file. The previous hardcoded `${plural}/${id}.md`
|
|
385
|
+
// missed any entry not at the legacy flat path (entries created in a
|
|
386
|
+
// sub-folder, MCP entries that landed elsewhere, etc.) and silently
|
|
387
|
+
// returned an empty history. Convert absolute path → repo-relative
|
|
388
|
+
// for git log.
|
|
389
|
+
let filePath;
|
|
341
390
|
try {
|
|
342
|
-
|
|
391
|
+
const entity = graph.get(type, id);
|
|
392
|
+
const repoRoot = graph.getRepoPath();
|
|
393
|
+
filePath = entity.path.startsWith(repoRoot + '/')
|
|
394
|
+
? entity.path.slice(repoRoot.length + 1)
|
|
395
|
+
: entity.path;
|
|
343
396
|
}
|
|
344
397
|
catch (err) {
|
|
345
398
|
return reply.status(404).send({ error: err.message });
|
|
346
399
|
}
|
|
400
|
+
try {
|
|
401
|
+
const history = gitService.getFileHistory(filePath, limit);
|
|
402
|
+
// Opt-in per-commit change summary for the History panel. Off by
|
|
403
|
+
// default so the eager gating fetch on every entity-page load stays
|
|
404
|
+
// cheap; the panel requests it only when expanded.
|
|
405
|
+
if (req.query.changes === 'true') {
|
|
406
|
+
return reply.send(history.map((c) => ({ ...c, changes: graph.getCommitChange(filePath, c.hash) })));
|
|
407
|
+
}
|
|
408
|
+
return reply.send(history);
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
return reply.send([]);
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
// POST /api/repos/:repo/entities/:type/:id/restore — restore an entry to
|
|
415
|
+
// its content at a past commit. Single-entity scope. Editor-or-admin role
|
|
416
|
+
// required (canWriteRepo). Returns 200 with the restore result, 404 if
|
|
417
|
+
// the entry doesn't exist at that commit, 409 PATH_CONFLICT if a different
|
|
418
|
+
// entry now occupies the target path, 403 if the caller lacks write access.
|
|
419
|
+
fastify.post('/api/repos/:repo/entities/:type/:id/restore', async (req, reply) => {
|
|
420
|
+
const { repo, type, id } = req.params;
|
|
421
|
+
const { commitHash } = req.body ?? {};
|
|
422
|
+
if (!commitHash || typeof commitHash !== 'string') {
|
|
423
|
+
return reply.status(400).send({ error: 'commitHash is required' });
|
|
424
|
+
}
|
|
425
|
+
// Access enforced by graphForWrite: 404 if no read access, 403 if no write.
|
|
426
|
+
const graph = graphForWrite(req, reply, repo);
|
|
427
|
+
if (!graph)
|
|
428
|
+
return;
|
|
429
|
+
// Flush any deferred autosave for this repo first — otherwise a restore
|
|
430
|
+
// could overwrite a pending (uncommitted) edit on disk and commit only
|
|
431
|
+
// the restored version, silently discarding the in-flight work. Matches
|
|
432
|
+
// rename/move/change-type, which all flush before mutating.
|
|
433
|
+
if (sessionManager)
|
|
434
|
+
await sessionManager.flush(repo);
|
|
435
|
+
try {
|
|
436
|
+
const result = await graph.restoreFile(type, id, commitHash, reqGitUser(req));
|
|
437
|
+
wsHub?.broadcast({
|
|
438
|
+
type: 'entity_change',
|
|
439
|
+
action: result.action === 'recreate' ? 'created' : 'updated',
|
|
440
|
+
repo,
|
|
441
|
+
entityType: type,
|
|
442
|
+
entityId: id,
|
|
443
|
+
actor: reqUser(req)?.displayName ?? 'API',
|
|
444
|
+
source: 'api',
|
|
445
|
+
timestamp: new Date().toISOString(),
|
|
446
|
+
});
|
|
447
|
+
return reply.send({
|
|
448
|
+
success: true,
|
|
449
|
+
action: result.action,
|
|
450
|
+
path: result.path,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
catch (err) {
|
|
454
|
+
// PathConflictError carries an error code distinct from generic
|
|
455
|
+
// failures so the UI can render an actionable conflict message.
|
|
456
|
+
if (err?.code === 'PATH_CONFLICT' && err?.occupiedBy) {
|
|
457
|
+
return reply.status(409).send({
|
|
458
|
+
error: 'PATH_CONFLICT',
|
|
459
|
+
path: err.path,
|
|
460
|
+
occupiedBy: err.occupiedBy,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
const message = err instanceof Error ? err.message : 'Restore failed';
|
|
464
|
+
// "not found at commit" / "did not exist at commit" → 404 so the
|
|
465
|
+
// client can show "that version isn't available" instead of a
|
|
466
|
+
// generic 500.
|
|
467
|
+
if (/not found at commit|did not exist at commit/i.test(message)) {
|
|
468
|
+
return reply.status(404).send({ error: message });
|
|
469
|
+
}
|
|
470
|
+
// Type-mismatch refusal (restoring would silently change entity_type)
|
|
471
|
+
// is a client-actionable conflict, not a server fault → 409.
|
|
472
|
+
if (/would change its type|was type '/i.test(message)) {
|
|
473
|
+
return reply.status(409).send({ error: message });
|
|
474
|
+
}
|
|
475
|
+
return reply.status(500).send({ error: message });
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
// GET /api/schema/:entityType/fields — field metadata for an entity type
|
|
479
|
+
fastify.get('/api/schema/:entityType/fields', async (req, reply) => {
|
|
480
|
+
const { entityType } = req.params;
|
|
481
|
+
const registry = workspaceManager.getSchemaRegistry();
|
|
482
|
+
const fields = registry.getFieldMetadata(entityType);
|
|
483
|
+
return reply.send(fields);
|
|
484
|
+
});
|
|
485
|
+
// GET /api/repos/:repo/search?q=&type=&tags= — search within a repo
|
|
486
|
+
fastify.get('/api/repos/:repo/search', async (req, reply) => {
|
|
487
|
+
const { repo } = req.params;
|
|
488
|
+
// Access enforced by graphForRead (WorkspaceAccessService).
|
|
489
|
+
const graph = graphForRead(req, reply, repo);
|
|
490
|
+
if (!graph)
|
|
491
|
+
return;
|
|
492
|
+
const { q, type, tags } = req.query;
|
|
347
493
|
const results = graph.search({
|
|
348
494
|
query: q,
|
|
349
495
|
entityType: type,
|
|
@@ -366,12 +512,11 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
366
512
|
const entityIds = new Set();
|
|
367
513
|
for (const repoConfig of repos) {
|
|
368
514
|
try {
|
|
515
|
+
// Direct WM access OK — `repos` came from getAccessibleRepos above.
|
|
369
516
|
const graph = workspaceManager.getGraph(repoConfig.name);
|
|
370
517
|
const entities = graph.search({});
|
|
371
518
|
for (const entity of entities) {
|
|
372
519
|
const nodeId = entity.id;
|
|
373
|
-
if (nodeId.toLowerCase().startsWith('readme'))
|
|
374
|
-
continue;
|
|
375
520
|
if (!entityIds.has(nodeId)) {
|
|
376
521
|
entityIds.add(nodeId);
|
|
377
522
|
nodes.push({
|
|
@@ -399,9 +544,9 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
399
544
|
const validEdges = edges.filter(e => entityIds.has(e.source) && entityIds.has(e.target));
|
|
400
545
|
return reply.send({ nodes, edges: validEdges });
|
|
401
546
|
});
|
|
402
|
-
// GET /api/search?q=&type=&tags=&repos= — cross-repo search
|
|
547
|
+
// GET /api/search?q=&type=&tags=&repos=&mode= — cross-repo search
|
|
403
548
|
fastify.get('/api/search', async (req, reply) => {
|
|
404
|
-
const { q, type, tags, repos } = req.query;
|
|
549
|
+
const { q, type, tags, repos, mode } = req.query;
|
|
405
550
|
const accessibleNames = new Set(getAccessibleRepos(req, workspaceManager, authService).map(r => r.name));
|
|
406
551
|
// Filter requested repos to only accessible ones
|
|
407
552
|
let repoNames = repos ? repos.split(',').map(r => r.trim()) : undefined;
|
|
@@ -411,12 +556,35 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
411
556
|
else {
|
|
412
557
|
repoNames = [...accessibleNames];
|
|
413
558
|
}
|
|
559
|
+
const keywordMode = mode === 'keyword';
|
|
414
560
|
const results = await workspaceManager.search({
|
|
415
561
|
query: q,
|
|
416
562
|
entityType: type,
|
|
417
563
|
tags: tags ? tags.split(',').map(t => t.trim()) : undefined,
|
|
418
564
|
repoNames,
|
|
565
|
+
mode: keywordMode ? 'keyword' : undefined,
|
|
566
|
+
// Autosuggest dropdown caps at 20 visible rows; capping server-side
|
|
567
|
+
// avoids serializing a long tail the client throws away.
|
|
568
|
+
limit: keywordMode ? 20 : undefined,
|
|
419
569
|
});
|
|
570
|
+
// Autosuggest doesn't need the markdown body or wikilinks — the
|
|
571
|
+
// dropdown renders {name, type, repo} only. Returning a slim shape
|
|
572
|
+
// cuts response size from ~hundreds of KB to a few KB on big
|
|
573
|
+
// workspaces, and skips the per-entity wikilink visibility filter.
|
|
574
|
+
if (keywordMode) {
|
|
575
|
+
const slim = results.map(e => ({
|
|
576
|
+
id: e.id,
|
|
577
|
+
path: e.path,
|
|
578
|
+
folder_path: e.folder_path,
|
|
579
|
+
entityType: e.entityType,
|
|
580
|
+
repoName: e.repoName,
|
|
581
|
+
data: {
|
|
582
|
+
name: e.data?.name,
|
|
583
|
+
updated_at: e.data?.updated_at,
|
|
584
|
+
},
|
|
585
|
+
}));
|
|
586
|
+
return reply.send(slim);
|
|
587
|
+
}
|
|
420
588
|
const accessibleIds = getAccessibleEntityIds(req, workspaceManager, authService);
|
|
421
589
|
const filtered = results.map((e) => {
|
|
422
590
|
if (!e.document?.wikilinks?.length)
|
|
@@ -428,20 +596,29 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
428
596
|
// PUT /api/repos/:repo/entities/:type/:id — update entity (data and/or content)
|
|
429
597
|
fastify.put('/api/repos/:repo/entities/:type/:id', async (req, reply) => {
|
|
430
598
|
const { repo, type, id } = req.params;
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
599
|
+
// Access enforced by graphForWrite \u2014 PUT updates an entity, write required.
|
|
600
|
+
const graph = graphForWrite(req, reply, repo);
|
|
601
|
+
if (!graph)
|
|
602
|
+
return;
|
|
603
|
+
const eagerError = checkEagerSkillPermission(req, type, req.body.data, repo, workspaceManager, authService);
|
|
604
|
+
if (eagerError)
|
|
605
|
+
return reply.status(403).send({ error: eagerError });
|
|
435
606
|
try {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
//
|
|
607
|
+
// Optimistic concurrency. Two preconditions:
|
|
608
|
+
// - `X-Studiograph-Base-Body-Hash` (Phase 3a): sha256 hex of the
|
|
609
|
+
// body the client last loaded. Enforced INSIDE the per-doc lock
|
|
610
|
+
// by graph.update via `baseRevision`, so it composes with
|
|
611
|
+
// serialization — two body-writing PUTs from the same baseline
|
|
612
|
+
// can't both pass and back-stomp each other.
|
|
613
|
+
// - `If-Unmodified-Since` (legacy): kept for non-web clients;
|
|
614
|
+
// RFC-7231 second-precision so aliases sub-millisecond writes,
|
|
615
|
+
// which is why the body-hash form is preferred.
|
|
616
|
+
// Body-hash header is IGNORED on metadata-only PUTs — a stale body
|
|
617
|
+
// baseline is irrelevant when the PUT doesn't touch the body, and
|
|
618
|
+
// enforcing it would cause spurious 412s after an agent body-write.
|
|
619
|
+
const baseBodyHash = req.headers['x-studiograph-base-body-hash'];
|
|
443
620
|
const ifUnmodifiedSince = req.headers['if-unmodified-since'];
|
|
444
|
-
if (ifUnmodifiedSince) {
|
|
621
|
+
if (ifUnmodifiedSince && !baseBodyHash) {
|
|
445
622
|
const current = graph.get(type, id);
|
|
446
623
|
const entityUpdatedAt = current?.data?.updated_at;
|
|
447
624
|
if (entityUpdatedAt && new Date(entityUpdatedAt) > new Date(ifUnmodifiedSince)) {
|
|
@@ -449,6 +626,29 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
449
626
|
}
|
|
450
627
|
}
|
|
451
628
|
const gitUser = reqGitUser(req);
|
|
629
|
+
// Workspace-wide uniqueness check for `user_handle` on person entities.
|
|
630
|
+
// Skip the entity being updated so we don't reject it for clashing
|
|
631
|
+
// with itself. Also gate WHO can change the bridge: only workspace
|
|
632
|
+
// owners may stamp/clear/rewrite user_handle. Other folder-writers
|
|
633
|
+
// see the field but can't edit it (defense against impersonation
|
|
634
|
+
// through the agent's Current User prompt).
|
|
635
|
+
if (type === 'person' && req.body.data) {
|
|
636
|
+
try {
|
|
637
|
+
validateUserHandleInPayload(workspaceManager, req.body.data, { repo, entityId: id });
|
|
638
|
+
if (Object.prototype.hasOwnProperty.call(req.body.data, 'user_handle')) {
|
|
639
|
+
const current = graph.get('person', id);
|
|
640
|
+
const currentHandle = (current?.data?.user_handle ?? '');
|
|
641
|
+
assertCanChangeUserHandle(reqUser(req), currentHandle, req.body.data.user_handle);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
catch (err) {
|
|
645
|
+
if (err instanceof BridgeError) {
|
|
646
|
+
const status = err.code === 'OWNER_REQUIRED' ? 403 : 409;
|
|
647
|
+
return reply.status(status).send({ error: err.message, code: err.code, ...err.details });
|
|
648
|
+
}
|
|
649
|
+
throw err;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
452
652
|
const updated = await graph.update({
|
|
453
653
|
entityType: type,
|
|
454
654
|
entityId: id,
|
|
@@ -457,6 +657,12 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
457
657
|
commitMessage: `Update ${type}/${id} from web UI`,
|
|
458
658
|
user: gitUser,
|
|
459
659
|
skipCommit: !!sessionManager, // defer commit if scheduler is active
|
|
660
|
+
source: 'autosave',
|
|
661
|
+
// Forward the body-hash precondition into graph.update so the
|
|
662
|
+
// check runs INSIDE the per-doc lock — see Phase 3 P1 fix.
|
|
663
|
+
// Only meaningful for body writes; ignored when content is
|
|
664
|
+
// undefined (graph.update gates on content !== undefined too).
|
|
665
|
+
baseRevision: req.body.content !== undefined ? baseBodyHash : undefined,
|
|
460
666
|
});
|
|
461
667
|
if (sessionManager && gitUser) {
|
|
462
668
|
sessionManager.markDirty(repo, gitUser, type, id, updated.path);
|
|
@@ -469,6 +675,19 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
469
675
|
return reply.send(updated);
|
|
470
676
|
}
|
|
471
677
|
catch (err) {
|
|
678
|
+
// Translate the locked-section baseRevision mismatch into the
|
|
679
|
+
// same 412 shape the route used to emit pre-lock. Preserves the
|
|
680
|
+
// client-side contract (code: STALE_BODY_HASH + currentBodyHash
|
|
681
|
+
// + entity).
|
|
682
|
+
if (err instanceof StaleBaseRevisionError) {
|
|
683
|
+
const current = graph.get(type, id);
|
|
684
|
+
return reply.status(412).send({
|
|
685
|
+
error: 'Precondition Failed: body was modified',
|
|
686
|
+
code: 'STALE_BODY_HASH',
|
|
687
|
+
currentBodyHash: err.actual,
|
|
688
|
+
entity: current,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
472
691
|
return reply.status(400).send({ error: err.message });
|
|
473
692
|
}
|
|
474
693
|
});
|
|
@@ -479,16 +698,10 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
479
698
|
if (!type || !id || !newType) {
|
|
480
699
|
return reply.status(400).send({ error: 'entityType, entityId, and newType are required' });
|
|
481
700
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
try {
|
|
487
|
-
graph = workspaceManager.getGraph(repo);
|
|
488
|
-
}
|
|
489
|
-
catch (err) {
|
|
490
|
-
return reply.status(404).send({ error: err.message });
|
|
491
|
-
}
|
|
701
|
+
// Access enforced by graphForRead (WorkspaceAccessService).
|
|
702
|
+
const graph = graphForRead(req, reply, repo);
|
|
703
|
+
if (!graph)
|
|
704
|
+
return;
|
|
492
705
|
try {
|
|
493
706
|
// Flush any pending session saves before moving the file
|
|
494
707
|
if (sessionManager) {
|
|
@@ -513,45 +726,111 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
513
726
|
return reply.status(status).send({ error: err.message });
|
|
514
727
|
}
|
|
515
728
|
});
|
|
516
|
-
// POST /api/repos/:repo/
|
|
517
|
-
fastify.post('/api/repos/:repo/
|
|
518
|
-
const
|
|
519
|
-
const { entityType, entityId
|
|
520
|
-
if (!
|
|
521
|
-
return reply.status(400).send({ error: 'entityType, entityId,
|
|
729
|
+
// POST /api/repos/:repo/rename — rename an entity (changes its id/filename)
|
|
730
|
+
fastify.post('/api/repos/:repo/rename', async (req, reply) => {
|
|
731
|
+
const { repo } = req.params;
|
|
732
|
+
const { entityType: type, entityId: id, newId } = req.body ?? {};
|
|
733
|
+
if (!type || !id || !newId) {
|
|
734
|
+
return reply.status(400).send({ error: 'entityType, entityId, and newId are required' });
|
|
522
735
|
}
|
|
523
|
-
if (
|
|
524
|
-
return reply.status(400).send({ error: '
|
|
736
|
+
if (!/^[a-z0-9][a-z0-9_-]*$/.test(newId)) {
|
|
737
|
+
return reply.status(400).send({ error: 'New ID must be kebab-case (lowercase letters, numbers, hyphens, underscores)' });
|
|
525
738
|
}
|
|
526
|
-
if (
|
|
527
|
-
return reply.status(
|
|
739
|
+
if (id === newId) {
|
|
740
|
+
return reply.status(400).send({ error: 'New ID must differ from the current ID' });
|
|
528
741
|
}
|
|
529
|
-
|
|
530
|
-
|
|
742
|
+
// Access enforced by graphForWrite (WorkspaceAccessService).
|
|
743
|
+
const graph = graphForWrite(req, reply, repo);
|
|
744
|
+
if (!graph)
|
|
745
|
+
return;
|
|
746
|
+
// Workspace-wide uniqueness — ids must be unique across all folders
|
|
747
|
+
const conflict = workspaceManager.findEntityById(newId);
|
|
748
|
+
if (conflict) {
|
|
749
|
+
return reply.status(409).send({ error: `An entry with id "${newId}" already exists in folder "${conflict.repoName}".` });
|
|
531
750
|
}
|
|
532
|
-
let sourceGraph, targetGraph;
|
|
533
751
|
try {
|
|
534
|
-
|
|
535
|
-
|
|
752
|
+
// Flush pending session saves so we don't PUT to the old path after rename
|
|
753
|
+
if (sessionManager) {
|
|
754
|
+
await sessionManager.flush(repo);
|
|
755
|
+
}
|
|
756
|
+
const gitUser = reqGitUser(req);
|
|
757
|
+
const renamed = await graph.rename(type, id, newId, undefined, gitUser);
|
|
758
|
+
// Rewrite any [[wikilinks]] that still point at the old id, across
|
|
759
|
+
// every folder the caller can access. Per-repo — cross-folder links
|
|
760
|
+
// are rare but supported.
|
|
761
|
+
const refactorMsg = `Refactor wikilinks: ${id} \u2192 ${newId}`;
|
|
762
|
+
for (const other of getAccessibleRepos(req, workspaceManager, authService)) {
|
|
763
|
+
try {
|
|
764
|
+
// Direct WM access OK — iterating the access-filtered list.
|
|
765
|
+
const otherGraph = workspaceManager.getGraph(other.name);
|
|
766
|
+
await otherGraph.refactorWikilink(id, newId, refactorMsg);
|
|
767
|
+
}
|
|
768
|
+
catch { /* skip unloadable repo */ }
|
|
769
|
+
}
|
|
770
|
+
wsHub?.broadcast({
|
|
771
|
+
type: 'entity_change', action: 'deleted', repo, entityType: type, entityId: id,
|
|
772
|
+
actor: req.user?.displayName ?? 'API',
|
|
773
|
+
source: 'api', timestamp: new Date().toISOString(),
|
|
774
|
+
});
|
|
775
|
+
wsHub?.broadcast({
|
|
776
|
+
type: 'entity_change', action: 'created', repo, entityType: type, entityId: newId,
|
|
777
|
+
actor: req.user?.displayName ?? 'API',
|
|
778
|
+
source: 'api', timestamp: new Date().toISOString(),
|
|
779
|
+
});
|
|
780
|
+
return reply.send({ success: true, entity: renamed, newUrl: `/${repo}/${type}/${newId}` });
|
|
536
781
|
}
|
|
537
782
|
catch (err) {
|
|
538
|
-
|
|
783
|
+
const status = err.message?.includes('already exists') ? 409 : 400;
|
|
784
|
+
return reply.status(status).send({ error: err.message });
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
// POST /api/repos/:repo/transfer — move or copy entity to another folder
|
|
788
|
+
fastify.post('/api/repos/:repo/transfer', async (req, reply) => {
|
|
789
|
+
const sourceRepo = req.params.repo;
|
|
790
|
+
const { entityType, entityId, targetRepo, mode, targetFolderPath } = req.body ?? {};
|
|
791
|
+
if (!entityType || !entityId || !targetRepo || !mode) {
|
|
792
|
+
return reply.status(400).send({ error: 'entityType, entityId, targetRepo, and mode are required' });
|
|
539
793
|
}
|
|
794
|
+
if (sourceRepo === targetRepo) {
|
|
795
|
+
return reply.status(400).send({ error: 'Source and target folders must be different' });
|
|
796
|
+
}
|
|
797
|
+
// Access enforced by graphForWrite — both source (delete on move)
|
|
798
|
+
// and target (create) require write access. Resolved up-front so a
|
|
799
|
+
// target-side denial never surfaces only after source has been
|
|
800
|
+
// mutated. Audit (2026-05) flagged the previous opt-in pattern.
|
|
801
|
+
const sourceGraph = graphForWrite(req, reply, sourceRepo);
|
|
802
|
+
if (!sourceGraph)
|
|
803
|
+
return;
|
|
804
|
+
const targetGraph = graphForWrite(req, reply, targetRepo);
|
|
805
|
+
if (!targetGraph)
|
|
806
|
+
return;
|
|
540
807
|
try {
|
|
541
|
-
//
|
|
542
|
-
|
|
808
|
+
// Phase 0 — prevalidate. Audit (2026-05) required this: previously
|
|
809
|
+
// the route created in target, broadcast `created`, and only then
|
|
810
|
+
// tried to delete from source. A delete failure left a duplicate
|
|
811
|
+
// entry the client had already been told about.
|
|
812
|
+
if (sessionManager) {
|
|
543
813
|
await sessionManager.flush(sourceRepo);
|
|
544
|
-
|
|
814
|
+
if (targetRepo !== sourceRepo)
|
|
815
|
+
await sessionManager.flush(targetRepo);
|
|
816
|
+
}
|
|
817
|
+
// Source must exist; target must not collide. Both checks run BEFORE
|
|
818
|
+
// any mutation so we never enter phase 2 unless phase 1 will succeed.
|
|
545
819
|
const source = sourceGraph.get(entityType, entityId);
|
|
546
|
-
|
|
820
|
+
const sourcePath = source.path;
|
|
821
|
+
const sourceRawMarkdown = readFileSync(sourcePath, 'utf-8');
|
|
547
822
|
try {
|
|
548
823
|
targetGraph.get(entityType, entityId);
|
|
549
824
|
return reply.status(409).send({ error: `"${entityId}" already exists in ${targetRepo}` });
|
|
550
825
|
}
|
|
551
826
|
catch { /* not found = good */ }
|
|
552
|
-
// Create in target
|
|
553
827
|
const gitUser = reqGitUser(req);
|
|
554
828
|
const { entity_type, entity_id, ...dataFields } = source.data;
|
|
829
|
+
// Phase 1 — create in target. No broadcast yet: a half-completed
|
|
830
|
+
// move (created in target, source still present) must NOT be
|
|
831
|
+
// observable to clients, or they will display two copies until the
|
|
832
|
+
// next refresh. `targetFolderPath` lets the caller drop the entry
|
|
833
|
+
// straight into a subfolder of the target repo.
|
|
555
834
|
const created = await targetGraph.create({
|
|
556
835
|
entityType: entityType,
|
|
557
836
|
entityId,
|
|
@@ -559,21 +838,123 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
559
838
|
content: source.document?.content ?? '',
|
|
560
839
|
commitMessage: `${mode === 'move' ? 'Move' : 'Copy'} ${entityType}: ${entityId} from ${sourceRepo}`,
|
|
561
840
|
user: gitUser,
|
|
841
|
+
folderPath: targetFolderPath,
|
|
562
842
|
});
|
|
843
|
+
// Phase 2 — for `move` only: delete from source. If this fails, run
|
|
844
|
+
// a compensating delete on target so we don't leave a duplicate.
|
|
845
|
+
// The compensating delete is best-effort; if it also fails, surface
|
|
846
|
+
// a 500 with both error messages so the residue is visible (the
|
|
847
|
+
// previous behavior silently succeeded and left orphans).
|
|
848
|
+
if (mode === 'move') {
|
|
849
|
+
try {
|
|
850
|
+
// keepAssets: the moved entity's immutable-keyed blobs are shared
|
|
851
|
+
// (same assetId in both repos' bodies during the window); the
|
|
852
|
+
// source delete must not destroy them.
|
|
853
|
+
await sourceGraph.delete(entityType, entityId, undefined, gitUser, { keepAssets: true });
|
|
854
|
+
// Re-point ownership to the target repo ONLY after the source
|
|
855
|
+
// delete succeeds. Doing it after means a failed source delete
|
|
856
|
+
// (handled below) leaves the index pointing at the source repo,
|
|
857
|
+
// which is correct for the entity we're about to restore there.
|
|
858
|
+
// Blobs/URLs are unchanged (immutable keys); only the index's
|
|
859
|
+
// repo column moves, which is what serve-time authz reads.
|
|
860
|
+
workspaceManager.getAssetIndex?.()?.reassignOwner(sourceRepo, entityType, entityId, { repo: targetRepo });
|
|
861
|
+
}
|
|
862
|
+
catch (deleteErr) {
|
|
863
|
+
const deleteMsg = deleteErr instanceof Error ? deleteErr.message : String(deleteErr);
|
|
864
|
+
let recoveryMsg = null;
|
|
865
|
+
try {
|
|
866
|
+
let restoreSource = !existsSync(sourcePath);
|
|
867
|
+
if (!restoreSource) {
|
|
868
|
+
try {
|
|
869
|
+
restoreSource = readFileSync(sourcePath, 'utf-8') !== sourceRawMarkdown;
|
|
870
|
+
}
|
|
871
|
+
catch {
|
|
872
|
+
restoreSource = true;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
if (restoreSource) {
|
|
876
|
+
mkdirSync(dirname(sourcePath), { recursive: true });
|
|
877
|
+
writeFileSync(sourcePath, sourceRawMarkdown, 'utf-8');
|
|
878
|
+
const recoveryPaths = [sourcePath];
|
|
879
|
+
const folderMarkerPath = join(dirname(sourcePath), '.gitkeep');
|
|
880
|
+
if (existsSync(folderMarkerPath)) {
|
|
881
|
+
rmSync(folderMarkerPath, { force: true });
|
|
882
|
+
recoveryPaths.push(folderMarkerPath);
|
|
883
|
+
}
|
|
884
|
+
const sourceAny = sourceGraph;
|
|
885
|
+
const recoveryGit = sourceAny.gitService;
|
|
886
|
+
const recoveryUser = gitUser ?? sourceAny.gitUser;
|
|
887
|
+
if (!recoveryGit || !recoveryUser) {
|
|
888
|
+
throw new Error('source graph git service or git user unavailable');
|
|
889
|
+
}
|
|
890
|
+
recoveryGit.commitFiles(recoveryPaths, `Recover ${entityType}: ${entityId} after failed move to ${targetRepo}`, recoveryUser);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
catch (recoveryErr) {
|
|
894
|
+
recoveryMsg = recoveryErr instanceof Error ? recoveryErr.message : String(recoveryErr);
|
|
895
|
+
log.error('[critical] cross-repo move source recovery failed', {
|
|
896
|
+
sourceRepo,
|
|
897
|
+
targetRepo,
|
|
898
|
+
entityType,
|
|
899
|
+
entityId,
|
|
900
|
+
sourcePath,
|
|
901
|
+
deleteError: deleteMsg,
|
|
902
|
+
recoveryError: recoveryMsg,
|
|
903
|
+
capturedContent: sourceRawMarkdown,
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
try {
|
|
907
|
+
// keepAssets: ownership was NOT reassigned (that happens only
|
|
908
|
+
// after a successful source delete), so the blobs still belong
|
|
909
|
+
// to the source entity we just restored. Destroying them here
|
|
910
|
+
// would leave the recovered source pointing at dead URLs.
|
|
911
|
+
await targetGraph.delete(entityType, entityId, undefined, gitUser, { keepAssets: true });
|
|
912
|
+
if (recoveryMsg) {
|
|
913
|
+
return reply.status(500).send({
|
|
914
|
+
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.`,
|
|
915
|
+
code: 'MOVE_SOURCE_RECOVERY_FAILED',
|
|
916
|
+
sourceRepo,
|
|
917
|
+
targetRepo,
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
return reply.status(500).send({
|
|
921
|
+
error: `Move failed: source delete of ${entityType}/${entityId} failed (${deleteMsg}). Target rolled back; folders are back to their pre-move state.`,
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
catch (compErr) {
|
|
925
|
+
const compMsg = compErr instanceof Error ? compErr.message : String(compErr);
|
|
926
|
+
if (recoveryMsg) {
|
|
927
|
+
return reply.status(500).send({
|
|
928
|
+
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.`,
|
|
929
|
+
code: 'MOVE_RECOVERY_AND_ROLLBACK_FAILED',
|
|
930
|
+
sourceRepo,
|
|
931
|
+
targetRepo,
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
return reply.status(500).send({
|
|
935
|
+
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.`,
|
|
936
|
+
code: 'MOVE_DUPLICATE',
|
|
937
|
+
sourceRepo,
|
|
938
|
+
targetRepo,
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
// Phase 3 — both phases succeeded; only now broadcast. Order:
|
|
944
|
+
// source-delete first so subscribers don't briefly observe the
|
|
945
|
+
// entity in both folders during the network race.
|
|
563
946
|
const actor = req.user?.displayName ?? 'API';
|
|
564
947
|
const ts = new Date().toISOString();
|
|
565
|
-
wsHub?.broadcast({
|
|
566
|
-
type: 'entity_change', action: 'created', repo: targetRepo, entityType, entityId,
|
|
567
|
-
actor, source: 'api', timestamp: ts,
|
|
568
|
-
});
|
|
569
|
-
// Delete from source if moving
|
|
570
948
|
if (mode === 'move') {
|
|
571
|
-
await sourceGraph.delete(entityType, entityId, undefined, gitUser);
|
|
572
949
|
wsHub?.broadcast({
|
|
573
950
|
type: 'entity_change', action: 'deleted', repo: sourceRepo, entityType, entityId,
|
|
574
951
|
actor, source: 'api', timestamp: ts,
|
|
575
952
|
});
|
|
576
953
|
}
|
|
954
|
+
wsHub?.broadcast({
|
|
955
|
+
type: 'entity_change', action: 'created', repo: targetRepo, entityType, entityId,
|
|
956
|
+
actor, source: 'api', timestamp: ts,
|
|
957
|
+
});
|
|
577
958
|
return reply.send({
|
|
578
959
|
success: true,
|
|
579
960
|
entity: created,
|
|
@@ -584,19 +965,107 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
584
965
|
return reply.status(400).send({ error: err.message });
|
|
585
966
|
}
|
|
586
967
|
});
|
|
587
|
-
//
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
968
|
+
// POST /api/repos/:repo/transfer-entities — bulk move a list of entries
|
|
969
|
+
// into a single target folder (in this or another repo). The web UI's
|
|
970
|
+
// multi-select Move flow used to loop and POST per entry; this collapses
|
|
971
|
+
// to one round-trip per source repo and one commit per repo touched.
|
|
972
|
+
fastify.post('/api/repos/:repo/transfer-entities', async (req, reply) => {
|
|
973
|
+
const sourceRepo = req.params.repo;
|
|
974
|
+
const { entries, targetRepo, targetFolder } = req.body ?? {};
|
|
975
|
+
if (!Array.isArray(entries) || !targetRepo || targetFolder === undefined) {
|
|
976
|
+
return reply.status(400).send({ error: 'entries, targetRepo, and targetFolder are required' });
|
|
977
|
+
}
|
|
978
|
+
// Access enforced by graphForWrite — both source (mutate) and
|
|
979
|
+
// target (write) require write access. Resolved up-front so a
|
|
980
|
+
// target-side denial never surfaces only after source mutation.
|
|
981
|
+
const sourceGraph = graphForWrite(req, reply, sourceRepo);
|
|
982
|
+
if (!sourceGraph)
|
|
983
|
+
return;
|
|
984
|
+
const targetGraph = graphForWrite(req, reply, targetRepo);
|
|
985
|
+
if (!targetGraph)
|
|
986
|
+
return;
|
|
987
|
+
try {
|
|
988
|
+
if (sessionManager) {
|
|
989
|
+
await sessionManager.flush(sourceRepo);
|
|
990
|
+
if (targetRepo !== sourceRepo)
|
|
991
|
+
await sessionManager.flush(targetRepo);
|
|
992
|
+
}
|
|
993
|
+
const result = await sourceGraph.moveEntitiesToRepo(entries.map(e => ({ entityType: e.entityType, entityId: e.entityId })), targetGraph, targetFolder, { user: reqGitUser(req) });
|
|
994
|
+
const actor = req.user?.displayName ?? 'API';
|
|
995
|
+
const ts = new Date().toISOString();
|
|
996
|
+
wsHub?.broadcast({ type: 'repo_sync', repo: sourceRepo, source: 'api', actor, timestamp: ts });
|
|
997
|
+
if (targetRepo !== sourceRepo) {
|
|
998
|
+
wsHub?.broadcast({ type: 'repo_sync', repo: targetRepo, source: 'api', actor, timestamp: ts });
|
|
999
|
+
}
|
|
1000
|
+
return reply.send({ success: true, ...result });
|
|
592
1001
|
}
|
|
593
|
-
|
|
1002
|
+
catch (err) {
|
|
1003
|
+
const status = err?.name === 'InvalidFolderPathError' ? 400
|
|
1004
|
+
: err.message?.includes('not found') ? 404
|
|
1005
|
+
: err.message?.includes('already occupied') ? 409
|
|
1006
|
+
: 400;
|
|
1007
|
+
return reply.status(status).send({ error: err.message });
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
// POST /api/repos/:repo/transfer-folder — move an entire folder (with all
|
|
1011
|
+
// descendants) into another repo in one shot. Replaces the per-entry loop
|
|
1012
|
+
// the web UI used to run, which produced 2 git commits per entry; this
|
|
1013
|
+
// route performs a single FS-level rename and one commit per repo.
|
|
1014
|
+
fastify.post('/api/repos/:repo/transfer-folder', async (req, reply) => {
|
|
1015
|
+
const sourceRepo = req.params.repo;
|
|
1016
|
+
const { sourceFolder, targetRepo, targetFolder } = req.body ?? {};
|
|
1017
|
+
if (!sourceFolder || !targetRepo || targetFolder === undefined) {
|
|
1018
|
+
return reply.status(400).send({ error: 'sourceFolder, targetRepo, and targetFolder are required' });
|
|
1019
|
+
}
|
|
1020
|
+
if (sourceRepo === targetRepo && sourceFolder === targetFolder) {
|
|
1021
|
+
return reply.status(400).send({ error: 'Source and target are the same' });
|
|
1022
|
+
}
|
|
1023
|
+
// Access enforced by graphForWrite — both source (mutate) and
|
|
1024
|
+
// target (write) require write access. Resolved up-front so a
|
|
1025
|
+
// target-side denial never surfaces only after source mutation.
|
|
1026
|
+
const sourceGraph = graphForWrite(req, reply, sourceRepo);
|
|
1027
|
+
if (!sourceGraph)
|
|
1028
|
+
return;
|
|
1029
|
+
const targetGraph = graphForWrite(req, reply, targetRepo);
|
|
1030
|
+
if (!targetGraph)
|
|
1031
|
+
return;
|
|
594
1032
|
try {
|
|
595
|
-
|
|
1033
|
+
// Flush any deferred saves on either side so we don't strand a
|
|
1034
|
+
// pending edit in the source folder mid-move.
|
|
1035
|
+
if (sessionManager) {
|
|
1036
|
+
await sessionManager.flush(sourceRepo);
|
|
1037
|
+
if (targetRepo !== sourceRepo)
|
|
1038
|
+
await sessionManager.flush(targetRepo);
|
|
1039
|
+
}
|
|
1040
|
+
const stats = await sourceGraph.moveFolderToRepo(sourceFolder, targetGraph, targetFolder, { user: reqGitUser(req) });
|
|
1041
|
+
const actor = req.user?.displayName ?? 'API';
|
|
1042
|
+
const ts = new Date().toISOString();
|
|
1043
|
+
// One refresh-trigger per repo. The receiving and sending lists
|
|
1044
|
+
// both need to update; using `repo_sync` keeps the realtime
|
|
1045
|
+
// handler simple — it already calls workspace.refreshEntities
|
|
1046
|
+
// on this event type. Avoids emitting 2× entries-moved entity_change
|
|
1047
|
+
// broadcasts (one created, one deleted) per file.
|
|
1048
|
+
wsHub?.broadcast({ type: 'repo_sync', repo: sourceRepo, source: 'api', actor, timestamp: ts });
|
|
1049
|
+
if (targetRepo !== sourceRepo) {
|
|
1050
|
+
wsHub?.broadcast({ type: 'repo_sync', repo: targetRepo, source: 'api', actor, timestamp: ts });
|
|
1051
|
+
}
|
|
1052
|
+
return reply.send({ success: true, ...stats });
|
|
596
1053
|
}
|
|
597
1054
|
catch (err) {
|
|
598
|
-
|
|
1055
|
+
const status = err?.name === 'InvalidFolderPathError' ? 400
|
|
1056
|
+
: err.message?.includes('not found') ? 404
|
|
1057
|
+
: err.message?.includes('not empty') ? 409
|
|
1058
|
+
: 400;
|
|
1059
|
+
return reply.status(status).send({ error: err.message });
|
|
599
1060
|
}
|
|
1061
|
+
});
|
|
1062
|
+
// DELETE /api/repos/:repo/entities/:type/:id — delete an entity
|
|
1063
|
+
fastify.delete('/api/repos/:repo/entities/:type/:id', async (req, reply) => {
|
|
1064
|
+
const { repo, type, id } = req.params;
|
|
1065
|
+
// Access enforced by graphForRead (WorkspaceAccessService).
|
|
1066
|
+
const graph = graphForRead(req, reply, repo);
|
|
1067
|
+
if (!graph)
|
|
1068
|
+
return;
|
|
600
1069
|
try {
|
|
601
1070
|
await graph.delete(type, id, `Delete ${type}/${id} from web UI`, reqGitUser(req));
|
|
602
1071
|
wsHub?.broadcast({
|
|
@@ -610,54 +1079,643 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
610
1079
|
return reply.status(400).send({ error: err.message });
|
|
611
1080
|
}
|
|
612
1081
|
});
|
|
1082
|
+
// POST /api/repos/:repo/entities/bulk-delete — delete many entities in
|
|
1083
|
+
// one git transaction + one WS broadcast. Replaces N parallel
|
|
1084
|
+
// DELETE /entities/:type/:id calls, which on a 500-entry bulk delete
|
|
1085
|
+
// produced 500 git commits + 500 entity_change broadcasts and froze
|
|
1086
|
+
// the UI for minutes.
|
|
1087
|
+
fastify.post('/api/repos/:repo/entities/bulk-delete', async (req, reply) => {
|
|
1088
|
+
const { repo } = req.params;
|
|
1089
|
+
// Access enforced by graphForWrite (WorkspaceAccessService).
|
|
1090
|
+
const graph = graphForWrite(req, reply, repo);
|
|
1091
|
+
if (!graph)
|
|
1092
|
+
return;
|
|
1093
|
+
const body = req.body;
|
|
1094
|
+
if (!body?.entities || !Array.isArray(body.entities)) {
|
|
1095
|
+
return reply.status(400).send({ error: 'Expected { entities: [{ entityType, entityId }] }' });
|
|
1096
|
+
}
|
|
1097
|
+
try {
|
|
1098
|
+
const result = await graph.bulkDelete(body.entities.map(e => ({ entityType: e.entityType, entityId: e.entityId })), {
|
|
1099
|
+
user: reqGitUser(req),
|
|
1100
|
+
commitMessage: `Bulk delete ${body.entities.length} ${body.entities.length === 1 ? 'entry' : 'entries'} from web UI`,
|
|
1101
|
+
});
|
|
1102
|
+
if (result.deleted.length > 0) {
|
|
1103
|
+
wsHub?.broadcast({
|
|
1104
|
+
type: 'repo_sync', repo,
|
|
1105
|
+
actor: req.user?.displayName ?? 'API',
|
|
1106
|
+
source: 'api', timestamp: new Date().toISOString(),
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
return reply.send({
|
|
1110
|
+
deleted: result.deleted.length,
|
|
1111
|
+
errors: result.errors,
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
catch (err) {
|
|
1115
|
+
return reply.status(400).send({ error: err.message });
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
613
1118
|
// POST /api/repos/:repo/entities — create a new entity
|
|
614
1119
|
fastify.post('/api/repos/:repo/entities', async (req, reply) => {
|
|
615
1120
|
const { repo } = req.params;
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
1121
|
+
// Access enforced by graphForWrite (WorkspaceAccessService).
|
|
1122
|
+
const graph = graphForWrite(req, reply, repo);
|
|
1123
|
+
if (!graph)
|
|
1124
|
+
return;
|
|
1125
|
+
const { entityType, entityId, data = {}, content = '', folder_path } = req.body;
|
|
620
1126
|
if (!entityType || !entityId) {
|
|
621
1127
|
return reply.status(400).send({ error: 'entityType and entityId are required' });
|
|
622
1128
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
1129
|
+
const eagerError = checkEagerSkillPermission(req, entityType, data, repo, workspaceManager, authService);
|
|
1130
|
+
if (eagerError)
|
|
1131
|
+
return reply.status(403).send({ error: eagerError });
|
|
1132
|
+
// Person entities can carry a `user_handle` pointer to an auth account;
|
|
1133
|
+
// enforce workspace-wide uniqueness AND owner-only authz before
|
|
1134
|
+
// delegating to the per-repo create path. Stamping a handle on a new
|
|
1135
|
+
// entity is the same trust action as stamping it on an existing one,
|
|
1136
|
+
// so non-owners can't create a pre-linked person entity either.
|
|
1137
|
+
if (entityType === 'person') {
|
|
1138
|
+
try {
|
|
1139
|
+
validateUserHandleInPayload(workspaceManager, data);
|
|
1140
|
+
if (Object.prototype.hasOwnProperty.call(data, 'user_handle')) {
|
|
1141
|
+
assertCanChangeUserHandle(reqUser(req), '', data.user_handle);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
catch (err) {
|
|
1145
|
+
if (err instanceof BridgeError) {
|
|
1146
|
+
const status = err.code === 'OWNER_REQUIRED' ? 403 : 409;
|
|
1147
|
+
return reply.status(status).send({ error: err.message, code: err.code, ...err.details });
|
|
1148
|
+
}
|
|
1149
|
+
throw err;
|
|
1150
|
+
}
|
|
629
1151
|
}
|
|
630
1152
|
try {
|
|
1153
|
+
// Detect implicit subfolder creation: if the entry lands in a folder
|
|
1154
|
+
// that didn't exist yet, the entity_change below only refreshes the
|
|
1155
|
+
// entry list — clients need a _folder event to refetch the folder tree
|
|
1156
|
+
// (sidebar + list/gallery folder rows). Mirrors the MCP create_entity
|
|
1157
|
+
// path; normalize the same way graph.create does so the existence
|
|
1158
|
+
// check matches the real on-disk path.
|
|
1159
|
+
const normFolder = folder_path ? validateFolderPath(folder_path) : '';
|
|
1160
|
+
const folderExisted = !normFolder || existsSync(join(graph.getRepoPath(), normFolder));
|
|
631
1161
|
const entity = await graph.create({
|
|
632
1162
|
entityType: entityType,
|
|
633
1163
|
entityId,
|
|
634
1164
|
data: { entity_type: entityType, entity_id: entityId, ...data },
|
|
635
1165
|
content,
|
|
1166
|
+
folderPath: folder_path,
|
|
636
1167
|
commitMessage: `Create ${entityType}/${entityId} from web UI`,
|
|
637
1168
|
user: reqGitUser(req),
|
|
638
1169
|
});
|
|
1170
|
+
const actor = req.user?.displayName ?? 'API';
|
|
1171
|
+
const ts = new Date().toISOString();
|
|
639
1172
|
wsHub?.broadcast({
|
|
640
1173
|
type: 'entity_change', action: 'created', repo, entityType, entityId,
|
|
1174
|
+
actor, source: 'api', timestamp: ts,
|
|
1175
|
+
});
|
|
1176
|
+
if (!folderExisted) {
|
|
1177
|
+
wsHub?.broadcast({
|
|
1178
|
+
type: 'entity_change', action: 'created', repo, entityType: '_folder', entityId: normFolder,
|
|
1179
|
+
actor, source: 'api', timestamp: ts,
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
return reply.status(201).send(entity);
|
|
1183
|
+
}
|
|
1184
|
+
catch (err) {
|
|
1185
|
+
return reply.status(400).send({ error: err.message });
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1188
|
+
// POST /api/repos/:repo/move — move entry to a different folder within the
|
|
1189
|
+
// same repo. Distinct from /transfer (cross-repo) and /rename (id change).
|
|
1190
|
+
// The entry's type and id are unchanged; only its folder_path moves.
|
|
1191
|
+
fastify.post('/api/repos/:repo/move', async (req, reply) => {
|
|
1192
|
+
const { repo } = req.params;
|
|
1193
|
+
const { entityType, entityId, folder_path } = req.body ?? {};
|
|
1194
|
+
if (!entityType || !entityId || folder_path === undefined) {
|
|
1195
|
+
return reply.status(400).send({ error: 'entityType, entityId, and folder_path are required' });
|
|
1196
|
+
}
|
|
1197
|
+
// Access enforced by graphForWrite (WorkspaceAccessService).
|
|
1198
|
+
const graph = graphForWrite(req, reply, repo);
|
|
1199
|
+
if (!graph)
|
|
1200
|
+
return;
|
|
1201
|
+
try {
|
|
1202
|
+
if (sessionManager)
|
|
1203
|
+
await sessionManager.flush(repo);
|
|
1204
|
+
// If the destination folder doesn't exist yet, the move creates it —
|
|
1205
|
+
// emit a _folder event (alongside the entry update) so clients refetch
|
|
1206
|
+
// the folder tree, not just the entry list. Mirrors MCP move_entity.
|
|
1207
|
+
const normTarget = folder_path ? validateFolderPath(folder_path) : '';
|
|
1208
|
+
const targetExisted = !normTarget || existsSync(join(graph.getRepoPath(), normTarget));
|
|
1209
|
+
const moved = await graph.move(entityType, entityId, folder_path, undefined, reqGitUser(req));
|
|
1210
|
+
const actor = req.user?.displayName ?? 'API';
|
|
1211
|
+
const ts = new Date().toISOString();
|
|
1212
|
+
wsHub?.broadcast({
|
|
1213
|
+
type: 'entity_change', action: 'updated', repo, entityType, entityId,
|
|
1214
|
+
actor, source: 'api', timestamp: ts,
|
|
1215
|
+
});
|
|
1216
|
+
if (!targetExisted) {
|
|
1217
|
+
wsHub?.broadcast({
|
|
1218
|
+
type: 'entity_change', action: 'created', repo, entityType: '_folder', entityId: normTarget,
|
|
1219
|
+
actor, source: 'api', timestamp: ts,
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
return reply.send({ success: true, entity: moved });
|
|
1223
|
+
}
|
|
1224
|
+
catch (err) {
|
|
1225
|
+
const status = err?.name === 'InvalidFolderPathError' ? 400
|
|
1226
|
+
: err.message?.includes('already occupied') ? 409
|
|
1227
|
+
: err.message?.includes('not found') ? 404
|
|
1228
|
+
: 400;
|
|
1229
|
+
return reply.status(status).send({ error: err.message });
|
|
1230
|
+
}
|
|
1231
|
+
});
|
|
1232
|
+
// POST /api/repos/:repo/folders — create a folder (with .gitkeep). Auto-creates parents.
|
|
1233
|
+
// Optional display_name is written to the folder's .studiograph-folder.json
|
|
1234
|
+
// sidecar when set; otherwise the client falls back to toDisplayName(slug).
|
|
1235
|
+
fastify.post('/api/repos/:repo/folders', async (req, reply) => {
|
|
1236
|
+
const { repo } = req.params;
|
|
1237
|
+
const { path, display_name } = req.body ?? {};
|
|
1238
|
+
if (!path || typeof path !== 'string') {
|
|
1239
|
+
return reply.status(400).send({ error: 'path is required' });
|
|
1240
|
+
}
|
|
1241
|
+
// Access enforced by graphForWrite (WorkspaceAccessService).
|
|
1242
|
+
const graph = graphForWrite(req, reply, repo);
|
|
1243
|
+
if (!graph)
|
|
1244
|
+
return;
|
|
1245
|
+
try {
|
|
1246
|
+
await graph.createFolder(path, undefined, reqGitUser(req), {
|
|
1247
|
+
displayName: display_name,
|
|
1248
|
+
});
|
|
1249
|
+
// Emit the _folder sentinel so OTHER connected clients refetch the
|
|
1250
|
+
// folder tree (sidebar + list/gallery folder rows). The local creator
|
|
1251
|
+
// already updated optimistically via workspace.addFolder.
|
|
1252
|
+
wsHub?.broadcast({
|
|
1253
|
+
type: 'entity_change', action: 'created', repo, entityType: '_folder', entityId: validateFolderPath(path),
|
|
641
1254
|
actor: req.user?.displayName ?? 'API',
|
|
642
1255
|
source: 'api', timestamp: new Date().toISOString(),
|
|
643
1256
|
});
|
|
644
|
-
return reply.status(201).send(
|
|
1257
|
+
return reply.status(201).send({ success: true, path });
|
|
645
1258
|
}
|
|
646
1259
|
catch (err) {
|
|
647
1260
|
return reply.status(400).send({ error: err.message });
|
|
648
1261
|
}
|
|
649
1262
|
});
|
|
1263
|
+
// PATCH /api/repos/:repo/folders — rename / move a folder within the repo,
|
|
1264
|
+
// optionally updating its display_name in the same commit. Three shapes:
|
|
1265
|
+
// { from, to } — slug-only rename / move
|
|
1266
|
+
// { from, to, display_name } — slug change + display update
|
|
1267
|
+
// { from, to: from, display_name } — display-only update (no FS move)
|
|
1268
|
+
// `display_name: ""` (or null) clears the override and reverts to the slug
|
|
1269
|
+
// fallback.
|
|
1270
|
+
fastify.patch('/api/repos/:repo/folders', async (req, reply) => {
|
|
1271
|
+
const { repo } = req.params;
|
|
1272
|
+
const { from, to, display_name } = req.body ?? {};
|
|
1273
|
+
if (!from || !to) {
|
|
1274
|
+
return reply.status(400).send({ error: 'from and to are required' });
|
|
1275
|
+
}
|
|
1276
|
+
// Access enforced by graphForWrite (WorkspaceAccessService).
|
|
1277
|
+
const graph = graphForWrite(req, reply, repo);
|
|
1278
|
+
if (!graph)
|
|
1279
|
+
return;
|
|
1280
|
+
try {
|
|
1281
|
+
if (sessionManager)
|
|
1282
|
+
await sessionManager.flush(repo);
|
|
1283
|
+
await graph.renameFolder(from, to, undefined, reqGitUser(req), {
|
|
1284
|
+
displayName: display_name === undefined ? undefined : display_name,
|
|
1285
|
+
});
|
|
1286
|
+
// Rename/move relocates entries on disk, so the _folder sentinel's
|
|
1287
|
+
// refreshRepoAfterFolderMutation (tree + entity summaries) is exactly
|
|
1288
|
+
// what other clients need. Carries the new path as the entityId.
|
|
1289
|
+
wsHub?.broadcast({
|
|
1290
|
+
type: 'entity_change', action: 'updated', repo, entityType: '_folder', entityId: validateFolderPath(to),
|
|
1291
|
+
actor: req.user?.displayName ?? 'API',
|
|
1292
|
+
source: 'api', timestamp: new Date().toISOString(),
|
|
1293
|
+
});
|
|
1294
|
+
return reply.send({ success: true, from, to });
|
|
1295
|
+
}
|
|
1296
|
+
catch (err) {
|
|
1297
|
+
const status = err?.name === 'InvalidFolderPathError' ? 400
|
|
1298
|
+
: err.message?.includes('not found') ? 404
|
|
1299
|
+
: err.message?.includes('already exists') ? 409
|
|
1300
|
+
: 400;
|
|
1301
|
+
return reply.status(status).send({ error: err.message });
|
|
1302
|
+
}
|
|
1303
|
+
});
|
|
1304
|
+
// DELETE /api/repos/:repo/folders?path=X&recursive=true — delete a folder.
|
|
1305
|
+
// Without recursive=true, non-empty folders return 409 with blast-radius
|
|
1306
|
+
// counts so the UI can confirm before retrying.
|
|
1307
|
+
fastify.delete('/api/repos/:repo/folders', async (req, reply) => {
|
|
1308
|
+
const { repo } = req.params;
|
|
1309
|
+
const { path, recursive } = req.query;
|
|
1310
|
+
if (!path) {
|
|
1311
|
+
return reply.status(400).send({ error: 'path query parameter is required' });
|
|
1312
|
+
}
|
|
1313
|
+
// Access enforced by graphForWrite (WorkspaceAccessService).
|
|
1314
|
+
const graph = graphForWrite(req, reply, repo);
|
|
1315
|
+
if (!graph)
|
|
1316
|
+
return;
|
|
1317
|
+
try {
|
|
1318
|
+
if (sessionManager)
|
|
1319
|
+
await sessionManager.flush(repo);
|
|
1320
|
+
const result = await graph.deleteFolder(path, {
|
|
1321
|
+
recursive: recursive === 'true',
|
|
1322
|
+
user: reqGitUser(req),
|
|
1323
|
+
});
|
|
1324
|
+
// Emit the _folder sentinel so other clients drop the folder (and any
|
|
1325
|
+
// entries it held, via the entity-summary refresh) from their view.
|
|
1326
|
+
wsHub?.broadcast({
|
|
1327
|
+
type: 'entity_change', action: 'deleted', repo, entityType: '_folder', entityId: validateFolderPath(path),
|
|
1328
|
+
actor: req.user?.displayName ?? 'API',
|
|
1329
|
+
source: 'api', timestamp: new Date().toISOString(),
|
|
1330
|
+
});
|
|
1331
|
+
return reply.send({ success: true, ...result });
|
|
1332
|
+
}
|
|
1333
|
+
catch (err) {
|
|
1334
|
+
if (err?.code === 'FOLDER_NOT_EMPTY') {
|
|
1335
|
+
return reply.status(409).send({
|
|
1336
|
+
error: err.message,
|
|
1337
|
+
code: 'FOLDER_NOT_EMPTY',
|
|
1338
|
+
blast_radius: err.blastRadius,
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
const status = err?.name === 'InvalidFolderPathError' ? 400
|
|
1342
|
+
: err.message?.includes('not found') ? 404
|
|
1343
|
+
: 400;
|
|
1344
|
+
return reply.status(status).send({ error: err.message });
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
650
1347
|
// POST /api/repos/:repo/commit — flush deferred commits (navigate-away signal)
|
|
651
1348
|
fastify.post('/api/repos/:repo/commit', async (req, reply) => {
|
|
652
1349
|
if (!sessionManager) {
|
|
653
1350
|
return reply.send({ committed: false });
|
|
654
1351
|
}
|
|
655
1352
|
const { repo } = req.params;
|
|
656
|
-
|
|
657
|
-
|
|
1353
|
+
// Access enforced via accessService.canRead \u2014 flush operates on the
|
|
1354
|
+
// SessionManager (no graph instance needed), so a boolean check is
|
|
1355
|
+
// sufficient. Pending commits were already authored by an authenticated
|
|
1356
|
+
// writer; this endpoint just flushes them on navigate-away.
|
|
1357
|
+
if (!accessService.canRead(reqUser(req), repo)) {
|
|
1358
|
+
return reply.status(404).send({ error: `Folder \u201C${repo}\u201D not found` });
|
|
658
1359
|
}
|
|
659
1360
|
const committed = await sessionManager.flush(repo);
|
|
660
1361
|
return reply.send({ committed });
|
|
661
1362
|
});
|
|
1363
|
+
// POST /api/repos/:repo/import — import files into a folder.
|
|
1364
|
+
//
|
|
1365
|
+
// Routing by extension:
|
|
1366
|
+
// .md / .txt / .csv → direct write via importFile() — no LLM
|
|
1367
|
+
// round-trip, preserves the source faithfully. Markdown frontmatter
|
|
1368
|
+
// (entity_type, entity_id) is honored when present; otherwise
|
|
1369
|
+
// sensible defaults.
|
|
1370
|
+
// .pdf + opaque binaries → asset wrapper via uploadAsset() —
|
|
1371
|
+
// creates an `asset` entity, stores the binary in R2 or the local
|
|
1372
|
+
// volume, writes a PDF text sidecar for vector indexing.
|
|
1373
|
+
// .docx → text extraction + agent capture
|
|
1374
|
+
// pipeline (until the mammoth → markdown workstream lands).
|
|
1375
|
+
fastify.post('/api/repos/:repo/import', async (req, reply) => {
|
|
1376
|
+
const { repo } = req.params;
|
|
1377
|
+
// Access enforced by graphForWrite — import creates entities, so write
|
|
1378
|
+
// is required. Graph captured here is reused by the inner closures so
|
|
1379
|
+
// they don't need to re-resolve (which would also bypass the gate).
|
|
1380
|
+
const repoGraph = graphForWrite(req, reply, repo);
|
|
1381
|
+
if (!repoGraph)
|
|
1382
|
+
return;
|
|
1383
|
+
const body = req.body;
|
|
1384
|
+
if (!body?.files || !Array.isArray(body.files)) {
|
|
1385
|
+
return reply.status(400).send({ error: 'Expected { files: [{ filename, content }] }' });
|
|
1386
|
+
}
|
|
1387
|
+
// Reject paths with traversal segments; importFile slugifies the rest.
|
|
1388
|
+
// Empty / undefined = repo root (existing behavior).
|
|
1389
|
+
const targetFolderPath = (body.targetFolderPath ?? '').trim();
|
|
1390
|
+
if (targetFolderPath.split('/').some(seg => seg === '..' || seg === '.')) {
|
|
1391
|
+
return reply.status(400).send({ error: 'Invalid targetFolderPath: contains traversal segments' });
|
|
1392
|
+
}
|
|
1393
|
+
const captureRepo = body.agentDecides ? undefined : repo;
|
|
1394
|
+
const gitUser = reqGitUser(req);
|
|
1395
|
+
try {
|
|
1396
|
+
const { extractText, isSupportedFile, importFile } = await import('../../services/import-service.js');
|
|
1397
|
+
const { isAssetUploadExtension } = await import('../../services/assets/base.js');
|
|
1398
|
+
const { uploadAsset } = await import('../../services/asset-upload-service.js');
|
|
1399
|
+
const { extname } = await import('path');
|
|
1400
|
+
const results = [];
|
|
1401
|
+
// Direct-write extensions never need an LLM. Verbatim preservation —
|
|
1402
|
+
// markdown stays markdown, CSV stays CSV.
|
|
1403
|
+
const DIRECT_WRITE_EXTS = new Set(['.md', '.txt', '.csv']);
|
|
1404
|
+
const CONCURRENCY = 3;
|
|
1405
|
+
async function processDirect(file) {
|
|
1406
|
+
// Reuse the access-checked graph captured in the outer scope. Do
|
|
1407
|
+
// NOT re-resolve via workspaceManager.getGraph here — the audit's
|
|
1408
|
+
// boundary is the outer graphForWrite call.
|
|
1409
|
+
const graph = repoGraph;
|
|
1410
|
+
const result = await importFile(graph, file.filename, file.content, {
|
|
1411
|
+
user: gitUser,
|
|
1412
|
+
// Caller-chosen destination subfolder; importFile combines it
|
|
1413
|
+
// with each file's own relative directory.
|
|
1414
|
+
folderPath: targetFolderPath || undefined,
|
|
1415
|
+
// Defer commits — we batch them into one transaction at the
|
|
1416
|
+
// end of the request so a 500-file import is one git commit
|
|
1417
|
+
// instead of 500. Per-file commits dominated wall time
|
|
1418
|
+
// (~50-200ms each) before this; batching collapses that to
|
|
1419
|
+
// one commit for the whole batch.
|
|
1420
|
+
skipCommit: true,
|
|
1421
|
+
});
|
|
1422
|
+
if (result.status === 'created' && result.path) {
|
|
1423
|
+
return {
|
|
1424
|
+
filename: file.filename,
|
|
1425
|
+
status: 'created',
|
|
1426
|
+
entities: [{ entityId: result.entityId, entityType: result.entityType }],
|
|
1427
|
+
entry: {
|
|
1428
|
+
operation: 'create',
|
|
1429
|
+
entityType: result.entityType,
|
|
1430
|
+
entityId: result.entityId,
|
|
1431
|
+
filePath: result.path,
|
|
1432
|
+
},
|
|
1433
|
+
sidecarPaths: result.sidecarPaths,
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
return { filename: file.filename, status: result.status, error: result.error };
|
|
1437
|
+
}
|
|
1438
|
+
async function processAsset(file) {
|
|
1439
|
+
// Reuse the outer access-checked graph; re-resolving would bypass
|
|
1440
|
+
// the architectural boundary even though the closure captures repo.
|
|
1441
|
+
const graph = repoGraph;
|
|
1442
|
+
try {
|
|
1443
|
+
const buffer = Buffer.from(file.content, 'base64');
|
|
1444
|
+
const result = await uploadAsset({
|
|
1445
|
+
graph,
|
|
1446
|
+
filename: file.filename,
|
|
1447
|
+
buffer,
|
|
1448
|
+
folderPath: targetFolderPath || undefined,
|
|
1449
|
+
user: gitUser,
|
|
1450
|
+
workspacePath: graph.getWorkspaceRoot(),
|
|
1451
|
+
// Defer commits — the dispatcher's batched commitPending
|
|
1452
|
+
// call at the bottom of the route covers every entry in
|
|
1453
|
+
// the same transaction.
|
|
1454
|
+
skipCommit: true,
|
|
1455
|
+
});
|
|
1456
|
+
return {
|
|
1457
|
+
filename: file.filename,
|
|
1458
|
+
status: 'created',
|
|
1459
|
+
entities: [{ entityId: result.entityId, entityType: result.entityType }],
|
|
1460
|
+
entry: {
|
|
1461
|
+
operation: 'create',
|
|
1462
|
+
entityType: 'asset',
|
|
1463
|
+
entityId: result.entityId,
|
|
1464
|
+
filePath: result.filePath,
|
|
1465
|
+
},
|
|
1466
|
+
// PDF text sidecars: include in extraPaths so they land in
|
|
1467
|
+
// the same commit as the entity files.
|
|
1468
|
+
sidecarPaths: result.sidecarPath ? [result.sidecarPath] : undefined,
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
catch (err) {
|
|
1472
|
+
return { filename: file.filename, status: 'error', error: err.message ?? 'Asset upload failed' };
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
async function processViaAgent(file) {
|
|
1476
|
+
try {
|
|
1477
|
+
const text = await extractText(file.filename, file.content);
|
|
1478
|
+
if (!text) {
|
|
1479
|
+
log.warn(`[import] No text extracted from ${file.filename}`);
|
|
1480
|
+
return { filename: file.filename, status: 'error', error: 'No text content extracted' };
|
|
1481
|
+
}
|
|
1482
|
+
log.info(`[import] Extracted ${text.length} chars from ${file.filename}`);
|
|
1483
|
+
const captureRes = await fastify.inject({
|
|
1484
|
+
method: 'POST',
|
|
1485
|
+
url: '/api/capture',
|
|
1486
|
+
headers: {
|
|
1487
|
+
'content-type': 'application/json',
|
|
1488
|
+
...(req.headers.authorization ? { authorization: req.headers.authorization } : {}),
|
|
1489
|
+
...(req.headers.cookie ? { cookie: req.headers.cookie } : {}),
|
|
1490
|
+
},
|
|
1491
|
+
payload: JSON.stringify({ text: `[Imported from ${file.filename}]\n\n${text}`, ...(captureRepo ? { repo: captureRepo } : {}) }),
|
|
1492
|
+
});
|
|
1493
|
+
const captureBody = JSON.parse(captureRes.body);
|
|
1494
|
+
if (captureRes.statusCode !== 200 || !captureBody.success) {
|
|
1495
|
+
log.warn(`[import] Capture failed for ${file.filename}: ${captureRes.statusCode} ${captureBody.error ?? JSON.stringify(captureBody).slice(0, 200)}`);
|
|
1496
|
+
return { filename: file.filename, status: 'error', error: captureBody.error ?? 'Agent processing failed' };
|
|
1497
|
+
}
|
|
1498
|
+
return { filename: file.filename, status: 'created', entities: captureBody.entities };
|
|
1499
|
+
}
|
|
1500
|
+
catch (err) {
|
|
1501
|
+
return { filename: file.filename, status: 'error', error: err.message ?? 'Processing failed' };
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
async function processFile(file) {
|
|
1505
|
+
if (!file.filename) {
|
|
1506
|
+
return { filename: file.filename, status: 'error', error: 'Missing filename' };
|
|
1507
|
+
}
|
|
1508
|
+
const ext = extname(file.filename).toLowerCase();
|
|
1509
|
+
// Direct-write text formats first — never collide with asset
|
|
1510
|
+
// detection. Then asset extensions (images, PDF, design files,
|
|
1511
|
+
// archives, …). Then the agent-extraction fallback for .docx
|
|
1512
|
+
// until the mammoth workstream lands. Anything else: reject.
|
|
1513
|
+
if (DIRECT_WRITE_EXTS.has(ext))
|
|
1514
|
+
return processDirect(file);
|
|
1515
|
+
if (isAssetUploadExtension(file.filename))
|
|
1516
|
+
return processAsset(file);
|
|
1517
|
+
if (isSupportedFile(file.filename))
|
|
1518
|
+
return processViaAgent(file);
|
|
1519
|
+
return { filename: file.filename, status: 'error', error: `Unsupported file: ${file.filename}` };
|
|
1520
|
+
}
|
|
1521
|
+
// Process files in batches of CONCURRENCY
|
|
1522
|
+
for (let i = 0; i < body.files.length; i += CONCURRENCY) {
|
|
1523
|
+
const batch = body.files.slice(i, i + CONCURRENCY);
|
|
1524
|
+
const batchResults = await Promise.all(batch.map(processFile));
|
|
1525
|
+
results.push(...batchResults);
|
|
1526
|
+
}
|
|
1527
|
+
// Direct-write path deferred git commits — flush them all in one
|
|
1528
|
+
// transaction now. Collapses N per-file commits into one batch
|
|
1529
|
+
// commit (~50ms vs N×50ms). Agent-path files committed inline via
|
|
1530
|
+
// /api/capture so they're not in this set.
|
|
1531
|
+
const pendingEntries = results
|
|
1532
|
+
.map(r => r.entry)
|
|
1533
|
+
.filter((e) => Boolean(e));
|
|
1534
|
+
// Collect every sidecar path produced by the import — these stage in
|
|
1535
|
+
// the same commit as the entity files, so the folder display name
|
|
1536
|
+
// and the entries it labels land atomically. De-dup because two
|
|
1537
|
+
// imported files inside the same Schema_OS/widgets/ would otherwise
|
|
1538
|
+
// produce duplicate sidecar entries.
|
|
1539
|
+
const sidecarPaths = Array.from(new Set(results.flatMap(r => r.sidecarPaths ?? [])));
|
|
1540
|
+
// commitPending failure used to be swallowed and the route returned
|
|
1541
|
+
// success=true even though the files were never committed. The audit
|
|
1542
|
+
// (2026-05) flagged this: a 500-file import that failed mid-commit
|
|
1543
|
+
// looked successful to the client but left N untracked files. Now
|
|
1544
|
+
// the failure propagates as 500 with a partial-failure shape so the
|
|
1545
|
+
// caller knows which entries are stranded.
|
|
1546
|
+
let commitError = null;
|
|
1547
|
+
if (pendingEntries.length > 0) {
|
|
1548
|
+
try {
|
|
1549
|
+
await repoGraph.commitPending(pendingEntries, {
|
|
1550
|
+
commitMessage: `Import ${pendingEntries.length} ${pendingEntries.length === 1 ? 'entry' : 'entries'} from web UI`,
|
|
1551
|
+
user: gitUser,
|
|
1552
|
+
extraPaths: sidecarPaths.length > 0 ? sidecarPaths : undefined,
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
catch (err) {
|
|
1556
|
+
commitError = err instanceof Error ? err.message : String(err);
|
|
1557
|
+
log.warn(`[import] batch commit failed for ${repo}: ${commitError}`);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
const created = results.filter(r => r.status === 'created').length;
|
|
1561
|
+
const entityCount = results.reduce((sum, r) => sum + (r.entities?.length ?? 0), 0);
|
|
1562
|
+
// Single repo_sync broadcast for the whole import. The realtime
|
|
1563
|
+
// store's repo_sync handler refreshes the repo's entity list once
|
|
1564
|
+
// — vs. one entity_change per file, which would do N re-renders
|
|
1565
|
+
// on every connected client and freeze the UI for large imports.
|
|
1566
|
+
// Skip when commit failed: there's nothing for clients to refresh
|
|
1567
|
+
// beyond stranded uncommitted files, and the broadcast would lie
|
|
1568
|
+
// about persistence.
|
|
1569
|
+
if (created > 0 && !commitError) {
|
|
1570
|
+
wsHub?.broadcast({
|
|
1571
|
+
type: 'repo_sync', repo,
|
|
1572
|
+
actor: req.user?.displayName ?? 'API',
|
|
1573
|
+
source: 'api', timestamp: new Date().toISOString(),
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
const errors = results.filter(r => r.status === 'error').map(r => r.error);
|
|
1577
|
+
if (commitError) {
|
|
1578
|
+
// Files are on disk but uncommitted — partial failure. Surface as
|
|
1579
|
+
// 500 with details so the client doesn't show "imported N entries"
|
|
1580
|
+
// for entries that won't survive a server restart.
|
|
1581
|
+
return reply.status(500).send({
|
|
1582
|
+
success: false,
|
|
1583
|
+
error: `Import wrote ${pendingEntries.length} entries to disk but the git commit failed: ${commitError}`,
|
|
1584
|
+
commitError,
|
|
1585
|
+
uncommittedEntries: pendingEntries.length,
|
|
1586
|
+
imported: created,
|
|
1587
|
+
entityCount,
|
|
1588
|
+
skipped: errors.length,
|
|
1589
|
+
errors,
|
|
1590
|
+
results,
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
return {
|
|
1594
|
+
success: true,
|
|
1595
|
+
imported: created,
|
|
1596
|
+
entityCount,
|
|
1597
|
+
skipped: errors.length,
|
|
1598
|
+
errors,
|
|
1599
|
+
results,
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
catch (err) {
|
|
1603
|
+
return reply.status(400).send({
|
|
1604
|
+
success: false,
|
|
1605
|
+
error: err instanceof Error ? err.message : 'Unknown error',
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
});
|
|
1609
|
+
// POST /api/repos/:repo/import-bookmarks — import browser bookmark exports as reference entities
|
|
1610
|
+
fastify.post('/api/repos/:repo/import-bookmarks', async (req, reply) => {
|
|
1611
|
+
const { repo } = req.params;
|
|
1612
|
+
// Access enforced by graphForWrite — bookmark import creates entities.
|
|
1613
|
+
const graph = graphForWrite(req, reply, repo);
|
|
1614
|
+
if (!graph)
|
|
1615
|
+
return;
|
|
1616
|
+
const body = req.body;
|
|
1617
|
+
if (!body?.content || typeof body.content !== 'string') {
|
|
1618
|
+
return reply.status(400).send({ error: 'Expected { content: string }' });
|
|
1619
|
+
}
|
|
1620
|
+
try {
|
|
1621
|
+
const { isNetscapeBookmarkFile, importBookmarks } = await import('../../services/bookmark-import-service.js');
|
|
1622
|
+
if (!isNetscapeBookmarkFile(body.content)) {
|
|
1623
|
+
return reply.status(400).send({
|
|
1624
|
+
error: "This doesn't appear to be a browser bookmark export. Export bookmarks from your browser's bookmark manager to generate the correct format.",
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
const user = reqGitUser(req);
|
|
1628
|
+
const result = await importBookmarks(graph, body.content, user);
|
|
1629
|
+
// Single git commit for all created entities
|
|
1630
|
+
if (result.created > 0) {
|
|
1631
|
+
const git = graph.getGitService();
|
|
1632
|
+
if (git) {
|
|
1633
|
+
const commitUser = user ?? { id: 'import', name: 'Import', email: 'import@studiograph' };
|
|
1634
|
+
git.commitAll(`Import ${result.created} bookmark${result.created !== 1 ? 's' : ''} as references`, commitUser);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
return {
|
|
1638
|
+
success: true,
|
|
1639
|
+
created: result.created,
|
|
1640
|
+
skipped: result.skipped,
|
|
1641
|
+
errors: result.errors,
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
catch (err) {
|
|
1645
|
+
return reply.status(400).send({
|
|
1646
|
+
success: false,
|
|
1647
|
+
error: err instanceof Error ? err.message : 'Unknown error',
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
});
|
|
1651
|
+
// POST /api/repos/:repo/assets — generic asset upload.
|
|
1652
|
+
//
|
|
1653
|
+
// Multipart `file` field. Creates a wrapper `asset` entity whose
|
|
1654
|
+
// frontmatter points at the stored binary (R2 or local volume) via
|
|
1655
|
+
// `asset_url`. PDFs additionally get a text sidecar at
|
|
1656
|
+
// `<workspace>/.studiograph/assets-text/asset/<id>.txt` so the vector
|
|
1657
|
+
// indexer can RAG them without bloating the markdown body. See
|
|
1658
|
+
// docs/asset-architecture.md.
|
|
1659
|
+
fastify.post('/api/repos/:repo/assets', async (req, reply) => {
|
|
1660
|
+
const { repo } = req.params;
|
|
1661
|
+
// Access enforced by graphForWrite (WorkspaceAccessService).
|
|
1662
|
+
const graph = graphForWrite(req, reply, repo);
|
|
1663
|
+
if (!graph)
|
|
1664
|
+
return;
|
|
1665
|
+
// Per-route fileSize override — the global multipart limit (10MB) is
|
|
1666
|
+
// tuned for inline image embeds; full asset uploads accept up to the
|
|
1667
|
+
// 100MB cap enforced inside AssetService.upload().
|
|
1668
|
+
let file;
|
|
1669
|
+
try {
|
|
1670
|
+
file = await req.file({ limits: { fileSize: 100 * 1024 * 1024 } });
|
|
1671
|
+
}
|
|
1672
|
+
catch (err) {
|
|
1673
|
+
return reply.status(400).send({ error: err?.message ?? 'Invalid multipart upload' });
|
|
1674
|
+
}
|
|
1675
|
+
if (!file)
|
|
1676
|
+
return reply.status(400).send({ error: 'No file uploaded' });
|
|
1677
|
+
const buffer = await file.toBuffer();
|
|
1678
|
+
const folderPath = (req.query.folder ?? '').trim();
|
|
1679
|
+
if (folderPath.split('/').some(seg => seg === '..' || seg === '.')) {
|
|
1680
|
+
return reply.status(400).send({ error: 'Invalid folder: contains traversal segments' });
|
|
1681
|
+
}
|
|
1682
|
+
try {
|
|
1683
|
+
const { uploadAsset } = await import('../../services/asset-upload-service.js');
|
|
1684
|
+
const result = await uploadAsset({
|
|
1685
|
+
graph,
|
|
1686
|
+
filename: file.filename,
|
|
1687
|
+
buffer,
|
|
1688
|
+
folderPath: folderPath || undefined,
|
|
1689
|
+
name: req.query.name?.trim() || undefined,
|
|
1690
|
+
user: reqGitUser(req),
|
|
1691
|
+
workspacePath: graph.getWorkspaceRoot(),
|
|
1692
|
+
});
|
|
1693
|
+
wsHub?.broadcast({
|
|
1694
|
+
type: 'repo_sync',
|
|
1695
|
+
repo,
|
|
1696
|
+
actor: req.user?.displayName ?? 'API',
|
|
1697
|
+
source: 'api',
|
|
1698
|
+
timestamp: new Date().toISOString(),
|
|
1699
|
+
});
|
|
1700
|
+
return reply.send({
|
|
1701
|
+
success: true,
|
|
1702
|
+
entity: {
|
|
1703
|
+
entityId: result.entityId,
|
|
1704
|
+
entityType: result.entityType,
|
|
1705
|
+
asset_url: result.asset_url,
|
|
1706
|
+
asset_kind: result.asset_kind,
|
|
1707
|
+
asset_size: result.asset_size,
|
|
1708
|
+
pages: result.pages,
|
|
1709
|
+
},
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
catch (err) {
|
|
1713
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1714
|
+
const status = msg.includes('not configured') ? 501
|
|
1715
|
+
: msg.includes('Unsupported asset extension') ? 415
|
|
1716
|
+
: 400;
|
|
1717
|
+
return reply.status(status).send({ success: false, error: msg });
|
|
1718
|
+
}
|
|
1719
|
+
});
|
|
662
1720
|
}
|
|
663
1721
|
//# sourceMappingURL=graph-api.js.map
|