studiograph 1.3.48-next.2 → 1.3.48-next.200
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 +574 -181
- package/dist/agent/orchestrator.js.map +1 -1
- package/dist/agent/prompts/entity-types-section.d.ts +42 -0
- package/dist/agent/prompts/entity-types-section.js +96 -0
- package/dist/agent/prompts/entity-types-section.js.map +1 -0
- package/dist/agent/prompts/system.md +147 -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 +673 -333
- 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.js +58 -9
- package/dist/agent/tools/web-tools.js.map +1 -1
- package/dist/cli/commands/about.d.ts +1 -0
- package/dist/cli/commands/about.js +55 -3
- package/dist/cli/commands/about.js.map +1 -1
- package/dist/cli/commands/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 +218 -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 +77 -0
- package/dist/core/embeds.js +147 -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 +142 -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/format-registry.d.ts +170 -0
- package/dist/core/format-registry.js +404 -0
- package/dist/core/format-registry.js.map +1 -0
- package/dist/core/graph.d.ts +731 -14
- package/dist/core/graph.js +2238 -313
- 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/refs.d.ts +41 -0
- package/dist/core/refs.js +80 -0
- package/dist/core/refs.js.map +1 -0
- package/dist/core/schema-registry.d.ts +82 -0
- package/dist/core/schema-registry.js +238 -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/tavily.d.ts +18 -0
- package/dist/core/secrets/probes/tavily.js +58 -0
- package/dist/core/secrets/probes/tavily.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 +616 -0
- package/dist/core/secrets/registry.js.map +1 -0
- package/dist/core/types.d.ts +80 -475
- package/dist/core/types.js +27 -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 +767 -1777
- package/dist/core/validation.js +463 -227
- package/dist/core/validation.js.map +1 -1
- package/dist/core/workspace-manager.d.ts +89 -12
- package/dist/core/workspace-manager.js +225 -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 +62 -0
- package/dist/mcp/state-signer.js +205 -0
- package/dist/mcp/state-signer.js.map +1 -0
- package/dist/mcp/tools.d.ts +35 -2
- package/dist/mcp/tools.js +1366 -112
- package/dist/mcp/tools.js.map +1 -1
- package/dist/server/__tests__/helpers/graph-api.d.ts +16 -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/asset-index-queue.d.ts +96 -0
- package/dist/server/asset-index-queue.js +191 -0
- package/dist/server/asset-index-queue.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 +22 -3
- package/dist/server/index.js +622 -198
- 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 +31 -0
- package/dist/server/routes/asset-api.js +593 -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 +573 -39
- 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-sessions-api.d.ts +9 -0
- package/dist/server/routes/chat-sessions-api.js +121 -0
- package/dist/server/routes/chat-sessions-api.js.map +1 -0
- package/dist/server/routes/chat.d.ts +12 -1
- package/dist/server/routes/chat.js +318 -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 +57 -0
- package/dist/server/routes/graph-api-access.js +112 -0
- package/dist/server/routes/graph-api-access.js.map +1 -0
- package/dist/server/routes/graph-api.d.ts +1 -1
- package/dist/server/routes/graph-api.js +1519 -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/import-batch.d.ts +24 -0
- package/dist/server/routes/import-batch.js +33 -0
- package/dist/server/routes/import-batch.js.map +1 -0
- package/dist/server/routes/insights-api.d.ts +9 -2
- package/dist/server/routes/insights-api.js +384 -38
- 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/oauth-api.d.ts +67 -0
- package/dist/server/routes/oauth-api.js +421 -0
- package/dist/server/routes/oauth-api.js.map +1 -0
- 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/share-api.d.ts +32 -0
- package/dist/server/routes/share-api.js +234 -0
- package/dist/server/routes/share-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 +75 -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 +129 -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 +68 -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 +165 -0
- package/dist/services/access-control.js.map +1 -0
- package/dist/services/asset-caption-service.d.ts +102 -0
- package/dist/services/asset-caption-service.js +184 -0
- package/dist/services/asset-caption-service.js.map +1 -0
- package/dist/services/asset-delete-authz.d.ts +31 -0
- package/dist/services/asset-delete-authz.js +61 -0
- package/dist/services/asset-delete-authz.js.map +1 -0
- package/dist/services/asset-finalize-service.d.ts +58 -0
- package/dist/services/asset-finalize-service.js +205 -0
- package/dist/services/asset-finalize-service.js.map +1 -0
- package/dist/services/asset-import-service.d.ts +111 -0
- package/dist/services/asset-import-service.js +394 -0
- package/dist/services/asset-import-service.js.map +1 -0
- package/dist/services/asset-index-sweep.d.ts +92 -0
- package/dist/services/asset-index-sweep.js +258 -0
- package/dist/services/asset-index-sweep.js.map +1 -0
- package/dist/services/asset-lifecycle-check.d.ts +22 -0
- package/dist/services/asset-lifecycle-check.js +80 -0
- package/dist/services/asset-lifecycle-check.js.map +1 -0
- package/dist/services/asset-listing.d.ts +72 -0
- package/dist/services/asset-listing.js +321 -0
- package/dist/services/asset-listing.js.map +1 -0
- package/dist/services/asset-presign-service.d.ts +68 -0
- package/dist/services/asset-presign-service.js +124 -0
- package/dist/services/asset-presign-service.js.map +1 -0
- package/dist/services/asset-sidecar-service.d.ts +30 -0
- package/dist/services/asset-sidecar-service.js +96 -0
- package/dist/services/asset-sidecar-service.js.map +1 -0
- package/dist/services/asset-upload-errors.d.ts +41 -0
- package/dist/services/asset-upload-errors.js +45 -0
- package/dist/services/asset-upload-errors.js.map +1 -0
- package/dist/services/asset-upload-service.d.ts +74 -0
- package/dist/services/asset-upload-service.js +244 -0
- package/dist/services/asset-upload-service.js.map +1 -0
- package/dist/services/asset-upload-token.d.ts +58 -0
- package/dist/services/asset-upload-token.js +75 -0
- package/dist/services/asset-upload-token.js.map +1 -0
- package/dist/services/assets/asset-index-service.d.ts +127 -0
- package/dist/services/assets/asset-index-service.js +227 -0
- package/dist/services/assets/asset-index-service.js.map +1 -0
- package/dist/services/assets/base.d.ts +118 -1
- package/dist/services/assets/base.js +77 -1
- package/dist/services/assets/base.js.map +1 -1
- package/dist/services/assets/index.d.ts +103 -1
- package/dist/services/assets/index.js +192 -0
- package/dist/services/assets/index.js.map +1 -1
- package/dist/services/assets/local.d.ts +16 -1
- package/dist/services/assets/local.js +110 -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 +87 -1
- package/dist/services/assets/r2.js +203 -1
- package/dist/services/assets/r2.js.map +1 -1
- package/dist/services/auth-service.d.ts +350 -36
- package/dist/services/auth-service.js +1179 -140
- 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/chat-session-service.d.ts +96 -0
- package/dist/services/chat-session-service.js +316 -0
- package/dist/services/chat-session-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/convert-service.d.ts +64 -0
- package/dist/services/convert-service.js +68 -0
- package/dist/services/convert-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 +210 -0
- package/dist/services/entity-mutations.js +377 -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/extract/docx.d.ts +1 -0
- package/dist/services/extract/docx.js +233 -0
- package/dist/services/extract/docx.js.map +1 -0
- package/dist/services/extract/index.d.ts +9 -0
- package/dist/services/extract/index.js +10 -0
- package/dist/services/extract/index.js.map +1 -0
- package/dist/services/extract/pdf.d.ts +13 -0
- package/dist/services/extract/pdf.js +69 -0
- package/dist/services/extract/pdf.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 +58 -3
- package/dist/services/import-service.js +316 -100
- package/dist/services/import-service.js.map +1 -1
- package/dist/services/lint-service.js +10 -17
- package/dist/services/lint-service.js.map +1 -1
- package/dist/services/markdown.d.ts +12 -1
- package/dist/services/markdown.js +77 -9
- 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/model-capabilities.d.ts +41 -0
- package/dist/services/model-capabilities.js +107 -0
- package/dist/services/model-capabilities.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/oauth-server-store.d.ts +147 -0
- package/dist/services/oauth-server-store.js +319 -0
- package/dist/services/oauth-server-store.js.map +1 -0
- package/dist/services/orphan-service.js +2 -2
- package/dist/services/orphan-service.js.map +1 -1
- package/dist/services/personal-folder.d.ts +40 -0
- package/dist/services/personal-folder.js +101 -0
- package/dist/services/personal-folder.js.map +1 -0
- package/dist/services/share-link-service.d.ts +57 -0
- package/dist/services/share-link-service.js +142 -0
- package/dist/services/share-link-service.js.map +1 -0
- package/dist/services/user-person-bridge.d.ts +136 -0
- package/dist/services/user-person-bridge.js +340 -0
- package/dist/services/user-person-bridge.js.map +1 -0
- package/dist/services/vector-service.d.ts +260 -6
- package/dist/services/vector-service.js +574 -31
- package/dist/services/vector-service.js.map +1 -1
- package/dist/services/workspace-access.d.ts +108 -0
- package/dist/services/workspace-access.js +149 -0
- package/dist/services/workspace-access.js.map +1 -0
- package/dist/services/workspace-assets-folder.d.ts +46 -0
- package/dist/services/workspace-assets-folder.js +75 -0
- package/dist/services/workspace-assets-folder.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.BZHoBJEc.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.BwBFdc1D.css +1 -0
- package/dist/web/_app/immutable/assets/15.CmLrbzZp.css +1 -0
- package/dist/web/_app/immutable/assets/16.B0GwXar3.css +1 -0
- package/dist/web/_app/immutable/assets/2.C9Vt4JHy.css +1 -0
- package/dist/web/_app/immutable/assets/3.BFvgCYSk.css +1 -0
- package/dist/web/_app/immutable/assets/4.B5mExQRc.css +1 -0
- package/dist/web/_app/immutable/assets/5.C6MySjPq.css +1 -0
- package/dist/web/_app/immutable/assets/BulkActionsBar.Bx7nxMjH.css +1 -0
- package/dist/web/_app/immutable/assets/CalendarView.lBCvS3X5.css +1 -0
- package/dist/web/_app/immutable/assets/CodeMirrorEditor.D6hf2pNJ.css +1 -0
- package/dist/web/_app/immutable/assets/CopyField.DVGZtw4U.css +1 -0
- package/dist/web/_app/immutable/assets/FolderMembersPanel.DX1oBeF_.css +1 -0
- package/dist/web/_app/immutable/assets/FolderMutationDialogs.D2B7KrRn.css +1 -0
- package/dist/web/_app/immutable/assets/FolderPicker.B5KdvRYl.css +1 -0
- package/dist/web/_app/immutable/assets/GalleryView.CF48FWYQ.css +1 -0
- package/dist/web/_app/immutable/assets/GraphView.BJtGL9py.css +1 -0
- package/dist/web/_app/immutable/assets/KanbanView.BOaI5KFL.css +1 -0
- package/dist/web/_app/immutable/assets/Link.vV0ne105.css +1 -0
- package/dist/web/_app/immutable/assets/Markdown.BqgpZCv5.css +1 -0
- package/dist/web/_app/immutable/assets/Rail.f8lsriOT.css +1 -0
- package/dist/web/_app/immutable/assets/SettingsDialog.DwMus2u2.css +1 -0
- package/dist/web/_app/immutable/assets/Spinner.UBfl0pab.css +1 -0
- package/dist/web/_app/immutable/assets/StopFilledAlt.fNlDN65D.css +1 -0
- package/dist/web/_app/immutable/assets/TimelineView.DriOGAKB.css +1 -0
- package/dist/web/_app/immutable/assets/WorkspaceView.u5VdwRR6.css +1 -0
- package/dist/web/_app/immutable/assets/button.BlLm4Dg1.css +1 -0
- package/dist/web/_app/immutable/assets/checkbox.DK0MgYgb.css +1 -0
- package/dist/web/_app/immutable/assets/dist.DDCWxn3B.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.DiLB0G5L.css +1 -0
- package/dist/web/_app/immutable/assets/select.BFL238eD.css +1 -0
- package/dist/web/_app/immutable/assets/sheet.4rY4hpfJ.css +1 -0
- package/dist/web/_app/immutable/chunks/-XvFB-zz2.js +2 -0
- package/dist/web/_app/immutable/chunks/-oRd_ir9.js +1 -0
- package/dist/web/_app/immutable/chunks/4XSri7g0.js +1 -0
- package/dist/web/_app/immutable/chunks/4iaT0dG72.js +1 -0
- package/dist/web/_app/immutable/chunks/5_TKU9QS2.js +1 -0
- package/dist/web/_app/immutable/chunks/6-29uWv0.js +1 -0
- package/dist/web/_app/immutable/chunks/8Yh26Z9_2.js +1 -0
- package/dist/web/_app/immutable/chunks/8q1f96y1.js +1 -0
- package/dist/web/_app/immutable/chunks/B-Z_xZy_.js +4 -0
- package/dist/web/_app/immutable/chunks/B1RnjnNt.js +1 -0
- package/dist/web/_app/immutable/chunks/B21rwvCB.js +1 -0
- package/dist/web/_app/immutable/chunks/B2n86UNJ.js +1 -0
- package/dist/web/_app/immutable/chunks/B318GEUB2.js +1 -0
- package/dist/web/_app/immutable/chunks/B6Lt-qaJ.js +1 -0
- package/dist/web/_app/immutable/chunks/BADt8daH.js +2 -0
- package/dist/web/_app/immutable/chunks/BBGt-XQo.js +3 -0
- package/dist/web/_app/immutable/chunks/BCCSYbMx.js +2 -0
- package/dist/web/_app/immutable/chunks/BDIUHcEz.js +1 -0
- package/dist/web/_app/immutable/chunks/BDSuq3KH.js +1 -0
- package/dist/web/_app/immutable/chunks/BGiLU5VN.js +1 -0
- package/dist/web/_app/immutable/chunks/BJgOZTHR.js +1 -0
- package/dist/web/_app/immutable/chunks/BN8IRueA2.js +1 -0
- package/dist/web/_app/immutable/chunks/BRRoo378.js +2 -0
- package/dist/web/_app/immutable/chunks/BVPNAzAK.js +1 -0
- package/dist/web/_app/immutable/chunks/BZ7qh0of.js +1 -0
- package/dist/web/_app/immutable/chunks/BaMo5OWI.js +1 -0
- package/dist/web/_app/immutable/chunks/BdYQewoD.js +1 -0
- package/dist/web/_app/immutable/chunks/BgRT-K6w.js +5 -0
- package/dist/web/_app/immutable/chunks/BibveGGd.js +1 -0
- package/dist/web/_app/immutable/chunks/BoDmwOTJ.js +1 -0
- package/dist/web/_app/immutable/chunks/BoglwbZW.js +1 -0
- package/dist/web/_app/immutable/chunks/Bqm_YrIg2.js +23 -0
- package/dist/web/_app/immutable/chunks/Bw3ftRwD2.js +1 -0
- package/dist/web/_app/immutable/chunks/C7-8fYBp.js +2 -0
- package/dist/web/_app/immutable/chunks/C84_yofr.js +1 -0
- package/dist/web/_app/immutable/chunks/CAXXrbj52.js +1 -0
- package/dist/web/_app/immutable/chunks/CEniyZ1Z2.js +6 -0
- package/dist/web/_app/immutable/chunks/CJhElIkU2.js +1 -0
- package/dist/web/_app/immutable/chunks/CK8tIaYT.js +1 -0
- package/dist/web/_app/immutable/chunks/CO2DvORW.js +2 -0
- package/dist/web/_app/immutable/chunks/CQDB9Yvp.js +1 -0
- package/dist/web/_app/immutable/chunks/CTzjJm_m2.js +1 -0
- package/dist/web/_app/immutable/chunks/CUXqa1nn2.js +1 -0
- package/dist/web/_app/immutable/chunks/CY0iY_WO.js +1 -0
- package/dist/web/_app/immutable/chunks/CYY3Iy2G.js +1 -0
- package/dist/web/_app/immutable/chunks/CbCfNNV_2.js +2692 -0
- package/dist/web/_app/immutable/chunks/CbuGIyDu2.js +22 -0
- package/dist/web/_app/immutable/chunks/CcXmQaYs2.js +1 -0
- package/dist/web/_app/immutable/chunks/CcaQuaSt2.js +1 -0
- package/dist/web/_app/immutable/chunks/CoHaBDpN2.js +2 -0
- package/dist/web/_app/immutable/chunks/Cvbd3myc2.js +14 -0
- package/dist/web/_app/immutable/chunks/CwgiPHda.js +2 -0
- package/dist/web/_app/immutable/chunks/Cwt5tUMq.js +1 -0
- package/dist/web/_app/immutable/chunks/CyGDzYpQ2.js +3 -0
- package/dist/web/_app/immutable/chunks/D-HVAF_w.js +2 -0
- package/dist/web/_app/immutable/chunks/D20C1thX2.js +1 -0
- package/dist/web/_app/immutable/chunks/D3JoMtWC.js +7 -0
- package/dist/web/_app/immutable/chunks/D3ch71s_.js +1 -0
- package/dist/web/_app/immutable/chunks/DBFijAmG.js +1 -0
- package/dist/web/_app/immutable/chunks/DJAStLNu.js +1 -0
- package/dist/web/_app/immutable/chunks/DMZS8I6d.js +7 -0
- package/dist/web/_app/immutable/chunks/DO_sw49q2.js +1 -0
- package/dist/web/_app/immutable/chunks/DP69PpIj.js +1 -0
- package/dist/web/_app/immutable/chunks/DQMWa2t7.js +1 -0
- package/dist/web/_app/immutable/chunks/DT0wzK622.js +1 -0
- package/dist/web/_app/immutable/chunks/DTnEJArL.js +1 -0
- package/dist/web/_app/immutable/chunks/DToS-G202.js +1 -0
- package/dist/web/_app/immutable/chunks/DV0hSm5g.js +2 -0
- package/dist/web/_app/immutable/chunks/DZcU6mGe2.js +1 -0
- package/dist/web/_app/immutable/chunks/DbslMybD.js +1 -0
- package/dist/web/_app/immutable/chunks/DcGThsSF.js +1 -0
- package/dist/web/_app/immutable/chunks/DcPrzUMJ.js +1 -0
- package/dist/web/_app/immutable/chunks/De0g5qop2.js +1 -0
- package/dist/web/_app/immutable/chunks/DeGcz9JU.js +1 -0
- package/dist/web/_app/immutable/chunks/DimS8dbZ2.js +1 -0
- package/dist/web/_app/immutable/chunks/DjuqhOjF.js +1 -0
- package/dist/web/_app/immutable/chunks/DnIQzIOz.js +1 -0
- package/dist/web/_app/immutable/chunks/DnfCmXUt.js +1 -0
- package/dist/web/_app/immutable/chunks/DnrImJan.js +1 -0
- package/dist/web/_app/immutable/chunks/Do09oEro2.js +2 -0
- package/dist/web/_app/immutable/chunks/Dpuq25vu2.js +1 -0
- package/dist/web/_app/immutable/chunks/DrSaepAD2.js +1 -0
- package/dist/web/_app/immutable/chunks/Dw-frCfE.js +1 -0
- package/dist/web/_app/immutable/chunks/Dz34u8Ie2.js +185 -0
- package/dist/web/_app/immutable/chunks/DzrRTvug.js +1 -0
- package/dist/web/_app/immutable/chunks/E5LDmXgv.js +1 -0
- package/dist/web/_app/immutable/chunks/FCTxXqoV.js +1 -0
- package/dist/web/_app/immutable/chunks/FLoGUKj7.js +2 -0
- package/dist/web/_app/immutable/chunks/F_crThMu2.js +1 -0
- package/dist/web/_app/immutable/chunks/GRN8IpJ0.js +2 -0
- package/dist/web/_app/immutable/chunks/J5nb9xxj.js +1 -0
- package/dist/web/_app/immutable/chunks/JCjEwqH9.js +1 -0
- package/dist/web/_app/immutable/chunks/JN_nsXUm2.js +1 -0
- package/dist/web/_app/immutable/chunks/JS2_GaB1.js +1 -0
- package/dist/web/_app/immutable/chunks/N6vnY5Si2.js +1 -0
- package/dist/web/_app/immutable/chunks/PKzH7OBb2.js +1 -0
- package/dist/web/_app/immutable/chunks/Wowi_89k.js +5 -0
- package/dist/web/_app/immutable/chunks/Y_asqJqs2.js +83 -0
- package/dist/web/_app/immutable/chunks/Yhg5Q4MT.js +1 -0
- package/dist/web/_app/immutable/chunks/cHSFvkPC.js +3 -0
- package/dist/web/_app/immutable/chunks/f_eYccPf2.js +1 -0
- package/dist/web/_app/immutable/chunks/hm_8RsuL.js +1 -0
- package/dist/web/_app/immutable/chunks/iPCkjliu.js +3 -0
- package/dist/web/_app/immutable/chunks/j2AvgZGz.js +1 -0
- package/dist/web/_app/immutable/chunks/kNaey6uv.js +1 -0
- package/dist/web/_app/immutable/chunks/kWdKf0Ej.js +8 -0
- package/dist/web/_app/immutable/chunks/l1761xxj.js +1 -0
- package/dist/web/_app/immutable/chunks/liGRFQqV2.js +184 -0
- package/dist/web/_app/immutable/chunks/lq1kR86d.js +1 -0
- package/dist/web/_app/immutable/chunks/mIykYR4F2.js +1 -0
- package/dist/web/_app/immutable/chunks/piVEn8CZ.js +66 -0
- package/dist/web/_app/immutable/chunks/rbhlO_t8.js +1 -0
- package/dist/web/_app/immutable/chunks/rgZlSXXV.js +1 -0
- package/dist/web/_app/immutable/chunks/rx_WUKwk2.js +1 -0
- package/dist/web/_app/immutable/chunks/u8G2EUvz2.js +224 -0
- package/dist/web/_app/immutable/chunks/vgsM0KTm2.js +1 -0
- package/dist/web/_app/immutable/chunks/xcGI4fuV.js +1 -0
- package/dist/web/_app/immutable/chunks/xihTtKlq.js +1 -0
- package/dist/web/_app/immutable/chunks/yEYlhV9G2.js +1 -0
- package/dist/web/_app/immutable/entry/app.CusiWZov.js +2 -0
- package/dist/web/_app/immutable/entry/start.DrLcQ4Lx.js +1 -0
- package/dist/web/_app/immutable/nodes/0.CXwev3zA.js +2 -0
- package/dist/web/_app/immutable/nodes/1.D93E26FU.js +1 -0
- package/dist/web/_app/immutable/nodes/10.BSkeTxej.js +1 -0
- package/dist/web/_app/immutable/nodes/11.CRoiFE32.js +1 -0
- package/dist/web/_app/immutable/nodes/12.Bcwm3-AY.js +1 -0
- package/dist/web/_app/immutable/nodes/13.BVWnarZm.js +1 -0
- package/dist/web/_app/immutable/nodes/14.D_x8-nk9.js +1 -0
- package/dist/web/_app/immutable/nodes/15.B_ReGv9R.js +1 -0
- package/dist/web/_app/immutable/nodes/16.DiOHgg-l.js +77 -0
- package/dist/web/_app/immutable/nodes/2.bwyeAMyx.js +109 -0
- package/dist/web/_app/immutable/nodes/3.CyIpLMSo.js +6 -0
- package/dist/web/_app/immutable/nodes/4.CBxNEgp_.js +11 -0
- package/dist/web/_app/immutable/nodes/5.BV-6fNbn.js +5 -0
- package/dist/web/_app/immutable/nodes/6.BanV644Z.js +1 -0
- package/dist/web/_app/immutable/nodes/7.B7rO4HSm.js +1 -0
- package/dist/web/_app/immutable/nodes/8.BdssFp4B.js +1 -0
- package/dist/web/_app/immutable/nodes/9.B_TACjMT.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 +58 -19
- package/dist/web/studiograph-logomark.svg +29 -0
- package/package.json +38 -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/-iaeBIWN.js +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/B340SUKR.js +0 -1
- package/dist/web/_app/immutable/chunks/B3Z4_Sps.js +0 -1
- package/dist/web/_app/immutable/chunks/BB_5th5W.js +0 -3383
- package/dist/web/_app/immutable/chunks/BPWYOQXG.js +0 -1
- package/dist/web/_app/immutable/chunks/BR77jbfR.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/Bq_KShUL.js +0 -4
- package/dist/web/_app/immutable/chunks/Bsacq2UX.js +0 -1
- package/dist/web/_app/immutable/chunks/ByunV8un.js +0 -1
- package/dist/web/_app/immutable/chunks/BzPu-Eht.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/CGc1sIb_.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/CX87mIM8.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/Cpsr8-Xy.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/DTZE-yoq.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/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.BeUKh4sD.js +0 -2
- package/dist/web/_app/immutable/entry/start.CZJnFuQn.js +0 -1
- package/dist/web/_app/immutable/nodes/0.DyF_RsLg.js +0 -2
- package/dist/web/_app/immutable/nodes/1.7iDQmMPh.js +0 -1
- package/dist/web/_app/immutable/nodes/10.Btl8d8HT.js +0 -1
- package/dist/web/_app/immutable/nodes/2.LDRE62jN.js +0 -168
- package/dist/web/_app/immutable/nodes/3.BaW1XB1x.js +0 -1
- package/dist/web/_app/immutable/nodes/4.D0Q8kpkI.js +0 -12
- package/dist/web/_app/immutable/nodes/5.n7j2UG9h.js +0 -4
- package/dist/web/_app/immutable/nodes/6.DQYxR0iy.js +0 -2
- package/dist/web/_app/immutable/nodes/7.BsURvFAp.js +0 -2
- package/dist/web/_app/immutable/nodes/8.CsiErG0p.js +0 -1
- package/dist/web/_app/immutable/nodes/9.CnN6OZgC.js +0 -1
|
@@ -2,135 +2,61 @@
|
|
|
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
|
-
if (!repo.private)
|
|
21
|
-
return true;
|
|
22
|
-
if (!user)
|
|
23
|
-
return !repo.owner_id; // API key / open mode — only if no owner set
|
|
24
|
-
if (!repo.owner_id)
|
|
25
|
-
return false; // Private without owner — hidden from all users
|
|
26
|
-
return user.id === repo.owner_id;
|
|
27
|
-
}
|
|
28
|
-
/** Get the list of repos the current user can access. */
|
|
29
|
-
function getAccessibleRepos(req, workspaceManager, authService) {
|
|
30
|
-
const allRepos = workspaceManager.getAllRepoConfigs();
|
|
31
|
-
const user = req.user;
|
|
32
|
-
// Filter out private repos owned by other users (applies to all roles)
|
|
33
|
-
const visible = allRepos.filter(r => canSeePrivateRepo(r, user));
|
|
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
|
-
}
|
|
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, getContentRepos, 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 { assertEntityRefs, EntityRefError, UnknownUserRefError } from '../../services/entity-mutations.js';
|
|
19
|
+
import { assetDeleteDenial } from '../../services/asset-delete-authz.js';
|
|
20
|
+
import { clearLegacyBucketCache } from '../../services/asset-listing.js';
|
|
21
|
+
import { log } from '../../utils/log.js';
|
|
97
22
|
export async function registerGraphApiRoutes(fastify, workspaceManager, authService, wsHub, sessionManager) {
|
|
98
|
-
//
|
|
23
|
+
// ARCHITECTURAL BOUNDARY: every per-route graph resolution flows through
|
|
24
|
+
// this service so the act of getting a graph IS the access check. Do not
|
|
25
|
+
// call `workspaceManager.getGraph(repo)` from a request-scoped handler —
|
|
26
|
+
// use `accessService.graphFor*(reqUser(req), repo)` instead. A security
|
|
27
|
+
// audit (2026-05) flagged the previous opt-in pattern (route had to
|
|
28
|
+
// remember `hasRepoAccess()` before `getGraph()`) as an architectural
|
|
29
|
+
// risk; if you find yourself reaching for the raw WM here, that's the
|
|
30
|
+
// smell the audit was about.
|
|
31
|
+
const accessService = new WorkspaceAccessService(workspaceManager, authService);
|
|
32
|
+
const graphForRead = (req, reply, repoName) => resolveGraphForRead(accessService, req, reply, repoName);
|
|
33
|
+
const graphForWrite = (req, reply, repoName) => resolveGraphForWrite(accessService, req, reply, repoName);
|
|
34
|
+
const graphForAdmin = (req, reply, repoName) => resolveGraphForAdmin(accessService, req, reply, repoName);
|
|
35
|
+
// POST /api/repos — any authenticated user can create a folder.
|
|
36
|
+
// The creator is granted folder-admin access automatically.
|
|
37
|
+
// Shared logic lives in services/folder-creation.ts so the MCP tool
|
|
38
|
+
// create_repo behaves identically.
|
|
99
39
|
fastify.post('/api/repos', async (req, reply) => {
|
|
100
40
|
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
|
-
}
|
|
41
|
+
const { name, display_name, description } = req.body || {};
|
|
109
42
|
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
|
-
});
|
|
43
|
+
const repo = await createSharedFolder(workspaceManager, authService, { name, display_name, description }, user, wsHub);
|
|
124
44
|
return reply.status(201).send(repo);
|
|
125
45
|
}
|
|
126
46
|
catch (err) {
|
|
127
|
-
|
|
47
|
+
if (err instanceof FolderCreationError) {
|
|
48
|
+
const status = err.code === 'invalid_name' ? 400
|
|
49
|
+
: err.code === 'duplicate' ? 409
|
|
50
|
+
: 500;
|
|
51
|
+
return reply.status(status).send({ error: err.message });
|
|
52
|
+
}
|
|
53
|
+
return reply.status(500).send({ error: err?.message ?? String(err) });
|
|
128
54
|
}
|
|
129
55
|
});
|
|
130
|
-
// DELETE /api/repos/:repo —
|
|
56
|
+
// DELETE /api/repos/:repo — folder admin or workspace owner only.
|
|
131
57
|
fastify.delete('/api/repos/:repo', async (req, reply) => {
|
|
132
|
-
if (!
|
|
133
|
-
return reply.status(403).send({ error: '
|
|
58
|
+
if (!isFolderAdminOrOwner(req, req.params.repo, workspaceManager, authService)) {
|
|
59
|
+
return reply.status(403).send({ error: 'Folder admin access required' });
|
|
134
60
|
}
|
|
135
61
|
const { repo } = req.params;
|
|
136
62
|
try {
|
|
@@ -138,14 +64,19 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
138
64
|
const config = await workspace.loadConfig();
|
|
139
65
|
const repoIndex = config.repos.findIndex(r => r.name === repo);
|
|
140
66
|
if (repoIndex === -1) {
|
|
141
|
-
return reply.status(404).send({ error: `
|
|
67
|
+
return reply.status(404).send({ error: `Folder \u201C${repo}\u201D not found` });
|
|
142
68
|
}
|
|
143
69
|
const repoConfig = config.repos[repoIndex];
|
|
70
|
+
// System folders (the reserved Workspace Assets store) are an app
|
|
71
|
+
// invariant — undeletable, so nothing needs to recreate them on boot.
|
|
72
|
+
if (repoConfig.system) {
|
|
73
|
+
return reply.status(403).send({ error: 'This is a system folder and cannot be deleted.' });
|
|
74
|
+
}
|
|
144
75
|
const repoPath = join(workspaceManager['workspacePath'], repoConfig.path);
|
|
145
76
|
// Remove from config
|
|
146
77
|
config.repos.splice(repoIndex, 1);
|
|
147
78
|
workspace.writeConfig(config);
|
|
148
|
-
workspace.pushWorkspaceConfig(`Remove
|
|
79
|
+
workspace.pushWorkspaceConfig(`Remove folder: ${repo}`);
|
|
149
80
|
// Delete files
|
|
150
81
|
const { rmSync } = await import('fs');
|
|
151
82
|
if (existsSync(repoPath)) {
|
|
@@ -153,7 +84,7 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
153
84
|
}
|
|
154
85
|
workspaceManager.reloadConfig();
|
|
155
86
|
wsHub?.broadcast({
|
|
156
|
-
type: '
|
|
87
|
+
type: 'folder_deleted', repo,
|
|
157
88
|
actor: req.user?.displayName ?? 'API',
|
|
158
89
|
timestamp: new Date().toISOString(),
|
|
159
90
|
});
|
|
@@ -163,16 +94,16 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
163
94
|
return reply.status(500).send({ error: err.message });
|
|
164
95
|
}
|
|
165
96
|
});
|
|
166
|
-
// PATCH /api/repos/:repo — update
|
|
97
|
+
// PATCH /api/repos/:repo — rename/update sync rules. Folder admin or workspace owner only.
|
|
167
98
|
fastify.patch('/api/repos/:repo', async (req, reply) => {
|
|
168
|
-
if (!
|
|
169
|
-
return reply.status(403).send({ error: '
|
|
99
|
+
if (!isFolderAdminOrOwner(req, req.params.repo, workspaceManager, authService)) {
|
|
100
|
+
return reply.status(403).send({ error: 'Folder admin access required' });
|
|
170
101
|
}
|
|
171
102
|
const { repo } = req.params;
|
|
172
103
|
const body = req.body || {};
|
|
173
104
|
const { name: newName, display_name: newDisplayName, sync: newSync, ...rest } = body;
|
|
174
|
-
if ('
|
|
175
|
-
return reply.status(400).send({ error: 'The
|
|
105
|
+
if ('personal' in rest) {
|
|
106
|
+
return reply.status(400).send({ error: 'The personal flag is immutable after creation' });
|
|
176
107
|
}
|
|
177
108
|
if (newName === undefined && newSync === undefined && newDisplayName === undefined) {
|
|
178
109
|
return reply.status(400).send({ error: 'Nothing to update' });
|
|
@@ -185,12 +116,17 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
185
116
|
const config = await workspace.loadConfig();
|
|
186
117
|
const repoConfig = config.repos.find(r => r.name === repo);
|
|
187
118
|
if (!repoConfig) {
|
|
188
|
-
return reply.status(404).send({ error: `
|
|
119
|
+
return reply.status(404).send({ error: `Folder \u201C${repo}\u201D not found` });
|
|
120
|
+
}
|
|
121
|
+
// System folders (the reserved Workspace Assets store) have a reserved
|
|
122
|
+
// name + fixed display; renaming/reconfiguring them is not allowed.
|
|
123
|
+
if (repoConfig.system) {
|
|
124
|
+
return reply.status(403).send({ error: 'This is a system folder and cannot be renamed or reconfigured.' });
|
|
189
125
|
}
|
|
190
126
|
// Handle rename
|
|
191
127
|
if (newName !== undefined) {
|
|
192
128
|
if (config.repos.some(r => r.name === newName)) {
|
|
193
|
-
return reply.status(409).send({ error: `
|
|
129
|
+
return reply.status(409).send({ error: `Folder \u201C${newName}\u201D already exists` });
|
|
194
130
|
}
|
|
195
131
|
const wsPath = workspaceManager['workspacePath'];
|
|
196
132
|
const oldPath = join(wsPath, repoConfig.path);
|
|
@@ -209,8 +145,8 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
209
145
|
if (existsSync(markerPath)) {
|
|
210
146
|
writeFS(markerPath, JSON.stringify({ repo_name: newName }, null, 2), 'utf-8');
|
|
211
147
|
}
|
|
212
|
-
// Update
|
|
213
|
-
authService.
|
|
148
|
+
// Update folder_access to follow the rename
|
|
149
|
+
authService.renameFolder(repo, newName);
|
|
214
150
|
}
|
|
215
151
|
// Handle display name update (without rename)
|
|
216
152
|
if (newDisplayName !== undefined && newName === undefined) {
|
|
@@ -221,7 +157,7 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
221
157
|
repoConfig.sync = newSync.length > 0 ? newSync : undefined;
|
|
222
158
|
}
|
|
223
159
|
workspace.writeConfig(config);
|
|
224
|
-
workspace.pushWorkspaceConfig(newName !== undefined ? `Rename
|
|
160
|
+
workspace.pushWorkspaceConfig(newName !== undefined ? `Rename folder: ${repo} → ${newName}` : `Update folder: ${repo}`);
|
|
225
161
|
workspaceManager.reloadConfig();
|
|
226
162
|
return reply.send(repoConfig);
|
|
227
163
|
}
|
|
@@ -229,50 +165,128 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
229
165
|
return reply.status(500).send({ error: err.message });
|
|
230
166
|
}
|
|
231
167
|
});
|
|
232
|
-
// GET /api/repos — list accessible repos (re-reads config to pick up CLI changes)
|
|
168
|
+
// GET /api/repos — list accessible repos (re-reads config to pick up CLI changes).
|
|
169
|
+
// Augments each repo with the caller's folder role ('admin' | 'editor'),
|
|
170
|
+
// member_count, entity_types (distinct types present), and folders (the
|
|
171
|
+
// flat list of nested-folder paths in the repo, used by the sidebar to
|
|
172
|
+
// render the active-path expansion).
|
|
233
173
|
fastify.get('/api/repos', async (req, reply) => {
|
|
234
174
|
workspaceManager.reloadConfig();
|
|
235
|
-
|
|
236
|
-
|
|
175
|
+
// System folders (the reserved Workspace Assets store) are accessible but
|
|
176
|
+
// excluded from the sidebar — they're surfaced via the asset manager, not
|
|
177
|
+
// the folder list. They stay in getAccessibleRepos so authz + the asset
|
|
178
|
+
// manager (/api/workspace/assets) still see them.
|
|
179
|
+
const repos = getContentRepos(req, workspaceManager, authService);
|
|
180
|
+
const user = req.user;
|
|
181
|
+
const enriched = repos.map(r => {
|
|
182
|
+
const memberCount = authService.getFolderMembers(r.name).length;
|
|
183
|
+
const callerRole = user ? authService.getFolderRole(user.id, r.name) : null;
|
|
184
|
+
let entityTypes = [];
|
|
185
|
+
// `folders` carries the per-folder display_name override (when one
|
|
186
|
+
// exists in the folder's .studiograph-folder.json sidecar); the client
|
|
187
|
+
// falls back to toDisplayName(slug) when display_name is absent.
|
|
188
|
+
let folders = [];
|
|
189
|
+
try {
|
|
190
|
+
// Direct WM access OK here — `repos` came from getAccessibleRepos
|
|
191
|
+
// above, so every iteration is over a folder the caller can read.
|
|
192
|
+
const graph = workspaceManager.getGraph(r.name);
|
|
193
|
+
entityTypes = graph.listEntityTypes();
|
|
194
|
+
folders = graph.listFolders().map(f => {
|
|
195
|
+
const entry = { path: f.path };
|
|
196
|
+
if (f.display_name)
|
|
197
|
+
entry.display_name = f.display_name;
|
|
198
|
+
if (f.id)
|
|
199
|
+
entry.id = f.id;
|
|
200
|
+
return entry;
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
catch { /* repo not mounted */ }
|
|
204
|
+
return {
|
|
205
|
+
...r,
|
|
206
|
+
caller_role: callerRole,
|
|
207
|
+
member_count: memberCount,
|
|
208
|
+
entity_types: entityTypes,
|
|
209
|
+
folders,
|
|
210
|
+
};
|
|
211
|
+
});
|
|
212
|
+
return reply.send(enriched);
|
|
237
213
|
});
|
|
214
|
+
// GET /api/admin/folders — workspace-owner-only metadata list of every folder
|
|
215
|
+
// (name, display_name, creator, member count). No content access implied —
|
|
216
|
+
// owners still need to join a folder to read its entries.
|
|
217
|
+
fastify.get('/api/admin/folders', async (req, reply) => {
|
|
218
|
+
const user = req.user;
|
|
219
|
+
if (user && !isWorkspaceOwner(user.role)) {
|
|
220
|
+
return reply.status(403).send({ error: 'Workspace owner access required' });
|
|
221
|
+
}
|
|
222
|
+
workspaceManager.reloadConfig();
|
|
223
|
+
const all = workspaceManager.getAllRepoConfigs();
|
|
224
|
+
const result = all.map(r => {
|
|
225
|
+
const members = authService.getFolderMembers(r.name);
|
|
226
|
+
const admins = members.filter(m => m.role === 'admin');
|
|
227
|
+
const creator = r.owner_id
|
|
228
|
+
? members.find(m => m.user.id === r.owner_id)?.user
|
|
229
|
+
: admins[0]?.user;
|
|
230
|
+
let entryCount = 0;
|
|
231
|
+
// Direct WM access OK — this endpoint is owner-only (gated above) and
|
|
232
|
+
// returns metadata for every folder the workspace owner needs to see
|
|
233
|
+
// for admin purposes. No content is leaked: only entry counts.
|
|
234
|
+
try {
|
|
235
|
+
entryCount = workspaceManager.getGraph(r.name).search({}).length;
|
|
236
|
+
}
|
|
237
|
+
catch { /* skip */ }
|
|
238
|
+
return {
|
|
239
|
+
name: r.name,
|
|
240
|
+
display_name: r.display_name ?? r.name,
|
|
241
|
+
description: r.description,
|
|
242
|
+
member_count: members.length,
|
|
243
|
+
admin_count: admins.length,
|
|
244
|
+
entry_count: entryCount,
|
|
245
|
+
creator: creator ? { id: creator.id, displayName: creator.displayName, email: creator.email } : null,
|
|
246
|
+
caller_is_member: user ? members.some(m => m.user.id === user.id) : true,
|
|
247
|
+
};
|
|
248
|
+
});
|
|
249
|
+
return reply.send(result);
|
|
250
|
+
});
|
|
251
|
+
// Build the lightweight summary objects used by /entities?summary=true.
|
|
252
|
+
// Extracted so the per-repo and bulk endpoints share the same shape.
|
|
253
|
+
const EXCLUDED_SUMMARY_FIELDS = new Set([
|
|
254
|
+
'entity_type', 'entity_id', 'created_by', 'updated_by',
|
|
255
|
+
'description', 'content', 'context', 'rationale', 'alternatives', 'consequences',
|
|
256
|
+
'steps', 'guidelines', 'bio', 'agenda', 'summary', 'decision', 'action_items',
|
|
257
|
+
]);
|
|
258
|
+
function buildSummaries(entities, accessibleIds) {
|
|
259
|
+
return entities.map(e => {
|
|
260
|
+
const extra = Object.fromEntries(Object.entries(e.data ?? {}).filter(([k, v]) => !EXCLUDED_SUMMARY_FIELDS.has(k) && k !== 'name' && k !== 'created_at' && k !== 'updated_at' &&
|
|
261
|
+
(typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || Array.isArray(v) || v instanceof Date)));
|
|
262
|
+
return {
|
|
263
|
+
id: e.id,
|
|
264
|
+
entityType: e.entityType,
|
|
265
|
+
folder_path: e.folder_path ?? '',
|
|
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
|
+
}
|
|
238
276
|
// GET /api/repos/:repo/entities — list all entities in a repo
|
|
239
277
|
// ?summary=true returns lightweight objects (no content, minimal data) for sidebar/graph
|
|
240
278
|
fastify.get('/api/repos/:repo/entities', async (req, reply) => {
|
|
241
279
|
const { repo } = req.params;
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
catch (err) {
|
|
250
|
-
return reply.status(404).send({ error: err.message });
|
|
251
|
-
}
|
|
280
|
+
// Access enforced by graphForRead \u2014 do not call workspaceManager.getGraph
|
|
281
|
+
// directly here. The audit (2026-05) flagged direct WM access from routes
|
|
282
|
+
// as the primary architectural risk; the service is the gate.
|
|
283
|
+
const graph = graphForRead(req, reply, repo);
|
|
284
|
+
if (!graph)
|
|
285
|
+
return;
|
|
252
286
|
const entities = graph.search({});
|
|
253
287
|
const accessibleIds = getAccessibleEntityIds(req, workspaceManager, authService);
|
|
254
288
|
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);
|
|
289
|
+
return reply.send(buildSummaries(entities, accessibleIds));
|
|
276
290
|
}
|
|
277
291
|
// Full response — filter wikilinks in document
|
|
278
292
|
const filtered = entities.map(e => {
|
|
@@ -282,19 +296,57 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
282
296
|
});
|
|
283
297
|
return reply.send(filtered);
|
|
284
298
|
});
|
|
299
|
+
// GET /api/workspace/entities?summary=true — bulk entity summaries for ALL
|
|
300
|
+
// accessible repos in a single round-trip. Replaces 15+ parallel
|
|
301
|
+
// /api/repos/:repo/entities?summary=true calls. Important on dev where each
|
|
302
|
+
// request has Vite middleware overhead and the browser's HTTP/1.1 6-slot
|
|
303
|
+
// limit serializes them, blocking other navigation fetches.
|
|
304
|
+
// Response shape: `{ [repoName]: EntitySummary[] }`.
|
|
305
|
+
fastify.get('/api/workspace/entities', async (req, reply) => {
|
|
306
|
+
// Mirror the per-repo endpoint's contract: only summary=true is supported
|
|
307
|
+
// here. Returning full bodies for the whole workspace would be huge and
|
|
308
|
+
// isn't a use case we have today.
|
|
309
|
+
if (req.query.summary !== 'true') {
|
|
310
|
+
return reply.status(400).send({ error: 'summary=true is required for the bulk entities endpoint' });
|
|
311
|
+
}
|
|
312
|
+
const repos = getContentRepos(req, workspaceManager, authService);
|
|
313
|
+
// Single-pass: collect the per-repo entity arrays AND the accessible-ids
|
|
314
|
+
// set in one walk. The previous shape called graph.search({}) twice per
|
|
315
|
+
// repo (once via getAccessibleEntityIds, once in this loop), doubling
|
|
316
|
+
// disk I/O on the most-trafficked endpoint.
|
|
317
|
+
const user = reqUser(req);
|
|
318
|
+
const collectIds = user !== undefined; // unauthenticated → no wikilink filtering
|
|
319
|
+
const accessibleIds = collectIds ? new Set() : null;
|
|
320
|
+
const perRepoEntities = [];
|
|
321
|
+
for (const r of repos) {
|
|
322
|
+
let graph;
|
|
323
|
+
// Direct WM access OK — `repos` is the access-filtered list above.
|
|
324
|
+
try {
|
|
325
|
+
graph = workspaceManager.getGraph(r.name);
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
continue;
|
|
329
|
+
} // skip unmounted repos rather than failing the whole request
|
|
330
|
+
const entities = graph.search({});
|
|
331
|
+
perRepoEntities.push({ name: r.name, entities });
|
|
332
|
+
if (accessibleIds) {
|
|
333
|
+
for (const e of entities)
|
|
334
|
+
accessibleIds.add(e.id);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const result = {};
|
|
338
|
+
for (const { name, entities } of perRepoEntities) {
|
|
339
|
+
result[name] = buildSummaries(entities, accessibleIds);
|
|
340
|
+
}
|
|
341
|
+
return reply.send(result);
|
|
342
|
+
});
|
|
285
343
|
// GET /api/repos/:repo/entities/:type — list entities of a type
|
|
286
344
|
fastify.get('/api/repos/:repo/entities/:type', async (req, reply) => {
|
|
287
345
|
const { repo, type } = req.params;
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
try {
|
|
293
|
-
graph = workspaceManager.getGraph(repo);
|
|
294
|
-
}
|
|
295
|
-
catch (err) {
|
|
296
|
-
return reply.status(404).send({ error: err.message });
|
|
297
|
-
}
|
|
346
|
+
// Access enforced by graphForRead (WorkspaceAccessService).
|
|
347
|
+
const graph = graphForRead(req, reply, repo);
|
|
348
|
+
if (!graph)
|
|
349
|
+
return;
|
|
298
350
|
const entities = graph.search({ entityType: type });
|
|
299
351
|
const accessibleIds = getAccessibleEntityIds(req, workspaceManager, authService);
|
|
300
352
|
const filtered = entities.map(e => {
|
|
@@ -307,17 +359,20 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
307
359
|
// GET /api/repos/:repo/entities/:type/:id — get single entity
|
|
308
360
|
fastify.get('/api/repos/:repo/entities/:type/:id', async (req, reply) => {
|
|
309
361
|
const { repo, type, id } = req.params;
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
362
|
+
// Access enforced by graphForRead (WorkspaceAccessService).
|
|
363
|
+
const graph = graphForRead(req, reply, repo);
|
|
364
|
+
if (!graph)
|
|
365
|
+
return;
|
|
366
|
+
let entity;
|
|
314
367
|
try {
|
|
315
|
-
|
|
368
|
+
entity = graph.get(type, id);
|
|
316
369
|
}
|
|
317
370
|
catch (err) {
|
|
318
|
-
return reply.status(404).send({ error: err.message });
|
|
371
|
+
return reply.status(404).send({ error: err.message ?? `Entity not found: ${type}/${id}` });
|
|
319
372
|
}
|
|
320
|
-
|
|
373
|
+
// graph.get may also return null for a missing entity without throwing
|
|
374
|
+
// (depends on the backing graph implementation). Treat that as 404 so
|
|
375
|
+
// we don't blow up on the wikilinks access below.
|
|
321
376
|
if (!entity) {
|
|
322
377
|
return reply.status(404).send({ error: `Entity not found: ${type}/${id}` });
|
|
323
378
|
}
|
|
@@ -330,20 +385,144 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
330
385
|
}
|
|
331
386
|
return reply.send(entity);
|
|
332
387
|
});
|
|
333
|
-
// GET /api/repos/:repo/
|
|
334
|
-
fastify.get('/api/repos/:repo/
|
|
335
|
-
const { repo } = req.params;
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
388
|
+
// GET /api/repos/:repo/entities/:type/:id/history — entity commit history
|
|
389
|
+
fastify.get('/api/repos/:repo/entities/:type/:id/history', async (req, reply) => {
|
|
390
|
+
const { repo, type, id } = req.params;
|
|
391
|
+
// Access enforced by graphForRead (WorkspaceAccessService).
|
|
392
|
+
const graph = graphForRead(req, reply, repo);
|
|
393
|
+
if (!graph)
|
|
394
|
+
return;
|
|
395
|
+
const gitService = graph.getGitService();
|
|
396
|
+
if (!gitService) {
|
|
397
|
+
return reply.send([]);
|
|
398
|
+
}
|
|
399
|
+
const limit = Math.min(parseInt(req.query.limit ?? '20', 10) || 20, 50);
|
|
400
|
+
// Resolve the actual on-disk path via graph.get() — it walks nested
|
|
401
|
+
// folders to find the file. The previous hardcoded `${plural}/${id}.md`
|
|
402
|
+
// missed any entry not at the legacy flat path (entries created in a
|
|
403
|
+
// sub-folder, MCP entries that landed elsewhere, etc.) and silently
|
|
404
|
+
// returned an empty history. Convert absolute path → repo-relative
|
|
405
|
+
// for git log.
|
|
406
|
+
let filePath;
|
|
341
407
|
try {
|
|
342
|
-
|
|
408
|
+
const entity = graph.get(type, id);
|
|
409
|
+
const repoRoot = graph.getRepoPath();
|
|
410
|
+
filePath = entity.path.startsWith(repoRoot + '/')
|
|
411
|
+
? entity.path.slice(repoRoot.length + 1)
|
|
412
|
+
: entity.path;
|
|
343
413
|
}
|
|
344
414
|
catch (err) {
|
|
345
415
|
return reply.status(404).send({ error: err.message });
|
|
346
416
|
}
|
|
417
|
+
try {
|
|
418
|
+
const history = gitService.getFileHistory(filePath, limit);
|
|
419
|
+
// Opt-in per-commit change summary for the History panel. Off by
|
|
420
|
+
// default so the eager gating fetch on every entity-page load stays
|
|
421
|
+
// cheap; the panel requests it only when expanded.
|
|
422
|
+
if (req.query.changes === 'true') {
|
|
423
|
+
return reply.send(history.map((c) => ({ ...c, changes: graph.getCommitChange(filePath, c.hash) })));
|
|
424
|
+
}
|
|
425
|
+
return reply.send(history);
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
return reply.send([]);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
// POST /api/repos/:repo/entities/:type/:id/restore — restore an entry to
|
|
432
|
+
// its content at a past commit. Single-entity scope. Editor-or-admin role
|
|
433
|
+
// required (canWriteRepo). Returns 200 with the restore result, 404 if
|
|
434
|
+
// the entry doesn't exist at that commit, 409 PATH_CONFLICT if a different
|
|
435
|
+
// entry now occupies the target path, 403 if the caller lacks write access.
|
|
436
|
+
fastify.post('/api/repos/:repo/entities/:type/:id/restore', async (req, reply) => {
|
|
437
|
+
const { repo, type, id } = req.params;
|
|
438
|
+
const { commitHash } = req.body ?? {};
|
|
439
|
+
if (!commitHash || typeof commitHash !== 'string') {
|
|
440
|
+
return reply.status(400).send({ error: 'commitHash is required' });
|
|
441
|
+
}
|
|
442
|
+
// Access enforced by graphForWrite: 404 if no read access, 403 if no write.
|
|
443
|
+
const graph = graphForWrite(req, reply, repo);
|
|
444
|
+
if (!graph)
|
|
445
|
+
return;
|
|
446
|
+
// Flush any deferred autosave for this repo first — otherwise a restore
|
|
447
|
+
// could overwrite a pending (uncommitted) edit on disk and commit only
|
|
448
|
+
// the restored version, silently discarding the in-flight work. Matches
|
|
449
|
+
// rename/move/change-type, which all flush before mutating.
|
|
450
|
+
if (sessionManager)
|
|
451
|
+
await sessionManager.flush(repo);
|
|
452
|
+
try {
|
|
453
|
+
// restoreFile reconciles any live editor Y.Doc against the restored disk
|
|
454
|
+
// content internally (via the BodyWriteCoordinator's per-doc lock), so an
|
|
455
|
+
// open editor shows the restored version instead of syncing stale content
|
|
456
|
+
// back over it. That lives in graph.restoreFile so every restore caller
|
|
457
|
+
// (this route, the agent tool, the MCP tool) gets it.
|
|
458
|
+
const result = await graph.restoreFile(type, id, commitHash, reqGitUser(req));
|
|
459
|
+
wsHub?.broadcast({
|
|
460
|
+
type: 'entity_change',
|
|
461
|
+
action: result.action === 'recreate' ? 'created' : 'updated',
|
|
462
|
+
repo,
|
|
463
|
+
entityType: type,
|
|
464
|
+
entityId: id,
|
|
465
|
+
actor: reqUser(req)?.displayName ?? 'API',
|
|
466
|
+
source: 'api',
|
|
467
|
+
timestamp: new Date().toISOString(),
|
|
468
|
+
});
|
|
469
|
+
return reply.send({
|
|
470
|
+
success: true,
|
|
471
|
+
action: result.action,
|
|
472
|
+
path: result.path,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
catch (err) {
|
|
476
|
+
// PathConflictError carries an error code distinct from generic
|
|
477
|
+
// failures so the UI can render an actionable conflict message.
|
|
478
|
+
if (err?.code === 'PATH_CONFLICT' && err?.occupiedBy) {
|
|
479
|
+
return reply.status(409).send({
|
|
480
|
+
error: 'PATH_CONFLICT',
|
|
481
|
+
path: err.path,
|
|
482
|
+
occupiedBy: err.occupiedBy,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
const message = err instanceof Error ? err.message : 'Restore failed';
|
|
486
|
+
// "not found at commit" / "did not exist at commit" → 404 so the
|
|
487
|
+
// client can show "that version isn't available" instead of a
|
|
488
|
+
// generic 500.
|
|
489
|
+
if (/not found at commit|did not exist at commit/i.test(message)) {
|
|
490
|
+
return reply.status(404).send({ error: message });
|
|
491
|
+
}
|
|
492
|
+
// Type-mismatch refusal (restoring would silently change entity_type)
|
|
493
|
+
// is a client-actionable conflict, not a server fault → 409.
|
|
494
|
+
if (/would change its type|was type '/i.test(message)) {
|
|
495
|
+
return reply.status(409).send({ error: message });
|
|
496
|
+
}
|
|
497
|
+
return reply.status(500).send({ error: message });
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
// GET /api/schema/:entityType/fields — field metadata for an entity type
|
|
501
|
+
fastify.get('/api/schema/:entityType/fields', async (req, reply) => {
|
|
502
|
+
const { entityType } = req.params;
|
|
503
|
+
const registry = workspaceManager.getSchemaRegistry();
|
|
504
|
+
const fields = registry.getFieldMetadata(entityType);
|
|
505
|
+
return reply.send(fields);
|
|
506
|
+
});
|
|
507
|
+
// GET /api/formats — the format registry, serialized for the client.
|
|
508
|
+
//
|
|
509
|
+
// Single source of truth for ingestion: the web client builds its accept
|
|
510
|
+
// lists, disposition matrix, and supported-types copy from this instead of
|
|
511
|
+
// maintaining parallel (and drifting) constants. Mirrors the
|
|
512
|
+
// GET /api/schema/:type/fields pattern. Workspace-invariant + auth-gated
|
|
513
|
+
// by the global middleware; no per-repo access needed.
|
|
514
|
+
fastify.get('/api/formats', async (_req, reply) => {
|
|
515
|
+
const { serializeForClient } = await import('../../core/format-registry.js');
|
|
516
|
+
return reply.send(serializeForClient());
|
|
517
|
+
});
|
|
518
|
+
// GET /api/repos/:repo/search?q=&type=&tags= — search within a repo
|
|
519
|
+
fastify.get('/api/repos/:repo/search', async (req, reply) => {
|
|
520
|
+
const { repo } = req.params;
|
|
521
|
+
// Access enforced by graphForRead (WorkspaceAccessService).
|
|
522
|
+
const graph = graphForRead(req, reply, repo);
|
|
523
|
+
if (!graph)
|
|
524
|
+
return;
|
|
525
|
+
const { q, type, tags } = req.query;
|
|
347
526
|
const results = graph.search({
|
|
348
527
|
query: q,
|
|
349
528
|
entityType: type,
|
|
@@ -359,19 +538,18 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
359
538
|
});
|
|
360
539
|
// GET /api/graph — full workspace graph topology (nodes + edges)
|
|
361
540
|
fastify.get('/api/graph', async (req, reply) => {
|
|
362
|
-
const repos =
|
|
541
|
+
const repos = getContentRepos(req, workspaceManager, authService);
|
|
363
542
|
const nodes = [];
|
|
364
543
|
const edgeSet = new Set();
|
|
365
544
|
const edges = [];
|
|
366
545
|
const entityIds = new Set();
|
|
367
546
|
for (const repoConfig of repos) {
|
|
368
547
|
try {
|
|
548
|
+
// Direct WM access OK — `repos` came from getAccessibleRepos above.
|
|
369
549
|
const graph = workspaceManager.getGraph(repoConfig.name);
|
|
370
550
|
const entities = graph.search({});
|
|
371
551
|
for (const entity of entities) {
|
|
372
552
|
const nodeId = entity.id;
|
|
373
|
-
if (nodeId.toLowerCase().startsWith('readme'))
|
|
374
|
-
continue;
|
|
375
553
|
if (!entityIds.has(nodeId)) {
|
|
376
554
|
entityIds.add(nodeId);
|
|
377
555
|
nodes.push({
|
|
@@ -399,10 +577,10 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
399
577
|
const validEdges = edges.filter(e => entityIds.has(e.source) && entityIds.has(e.target));
|
|
400
578
|
return reply.send({ nodes, edges: validEdges });
|
|
401
579
|
});
|
|
402
|
-
// GET /api/search?q=&type=&tags=&repos= — cross-repo search
|
|
580
|
+
// GET /api/search?q=&type=&tags=&repos=&mode= — cross-repo search
|
|
403
581
|
fastify.get('/api/search', async (req, reply) => {
|
|
404
|
-
const { q, type, tags, repos } = req.query;
|
|
405
|
-
const accessibleNames = new Set(
|
|
582
|
+
const { q, type, tags, repos, mode } = req.query;
|
|
583
|
+
const accessibleNames = new Set(getContentRepos(req, workspaceManager, authService).map(r => r.name));
|
|
406
584
|
// Filter requested repos to only accessible ones
|
|
407
585
|
let repoNames = repos ? repos.split(',').map(r => r.trim()) : undefined;
|
|
408
586
|
if (repoNames) {
|
|
@@ -411,12 +589,35 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
411
589
|
else {
|
|
412
590
|
repoNames = [...accessibleNames];
|
|
413
591
|
}
|
|
592
|
+
const keywordMode = mode === 'keyword';
|
|
414
593
|
const results = await workspaceManager.search({
|
|
415
594
|
query: q,
|
|
416
595
|
entityType: type,
|
|
417
596
|
tags: tags ? tags.split(',').map(t => t.trim()) : undefined,
|
|
418
597
|
repoNames,
|
|
598
|
+
mode: keywordMode ? 'keyword' : undefined,
|
|
599
|
+
// Autosuggest dropdown caps at 20 visible rows; capping server-side
|
|
600
|
+
// avoids serializing a long tail the client throws away.
|
|
601
|
+
limit: keywordMode ? 20 : undefined,
|
|
419
602
|
});
|
|
603
|
+
// Autosuggest doesn't need the markdown body or wikilinks — the
|
|
604
|
+
// dropdown renders {name, type, repo} only. Returning a slim shape
|
|
605
|
+
// cuts response size from ~hundreds of KB to a few KB on big
|
|
606
|
+
// workspaces, and skips the per-entity wikilink visibility filter.
|
|
607
|
+
if (keywordMode) {
|
|
608
|
+
const slim = results.map(e => ({
|
|
609
|
+
id: e.id,
|
|
610
|
+
path: e.path,
|
|
611
|
+
folder_path: e.folder_path,
|
|
612
|
+
entityType: e.entityType,
|
|
613
|
+
repoName: e.repoName,
|
|
614
|
+
data: {
|
|
615
|
+
name: e.data?.name,
|
|
616
|
+
updated_at: e.data?.updated_at,
|
|
617
|
+
},
|
|
618
|
+
}));
|
|
619
|
+
return reply.send(slim);
|
|
620
|
+
}
|
|
420
621
|
const accessibleIds = getAccessibleEntityIds(req, workspaceManager, authService);
|
|
421
622
|
const filtered = results.map((e) => {
|
|
422
623
|
if (!e.document?.wikilinks?.length)
|
|
@@ -428,20 +629,29 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
428
629
|
// PUT /api/repos/:repo/entities/:type/:id — update entity (data and/or content)
|
|
429
630
|
fastify.put('/api/repos/:repo/entities/:type/:id', async (req, reply) => {
|
|
430
631
|
const { repo, type, id } = req.params;
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
632
|
+
// Access enforced by graphForWrite \u2014 PUT updates an entity, write required.
|
|
633
|
+
const graph = graphForWrite(req, reply, repo);
|
|
634
|
+
if (!graph)
|
|
635
|
+
return;
|
|
636
|
+
const eagerError = checkEagerSkillPermission(req, type, req.body.data, repo, workspaceManager, authService);
|
|
637
|
+
if (eagerError)
|
|
638
|
+
return reply.status(403).send({ error: eagerError });
|
|
435
639
|
try {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
//
|
|
640
|
+
// Optimistic concurrency. Two preconditions:
|
|
641
|
+
// - `X-Studiograph-Base-Body-Hash` (Phase 3a): sha256 hex of the
|
|
642
|
+
// body the client last loaded. Enforced INSIDE the per-doc lock
|
|
643
|
+
// by graph.update via `baseRevision`, so it composes with
|
|
644
|
+
// serialization — two body-writing PUTs from the same baseline
|
|
645
|
+
// can't both pass and back-stomp each other.
|
|
646
|
+
// - `If-Unmodified-Since` (legacy): kept for non-web clients;
|
|
647
|
+
// RFC-7231 second-precision so aliases sub-millisecond writes,
|
|
648
|
+
// which is why the body-hash form is preferred.
|
|
649
|
+
// Body-hash header is IGNORED on metadata-only PUTs — a stale body
|
|
650
|
+
// baseline is irrelevant when the PUT doesn't touch the body, and
|
|
651
|
+
// enforcing it would cause spurious 412s after an agent body-write.
|
|
652
|
+
const baseBodyHash = req.headers['x-studiograph-base-body-hash'];
|
|
443
653
|
const ifUnmodifiedSince = req.headers['if-unmodified-since'];
|
|
444
|
-
if (ifUnmodifiedSince) {
|
|
654
|
+
if (ifUnmodifiedSince && !baseBodyHash) {
|
|
445
655
|
const current = graph.get(type, id);
|
|
446
656
|
const entityUpdatedAt = current?.data?.updated_at;
|
|
447
657
|
if (entityUpdatedAt && new Date(entityUpdatedAt) > new Date(ifUnmodifiedSince)) {
|
|
@@ -449,6 +659,40 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
449
659
|
}
|
|
450
660
|
}
|
|
451
661
|
const gitUser = reqGitUser(req);
|
|
662
|
+
// Workspace-wide uniqueness check for `user_handle` on person entities.
|
|
663
|
+
// Skip the entity being updated so we don't reject it for clashing
|
|
664
|
+
// with itself. Also gate WHO can change the bridge: only workspace
|
|
665
|
+
// owners may stamp/clear/rewrite user_handle. Other folder-writers
|
|
666
|
+
// see the field but can't edit it (defense against impersonation
|
|
667
|
+
// through the agent's Current User prompt).
|
|
668
|
+
if (type === 'person' && req.body.data) {
|
|
669
|
+
try {
|
|
670
|
+
validateUserHandleInPayload(workspaceManager, req.body.data, { repo, entityId: id });
|
|
671
|
+
if (Object.prototype.hasOwnProperty.call(req.body.data, 'user_handle')) {
|
|
672
|
+
const current = graph.get('person', id);
|
|
673
|
+
const currentHandle = (current?.data?.user_handle ?? '');
|
|
674
|
+
assertCanChangeUserHandle(reqUser(req), currentHandle, req.body.data.user_handle);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
catch (err) {
|
|
678
|
+
if (err instanceof BridgeError) {
|
|
679
|
+
const status = err.code === 'OWNER_REQUIRED' ? 403 : 409;
|
|
680
|
+
return reply.status(status).send({ error: err.message, code: err.code, ...err.details });
|
|
681
|
+
}
|
|
682
|
+
throw err;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
// Scope ref fields (today: task.assignee → person). Rejects a wikilink
|
|
686
|
+
// that resolves to a non-person; forward references are allowed.
|
|
687
|
+
try {
|
|
688
|
+
assertEntityRefs(workspaceManager, type, req.body.data, { userExists: (id) => !!authService.findUserById(Number(id)) });
|
|
689
|
+
}
|
|
690
|
+
catch (err) {
|
|
691
|
+
if (err instanceof EntityRefError || err instanceof UnknownUserRefError) {
|
|
692
|
+
return reply.status(400).send({ error: err.message, code: err.code, field: err.field });
|
|
693
|
+
}
|
|
694
|
+
throw err;
|
|
695
|
+
}
|
|
452
696
|
const updated = await graph.update({
|
|
453
697
|
entityType: type,
|
|
454
698
|
entityId: id,
|
|
@@ -457,6 +701,12 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
457
701
|
commitMessage: `Update ${type}/${id} from web UI`,
|
|
458
702
|
user: gitUser,
|
|
459
703
|
skipCommit: !!sessionManager, // defer commit if scheduler is active
|
|
704
|
+
source: 'autosave',
|
|
705
|
+
// Forward the body-hash precondition into graph.update so the
|
|
706
|
+
// check runs INSIDE the per-doc lock — see Phase 3 P1 fix.
|
|
707
|
+
// Only meaningful for body writes; ignored when content is
|
|
708
|
+
// undefined (graph.update gates on content !== undefined too).
|
|
709
|
+
baseRevision: req.body.content !== undefined ? baseBodyHash : undefined,
|
|
460
710
|
});
|
|
461
711
|
if (sessionManager && gitUser) {
|
|
462
712
|
sessionManager.markDirty(repo, gitUser, type, id, updated.path);
|
|
@@ -469,6 +719,19 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
469
719
|
return reply.send(updated);
|
|
470
720
|
}
|
|
471
721
|
catch (err) {
|
|
722
|
+
// Translate the locked-section baseRevision mismatch into the
|
|
723
|
+
// same 412 shape the route used to emit pre-lock. Preserves the
|
|
724
|
+
// client-side contract (code: STALE_BODY_HASH + currentBodyHash
|
|
725
|
+
// + entity).
|
|
726
|
+
if (err instanceof StaleBaseRevisionError) {
|
|
727
|
+
const current = graph.get(type, id);
|
|
728
|
+
return reply.status(412).send({
|
|
729
|
+
error: 'Precondition Failed: body was modified',
|
|
730
|
+
code: 'STALE_BODY_HASH',
|
|
731
|
+
currentBodyHash: err.actual,
|
|
732
|
+
entity: current,
|
|
733
|
+
});
|
|
734
|
+
}
|
|
472
735
|
return reply.status(400).send({ error: err.message });
|
|
473
736
|
}
|
|
474
737
|
});
|
|
@@ -479,16 +742,10 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
479
742
|
if (!type || !id || !newType) {
|
|
480
743
|
return reply.status(400).send({ error: 'entityType, entityId, and newType are required' });
|
|
481
744
|
}
|
|
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
|
-
}
|
|
745
|
+
// Access enforced by graphForRead (WorkspaceAccessService).
|
|
746
|
+
const graph = graphForRead(req, reply, repo);
|
|
747
|
+
if (!graph)
|
|
748
|
+
return;
|
|
492
749
|
try {
|
|
493
750
|
// Flush any pending session saves before moving the file
|
|
494
751
|
if (sessionManager) {
|
|
@@ -513,45 +770,121 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
513
770
|
return reply.status(status).send({ error: err.message });
|
|
514
771
|
}
|
|
515
772
|
});
|
|
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,
|
|
773
|
+
// POST /api/repos/:repo/rename — rename an entity (changes its id/filename)
|
|
774
|
+
fastify.post('/api/repos/:repo/rename', async (req, reply) => {
|
|
775
|
+
const { repo } = req.params;
|
|
776
|
+
const { entityType: type, entityId: id, newId } = req.body ?? {};
|
|
777
|
+
if (!type || !id || !newId) {
|
|
778
|
+
return reply.status(400).send({ error: 'entityType, entityId, and newId are required' });
|
|
522
779
|
}
|
|
523
|
-
if (
|
|
524
|
-
return reply.status(400).send({ error: '
|
|
780
|
+
if (!/^[a-z0-9][a-z0-9_-]*$/.test(newId)) {
|
|
781
|
+
return reply.status(400).send({ error: 'New ID must be kebab-case (lowercase letters, numbers, hyphens, underscores)' });
|
|
525
782
|
}
|
|
526
|
-
if (
|
|
527
|
-
return reply.status(
|
|
783
|
+
if (id === newId) {
|
|
784
|
+
return reply.status(400).send({ error: 'New ID must differ from the current ID' });
|
|
528
785
|
}
|
|
529
|
-
|
|
530
|
-
|
|
786
|
+
// Access enforced by graphForWrite (WorkspaceAccessService).
|
|
787
|
+
const graph = graphForWrite(req, reply, repo);
|
|
788
|
+
if (!graph)
|
|
789
|
+
return;
|
|
790
|
+
// Workspace-wide uniqueness — ids must be unique across all folders
|
|
791
|
+
const conflict = workspaceManager.findEntityById(newId);
|
|
792
|
+
if (conflict) {
|
|
793
|
+
return reply.status(409).send({ error: `An entry with id "${newId}" already exists in folder "${conflict.repoName}".` });
|
|
531
794
|
}
|
|
532
|
-
let sourceGraph, targetGraph;
|
|
533
795
|
try {
|
|
534
|
-
|
|
535
|
-
|
|
796
|
+
// Flush pending session saves so we don't PUT to the old path after rename
|
|
797
|
+
if (sessionManager) {
|
|
798
|
+
await sessionManager.flush(repo);
|
|
799
|
+
}
|
|
800
|
+
const gitUser = reqGitUser(req);
|
|
801
|
+
const renamed = await graph.rename(type, id, newId, undefined, gitUser);
|
|
802
|
+
// Rewrite any [[wikilinks]] that still point at the old id, across
|
|
803
|
+
// every folder the caller can access. Per-repo — cross-folder links
|
|
804
|
+
// are rare but supported.
|
|
805
|
+
const refactorMsg = `Refactor wikilinks: ${id} \u2192 ${newId}`;
|
|
806
|
+
for (const other of getAccessibleRepos(req, workspaceManager, authService)) {
|
|
807
|
+
try {
|
|
808
|
+
// Direct WM access OK — iterating the access-filtered list.
|
|
809
|
+
const otherGraph = workspaceManager.getGraph(other.name);
|
|
810
|
+
await otherGraph.refactorWikilink(id, newId, refactorMsg);
|
|
811
|
+
}
|
|
812
|
+
catch { /* skip unloadable repo */ }
|
|
813
|
+
}
|
|
814
|
+
wsHub?.broadcast({
|
|
815
|
+
type: 'entity_change', action: 'deleted', repo, entityType: type, entityId: id,
|
|
816
|
+
actor: req.user?.displayName ?? 'API',
|
|
817
|
+
source: 'api', timestamp: new Date().toISOString(),
|
|
818
|
+
});
|
|
819
|
+
wsHub?.broadcast({
|
|
820
|
+
type: 'entity_change', action: 'created', repo, entityType: type, entityId: newId,
|
|
821
|
+
actor: req.user?.displayName ?? 'API',
|
|
822
|
+
source: 'api', timestamp: new Date().toISOString(),
|
|
823
|
+
});
|
|
824
|
+
return reply.send({ success: true, entity: renamed, newUrl: `/${repo}/${type}/${newId}` });
|
|
536
825
|
}
|
|
537
826
|
catch (err) {
|
|
538
|
-
|
|
827
|
+
const status = err.message?.includes('already exists') ? 409 : 400;
|
|
828
|
+
return reply.status(status).send({ error: err.message });
|
|
539
829
|
}
|
|
830
|
+
});
|
|
831
|
+
// POST /api/repos/:repo/transfer — move or copy entity to another folder
|
|
832
|
+
fastify.post('/api/repos/:repo/transfer', async (req, reply) => {
|
|
833
|
+
const sourceRepo = req.params.repo;
|
|
834
|
+
const { entityType, entityId, targetRepo, mode, targetFolderPath } = req.body ?? {};
|
|
835
|
+
if (!entityType || !entityId || !targetRepo || !mode) {
|
|
836
|
+
return reply.status(400).send({ error: 'entityType, entityId, targetRepo, and mode are required' });
|
|
837
|
+
}
|
|
838
|
+
if (sourceRepo === targetRepo) {
|
|
839
|
+
return reply.status(400).send({ error: 'Source and target folders must be different' });
|
|
840
|
+
}
|
|
841
|
+
// Workspace Assets store ownership gate (codex P1 / D2). Moving an `asset`
|
|
842
|
+
// OUT of a workspace_wide store reassigns its index repo, after which the
|
|
843
|
+
// uploader/owner delete rule no longer applies — so the move itself must
|
|
844
|
+
// honor that rule. assetDeleteDenial is a no-op for non-workspace_wide
|
|
845
|
+
// sources, so normal folder→folder moves are unaffected.
|
|
846
|
+
if (entityType === 'asset') {
|
|
847
|
+
const denial = assetDeleteDenial(workspaceManager.getAssetIndex?.(), workspaceManager.getRepoConfig(sourceRepo), reqUser(req), sourceRepo, entityType, entityId);
|
|
848
|
+
if (denial)
|
|
849
|
+
return reply.status(403).send({ error: denial });
|
|
850
|
+
}
|
|
851
|
+
// Access enforced by graphForWrite — both source (delete on move)
|
|
852
|
+
// and target (create) require write access. Resolved up-front so a
|
|
853
|
+
// target-side denial never surfaces only after source has been
|
|
854
|
+
// mutated. Audit (2026-05) flagged the previous opt-in pattern.
|
|
855
|
+
const sourceGraph = graphForWrite(req, reply, sourceRepo);
|
|
856
|
+
if (!sourceGraph)
|
|
857
|
+
return;
|
|
858
|
+
const targetGraph = graphForWrite(req, reply, targetRepo);
|
|
859
|
+
if (!targetGraph)
|
|
860
|
+
return;
|
|
540
861
|
try {
|
|
541
|
-
//
|
|
542
|
-
|
|
862
|
+
// Phase 0 — prevalidate. Audit (2026-05) required this: previously
|
|
863
|
+
// the route created in target, broadcast `created`, and only then
|
|
864
|
+
// tried to delete from source. A delete failure left a duplicate
|
|
865
|
+
// entry the client had already been told about.
|
|
866
|
+
if (sessionManager) {
|
|
543
867
|
await sessionManager.flush(sourceRepo);
|
|
544
|
-
|
|
868
|
+
if (targetRepo !== sourceRepo)
|
|
869
|
+
await sessionManager.flush(targetRepo);
|
|
870
|
+
}
|
|
871
|
+
// Source must exist; target must not collide. Both checks run BEFORE
|
|
872
|
+
// any mutation so we never enter phase 2 unless phase 1 will succeed.
|
|
545
873
|
const source = sourceGraph.get(entityType, entityId);
|
|
546
|
-
|
|
874
|
+
const sourcePath = source.path;
|
|
875
|
+
const sourceRawMarkdown = readFileSync(sourcePath, 'utf-8');
|
|
547
876
|
try {
|
|
548
877
|
targetGraph.get(entityType, entityId);
|
|
549
878
|
return reply.status(409).send({ error: `"${entityId}" already exists in ${targetRepo}` });
|
|
550
879
|
}
|
|
551
880
|
catch { /* not found = good */ }
|
|
552
|
-
// Create in target
|
|
553
881
|
const gitUser = reqGitUser(req);
|
|
554
882
|
const { entity_type, entity_id, ...dataFields } = source.data;
|
|
883
|
+
// Phase 1 — create in target. No broadcast yet: a half-completed
|
|
884
|
+
// move (created in target, source still present) must NOT be
|
|
885
|
+
// observable to clients, or they will display two copies until the
|
|
886
|
+
// next refresh. `targetFolderPath` lets the caller drop the entry
|
|
887
|
+
// straight into a subfolder of the target repo.
|
|
555
888
|
const created = await targetGraph.create({
|
|
556
889
|
entityType: entityType,
|
|
557
890
|
entityId,
|
|
@@ -559,21 +892,123 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
559
892
|
content: source.document?.content ?? '',
|
|
560
893
|
commitMessage: `${mode === 'move' ? 'Move' : 'Copy'} ${entityType}: ${entityId} from ${sourceRepo}`,
|
|
561
894
|
user: gitUser,
|
|
895
|
+
folderPath: targetFolderPath,
|
|
562
896
|
});
|
|
897
|
+
// Phase 2 — for `move` only: delete from source. If this fails, run
|
|
898
|
+
// a compensating delete on target so we don't leave a duplicate.
|
|
899
|
+
// The compensating delete is best-effort; if it also fails, surface
|
|
900
|
+
// a 500 with both error messages so the residue is visible (the
|
|
901
|
+
// previous behavior silently succeeded and left orphans).
|
|
902
|
+
if (mode === 'move') {
|
|
903
|
+
try {
|
|
904
|
+
// keepAssets: the moved entity's immutable-keyed blobs are shared
|
|
905
|
+
// (same assetId in both repos' bodies during the window); the
|
|
906
|
+
// source delete must not destroy them.
|
|
907
|
+
await sourceGraph.delete(entityType, entityId, undefined, gitUser, { keepAssets: true });
|
|
908
|
+
// Re-point ownership to the target repo ONLY after the source
|
|
909
|
+
// delete succeeds. Doing it after means a failed source delete
|
|
910
|
+
// (handled below) leaves the index pointing at the source repo,
|
|
911
|
+
// which is correct for the entity we're about to restore there.
|
|
912
|
+
// Blobs/URLs are unchanged (immutable keys); only the index's
|
|
913
|
+
// repo column moves, which is what serve-time authz reads.
|
|
914
|
+
workspaceManager.getAssetIndex?.()?.reassignOwner(sourceRepo, entityType, entityId, { repo: targetRepo });
|
|
915
|
+
}
|
|
916
|
+
catch (deleteErr) {
|
|
917
|
+
const deleteMsg = deleteErr instanceof Error ? deleteErr.message : String(deleteErr);
|
|
918
|
+
let recoveryMsg = null;
|
|
919
|
+
try {
|
|
920
|
+
let restoreSource = !existsSync(sourcePath);
|
|
921
|
+
if (!restoreSource) {
|
|
922
|
+
try {
|
|
923
|
+
restoreSource = readFileSync(sourcePath, 'utf-8') !== sourceRawMarkdown;
|
|
924
|
+
}
|
|
925
|
+
catch {
|
|
926
|
+
restoreSource = true;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
if (restoreSource) {
|
|
930
|
+
mkdirSync(dirname(sourcePath), { recursive: true });
|
|
931
|
+
writeFileSync(sourcePath, sourceRawMarkdown, 'utf-8');
|
|
932
|
+
const recoveryPaths = [sourcePath];
|
|
933
|
+
const folderMarkerPath = join(dirname(sourcePath), '.gitkeep');
|
|
934
|
+
if (existsSync(folderMarkerPath)) {
|
|
935
|
+
rmSync(folderMarkerPath, { force: true });
|
|
936
|
+
recoveryPaths.push(folderMarkerPath);
|
|
937
|
+
}
|
|
938
|
+
const sourceAny = sourceGraph;
|
|
939
|
+
const recoveryGit = sourceAny.gitService;
|
|
940
|
+
const recoveryUser = gitUser ?? sourceAny.gitUser;
|
|
941
|
+
if (!recoveryGit || !recoveryUser) {
|
|
942
|
+
throw new Error('source graph git service or git user unavailable');
|
|
943
|
+
}
|
|
944
|
+
recoveryGit.commitFiles(recoveryPaths, `Recover ${entityType}: ${entityId} after failed move to ${targetRepo}`, recoveryUser);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
catch (recoveryErr) {
|
|
948
|
+
recoveryMsg = recoveryErr instanceof Error ? recoveryErr.message : String(recoveryErr);
|
|
949
|
+
log.error('[critical] cross-repo move source recovery failed', {
|
|
950
|
+
sourceRepo,
|
|
951
|
+
targetRepo,
|
|
952
|
+
entityType,
|
|
953
|
+
entityId,
|
|
954
|
+
sourcePath,
|
|
955
|
+
deleteError: deleteMsg,
|
|
956
|
+
recoveryError: recoveryMsg,
|
|
957
|
+
capturedContent: sourceRawMarkdown,
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
try {
|
|
961
|
+
// keepAssets: ownership was NOT reassigned (that happens only
|
|
962
|
+
// after a successful source delete), so the blobs still belong
|
|
963
|
+
// to the source entity we just restored. Destroying them here
|
|
964
|
+
// would leave the recovered source pointing at dead URLs.
|
|
965
|
+
await targetGraph.delete(entityType, entityId, undefined, gitUser, { keepAssets: true });
|
|
966
|
+
if (recoveryMsg) {
|
|
967
|
+
return reply.status(500).send({
|
|
968
|
+
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.`,
|
|
969
|
+
code: 'MOVE_SOURCE_RECOVERY_FAILED',
|
|
970
|
+
sourceRepo,
|
|
971
|
+
targetRepo,
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
return reply.status(500).send({
|
|
975
|
+
error: `Move failed: source delete of ${entityType}/${entityId} failed (${deleteMsg}). Target rolled back; folders are back to their pre-move state.`,
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
catch (compErr) {
|
|
979
|
+
const compMsg = compErr instanceof Error ? compErr.message : String(compErr);
|
|
980
|
+
if (recoveryMsg) {
|
|
981
|
+
return reply.status(500).send({
|
|
982
|
+
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.`,
|
|
983
|
+
code: 'MOVE_RECOVERY_AND_ROLLBACK_FAILED',
|
|
984
|
+
sourceRepo,
|
|
985
|
+
targetRepo,
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
return reply.status(500).send({
|
|
989
|
+
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.`,
|
|
990
|
+
code: 'MOVE_DUPLICATE',
|
|
991
|
+
sourceRepo,
|
|
992
|
+
targetRepo,
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
// Phase 3 — both phases succeeded; only now broadcast. Order:
|
|
998
|
+
// source-delete first so subscribers don't briefly observe the
|
|
999
|
+
// entity in both folders during the network race.
|
|
563
1000
|
const actor = req.user?.displayName ?? 'API';
|
|
564
1001
|
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
1002
|
if (mode === 'move') {
|
|
571
|
-
await sourceGraph.delete(entityType, entityId, undefined, gitUser);
|
|
572
1003
|
wsHub?.broadcast({
|
|
573
1004
|
type: 'entity_change', action: 'deleted', repo: sourceRepo, entityType, entityId,
|
|
574
1005
|
actor, source: 'api', timestamp: ts,
|
|
575
1006
|
});
|
|
576
1007
|
}
|
|
1008
|
+
wsHub?.broadcast({
|
|
1009
|
+
type: 'entity_change', action: 'created', repo: targetRepo, entityType, entityId,
|
|
1010
|
+
actor, source: 'api', timestamp: ts,
|
|
1011
|
+
});
|
|
577
1012
|
return reply.send({
|
|
578
1013
|
success: true,
|
|
579
1014
|
entity: created,
|
|
@@ -584,27 +1019,208 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
584
1019
|
return reply.status(400).send({ error: err.message });
|
|
585
1020
|
}
|
|
586
1021
|
});
|
|
587
|
-
//
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
1022
|
+
// POST /api/repos/:repo/transfer-entities — bulk move a list of entries
|
|
1023
|
+
// into a single target folder (in this or another repo). The web UI's
|
|
1024
|
+
// multi-select Move flow used to loop and POST per entry; this collapses
|
|
1025
|
+
// to one round-trip per source repo and one commit per repo touched.
|
|
1026
|
+
fastify.post('/api/repos/:repo/transfer-entities', async (req, reply) => {
|
|
1027
|
+
const sourceRepo = req.params.repo;
|
|
1028
|
+
const { entries, targetRepo, targetFolder } = req.body ?? {};
|
|
1029
|
+
if (!Array.isArray(entries) || !targetRepo || targetFolder === undefined) {
|
|
1030
|
+
return reply.status(400).send({ error: 'entries, targetRepo, and targetFolder are required' });
|
|
1031
|
+
}
|
|
1032
|
+
// Workspace Assets store ownership gate (codex P1 / D2): same rule as the
|
|
1033
|
+
// single /transfer route, applied per asset entry. No-op when the source
|
|
1034
|
+
// isn't workspace_wide.
|
|
1035
|
+
for (const e of entries) {
|
|
1036
|
+
if (e.entityType !== 'asset')
|
|
1037
|
+
continue;
|
|
1038
|
+
const denial = assetDeleteDenial(workspaceManager.getAssetIndex?.(), workspaceManager.getRepoConfig(sourceRepo), reqUser(req), sourceRepo, e.entityType, e.entityId);
|
|
1039
|
+
if (denial)
|
|
1040
|
+
return reply.status(403).send({ error: denial });
|
|
1041
|
+
}
|
|
1042
|
+
// Access enforced by graphForWrite — both source (mutate) and
|
|
1043
|
+
// target (write) require write access. Resolved up-front so a
|
|
1044
|
+
// target-side denial never surfaces only after source mutation.
|
|
1045
|
+
const sourceGraph = graphForWrite(req, reply, sourceRepo);
|
|
1046
|
+
if (!sourceGraph)
|
|
1047
|
+
return;
|
|
1048
|
+
const targetGraph = graphForWrite(req, reply, targetRepo);
|
|
1049
|
+
if (!targetGraph)
|
|
1050
|
+
return;
|
|
1051
|
+
try {
|
|
1052
|
+
if (sessionManager) {
|
|
1053
|
+
await sessionManager.flush(sourceRepo);
|
|
1054
|
+
if (targetRepo !== sourceRepo)
|
|
1055
|
+
await sessionManager.flush(targetRepo);
|
|
1056
|
+
}
|
|
1057
|
+
const result = await sourceGraph.moveEntitiesToRepo(entries.map(e => ({ entityType: e.entityType, entityId: e.entityId })), targetGraph, targetFolder, { user: reqGitUser(req) });
|
|
1058
|
+
const actor = req.user?.displayName ?? 'API';
|
|
1059
|
+
const ts = new Date().toISOString();
|
|
1060
|
+
wsHub?.broadcast({ type: 'repo_sync', repo: sourceRepo, source: 'api', actor, timestamp: ts });
|
|
1061
|
+
if (targetRepo !== sourceRepo) {
|
|
1062
|
+
wsHub?.broadcast({ type: 'repo_sync', repo: targetRepo, source: 'api', actor, timestamp: ts });
|
|
1063
|
+
}
|
|
1064
|
+
return reply.send({ success: true, ...result });
|
|
1065
|
+
}
|
|
1066
|
+
catch (err) {
|
|
1067
|
+
const status = err?.name === 'InvalidFolderPathError' ? 400
|
|
1068
|
+
: err.message?.includes('not found') ? 404
|
|
1069
|
+
: err.message?.includes('already occupied') ? 409
|
|
1070
|
+
: 400;
|
|
1071
|
+
return reply.status(status).send({ error: err.message });
|
|
592
1072
|
}
|
|
593
|
-
|
|
1073
|
+
});
|
|
1074
|
+
// POST /api/repos/:repo/transfer-folder — move an entire folder (with all
|
|
1075
|
+
// descendants) into another repo in one shot. Replaces the per-entry loop
|
|
1076
|
+
// the web UI used to run, which produced 2 git commits per entry; this
|
|
1077
|
+
// route performs a single FS-level rename and one commit per repo.
|
|
1078
|
+
fastify.post('/api/repos/:repo/transfer-folder', async (req, reply) => {
|
|
1079
|
+
const sourceRepo = req.params.repo;
|
|
1080
|
+
const { sourceFolder, targetRepo, targetFolder } = req.body ?? {};
|
|
1081
|
+
if (!sourceFolder || !targetRepo || targetFolder === undefined) {
|
|
1082
|
+
return reply.status(400).send({ error: 'sourceFolder, targetRepo, and targetFolder are required' });
|
|
1083
|
+
}
|
|
1084
|
+
if (sourceRepo === targetRepo && sourceFolder === targetFolder) {
|
|
1085
|
+
return reply.status(400).send({ error: 'Source and target are the same' });
|
|
1086
|
+
}
|
|
1087
|
+
// Workspace Assets store gate (codex P1 / D2). Moving a folder OUT of a
|
|
1088
|
+
// workspace_wide store relocates+reassigns every asset under it, escaping
|
|
1089
|
+
// the per-asset uploader/owner delete rule. A folder-level bulk move in
|
|
1090
|
+
// the shared store is an admin action — restrict it to a workspace owner.
|
|
1091
|
+
// (No-op for normal folders; the manager never folder-moves the store.)
|
|
1092
|
+
if (workspaceManager.getRepoConfig(sourceRepo)?.workspace_wide && !isWorkspaceOwner(reqUser(req)?.role ?? '')) {
|
|
1093
|
+
return reply.status(403).send({ error: 'Only a workspace owner can move folders out of the shared Assets store.' });
|
|
1094
|
+
}
|
|
1095
|
+
// Access enforced by graphForWrite — both source (mutate) and
|
|
1096
|
+
// target (write) require write access. Resolved up-front so a
|
|
1097
|
+
// target-side denial never surfaces only after source mutation.
|
|
1098
|
+
const sourceGraph = graphForWrite(req, reply, sourceRepo);
|
|
1099
|
+
if (!sourceGraph)
|
|
1100
|
+
return;
|
|
1101
|
+
const targetGraph = graphForWrite(req, reply, targetRepo);
|
|
1102
|
+
if (!targetGraph)
|
|
1103
|
+
return;
|
|
594
1104
|
try {
|
|
595
|
-
|
|
1105
|
+
// Flush any deferred saves on either side so we don't strand a
|
|
1106
|
+
// pending edit in the source folder mid-move.
|
|
1107
|
+
if (sessionManager) {
|
|
1108
|
+
await sessionManager.flush(sourceRepo);
|
|
1109
|
+
if (targetRepo !== sourceRepo)
|
|
1110
|
+
await sessionManager.flush(targetRepo);
|
|
1111
|
+
}
|
|
1112
|
+
const stats = await sourceGraph.moveFolderToRepo(sourceFolder, targetGraph, targetFolder, { user: reqGitUser(req) });
|
|
1113
|
+
const actor = req.user?.displayName ?? 'API';
|
|
1114
|
+
const ts = new Date().toISOString();
|
|
1115
|
+
// One refresh-trigger per repo. The receiving and sending lists
|
|
1116
|
+
// both need to update; using `repo_sync` keeps the realtime
|
|
1117
|
+
// handler simple — it already calls workspace.refreshEntities
|
|
1118
|
+
// on this event type. Avoids emitting 2× entries-moved entity_change
|
|
1119
|
+
// broadcasts (one created, one deleted) per file.
|
|
1120
|
+
wsHub?.broadcast({ type: 'repo_sync', repo: sourceRepo, source: 'api', actor, timestamp: ts });
|
|
1121
|
+
if (targetRepo !== sourceRepo) {
|
|
1122
|
+
wsHub?.broadcast({ type: 'repo_sync', repo: targetRepo, source: 'api', actor, timestamp: ts });
|
|
1123
|
+
}
|
|
1124
|
+
return reply.send({ success: true, ...stats });
|
|
596
1125
|
}
|
|
597
1126
|
catch (err) {
|
|
598
|
-
|
|
1127
|
+
const status = err?.name === 'InvalidFolderPathError' ? 400
|
|
1128
|
+
: err.message?.includes('not found') ? 404
|
|
1129
|
+
: err.message?.includes('not empty') ? 409
|
|
1130
|
+
: 400;
|
|
1131
|
+
return reply.status(status).send({ error: err.message });
|
|
599
1132
|
}
|
|
1133
|
+
});
|
|
1134
|
+
// Workspace Assets store delete gate (codex P1 / D2) — uploader/owner only.
|
|
1135
|
+
// Pure decision lives in services/asset-delete-authz.ts (unit-tested); this
|
|
1136
|
+
// closure binds it to the route's workspaceManager.
|
|
1137
|
+
const denyAssetDelete = (user, repo, entityType, entityId) => assetDeleteDenial(workspaceManager.getAssetIndex?.(), workspaceManager.getRepoConfig(repo), user, repo, entityType, entityId);
|
|
1138
|
+
// DELETE /api/repos/:repo/entities/:type/:id — delete an entity
|
|
1139
|
+
fastify.delete('/api/repos/:repo/entities/:type/:id', async (req, reply) => {
|
|
1140
|
+
const { repo, type, id } = req.params;
|
|
1141
|
+
const denial = denyAssetDelete(reqUser(req), repo, type, id);
|
|
1142
|
+
if (denial)
|
|
1143
|
+
return reply.status(403).send({ error: denial });
|
|
1144
|
+
// Access enforced by graphForRead (WorkspaceAccessService).
|
|
1145
|
+
const graph = graphForRead(req, reply, repo);
|
|
1146
|
+
if (!graph)
|
|
1147
|
+
return;
|
|
600
1148
|
try {
|
|
1149
|
+
// Soft bridge-awareness: deleting a person that's linked to an auth
|
|
1150
|
+
// account orphans that user (their "My Tasks"/@mentions stop
|
|
1151
|
+
// resolving). We don't BLOCK the delete — the link is a soft
|
|
1152
|
+
// relationship — but we surface a warning so the caller knows, and the
|
|
1153
|
+
// orphan is then caught by the Profile panel's unlinked affordance.
|
|
1154
|
+
let bridgeWarning;
|
|
1155
|
+
if (type === 'person') {
|
|
1156
|
+
try {
|
|
1157
|
+
const handle = graph.get('person', id)?.data?.user_handle;
|
|
1158
|
+
if (typeof handle === 'string' && handle.length > 0) {
|
|
1159
|
+
bridgeWarning = `This record was linked to the "${handle}" account. Deleting it unlinks that user — their "My Tasks", @mentions, and task assignments stop resolving until they link a new record (Settings → Profile → "Create & link").`;
|
|
1160
|
+
log.warn(`[bridge] deleting linked person ${repo}/${id} (was user ${handle})`);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
catch { /* not readable / already gone — let graph.delete surface it */ }
|
|
1164
|
+
}
|
|
601
1165
|
await graph.delete(type, id, `Delete ${type}/${id} from web UI`, reqGitUser(req));
|
|
1166
|
+
// Deleting an asset entity removes its blob(s); drop the cached bucket
|
|
1167
|
+
// walk so a legacy twin doesn't linger in the manager (codex P2).
|
|
1168
|
+
if (type === 'asset')
|
|
1169
|
+
clearLegacyBucketCache();
|
|
602
1170
|
wsHub?.broadcast({
|
|
603
1171
|
type: 'entity_change', action: 'deleted', repo, entityType: type, entityId: id,
|
|
604
1172
|
actor: req.user?.displayName ?? 'API',
|
|
605
1173
|
source: 'api', timestamp: new Date().toISOString(),
|
|
606
1174
|
});
|
|
607
|
-
return reply.send({ ok: true });
|
|
1175
|
+
return reply.send({ ok: true, ...(bridgeWarning ? { warning: bridgeWarning } : {}) });
|
|
1176
|
+
}
|
|
1177
|
+
catch (err) {
|
|
1178
|
+
return reply.status(400).send({ error: err.message });
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
// POST /api/repos/:repo/entities/bulk-delete — delete many entities in
|
|
1182
|
+
// one git transaction + one WS broadcast. Replaces N parallel
|
|
1183
|
+
// DELETE /entities/:type/:id calls, which on a 500-entry bulk delete
|
|
1184
|
+
// produced 500 git commits + 500 entity_change broadcasts and froze
|
|
1185
|
+
// the UI for minutes.
|
|
1186
|
+
fastify.post('/api/repos/:repo/entities/bulk-delete', async (req, reply) => {
|
|
1187
|
+
const { repo } = req.params;
|
|
1188
|
+
// Access enforced by graphForWrite (WorkspaceAccessService).
|
|
1189
|
+
const graph = graphForWrite(req, reply, repo);
|
|
1190
|
+
if (!graph)
|
|
1191
|
+
return;
|
|
1192
|
+
const body = req.body;
|
|
1193
|
+
if (!body?.entities || !Array.isArray(body.entities)) {
|
|
1194
|
+
return reply.status(400).send({ error: 'Expected { entities: [{ entityType, entityId }] }' });
|
|
1195
|
+
}
|
|
1196
|
+
// Apply the workspace-Assets delete gate per entity (codex P1 / D2):
|
|
1197
|
+
// uploader/owner-only. Denied entries are reported, not silently dropped.
|
|
1198
|
+
const deleteUser = reqUser(req);
|
|
1199
|
+
const denied = [];
|
|
1200
|
+
const allowed = body.entities.filter(e => {
|
|
1201
|
+
const d = denyAssetDelete(deleteUser, repo, e.entityType, e.entityId);
|
|
1202
|
+
if (d) {
|
|
1203
|
+
denied.push({ entityType: e.entityType, entityId: e.entityId, error: d });
|
|
1204
|
+
return false;
|
|
1205
|
+
}
|
|
1206
|
+
return true;
|
|
1207
|
+
});
|
|
1208
|
+
try {
|
|
1209
|
+
const result = await graph.bulkDelete(allowed.map(e => ({ entityType: e.entityType, entityId: e.entityId })), {
|
|
1210
|
+
user: reqGitUser(req),
|
|
1211
|
+
commitMessage: `Bulk delete ${allowed.length} ${allowed.length === 1 ? 'entry' : 'entries'} from web UI`,
|
|
1212
|
+
});
|
|
1213
|
+
if (result.deleted.length > 0) {
|
|
1214
|
+
wsHub?.broadcast({
|
|
1215
|
+
type: 'repo_sync', repo,
|
|
1216
|
+
actor: req.user?.displayName ?? 'API',
|
|
1217
|
+
source: 'api', timestamp: new Date().toISOString(),
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
return reply.send({
|
|
1221
|
+
deleted: result.deleted.length,
|
|
1222
|
+
errors: [...result.errors, ...denied],
|
|
1223
|
+
});
|
|
608
1224
|
}
|
|
609
1225
|
catch (err) {
|
|
610
1226
|
return reply.status(400).send({ error: err.message });
|
|
@@ -613,51 +1229,691 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
|
|
|
613
1229
|
// POST /api/repos/:repo/entities — create a new entity
|
|
614
1230
|
fastify.post('/api/repos/:repo/entities', async (req, reply) => {
|
|
615
1231
|
const { repo } = req.params;
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
1232
|
+
// Access enforced by graphForWrite (WorkspaceAccessService).
|
|
1233
|
+
const graph = graphForWrite(req, reply, repo);
|
|
1234
|
+
if (!graph)
|
|
1235
|
+
return;
|
|
1236
|
+
const { entityType, entityId, data = {}, content = '', folder_path } = req.body;
|
|
620
1237
|
if (!entityType || !entityId) {
|
|
621
1238
|
return reply.status(400).send({ error: 'entityType and entityId are required' });
|
|
622
1239
|
}
|
|
623
|
-
|
|
1240
|
+
const eagerError = checkEagerSkillPermission(req, entityType, data, repo, workspaceManager, authService);
|
|
1241
|
+
if (eagerError)
|
|
1242
|
+
return reply.status(403).send({ error: eagerError });
|
|
1243
|
+
// Person entities can carry a `user_handle` pointer to an auth account;
|
|
1244
|
+
// enforce workspace-wide uniqueness AND owner-only authz before
|
|
1245
|
+
// delegating to the per-repo create path. Stamping a handle on a new
|
|
1246
|
+
// entity is the same trust action as stamping it on an existing one,
|
|
1247
|
+
// so non-owners can't create a pre-linked person entity either.
|
|
1248
|
+
if (entityType === 'person') {
|
|
1249
|
+
try {
|
|
1250
|
+
validateUserHandleInPayload(workspaceManager, data);
|
|
1251
|
+
if (Object.prototype.hasOwnProperty.call(data, 'user_handle')) {
|
|
1252
|
+
assertCanChangeUserHandle(reqUser(req), '', data.user_handle);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
catch (err) {
|
|
1256
|
+
if (err instanceof BridgeError) {
|
|
1257
|
+
const status = err.code === 'OWNER_REQUIRED' ? 403 : 409;
|
|
1258
|
+
return reply.status(status).send({ error: err.message, code: err.code, ...err.details });
|
|
1259
|
+
}
|
|
1260
|
+
throw err;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
// Scope ref fields (today: task.assignee → person). Rejects a wikilink
|
|
1264
|
+
// that resolves to a non-person; forward references are allowed.
|
|
624
1265
|
try {
|
|
625
|
-
|
|
1266
|
+
assertEntityRefs(workspaceManager, entityType, data, { userExists: (id) => !!authService.findUserById(Number(id)) });
|
|
626
1267
|
}
|
|
627
1268
|
catch (err) {
|
|
628
|
-
|
|
1269
|
+
if (err instanceof EntityRefError || err instanceof UnknownUserRefError) {
|
|
1270
|
+
return reply.status(400).send({ error: err.message, code: err.code, field: err.field });
|
|
1271
|
+
}
|
|
1272
|
+
throw err;
|
|
629
1273
|
}
|
|
630
1274
|
try {
|
|
1275
|
+
// Detect implicit subfolder creation: if the entry lands in a folder
|
|
1276
|
+
// that didn't exist yet, the entity_change below only refreshes the
|
|
1277
|
+
// entry list — clients need a _folder event to refetch the folder tree
|
|
1278
|
+
// (sidebar + list/gallery folder rows). Mirrors the MCP create_entity
|
|
1279
|
+
// path; normalize the same way graph.create does so the existence
|
|
1280
|
+
// check matches the real on-disk path.
|
|
1281
|
+
const normFolder = folder_path ? validateFolderPath(folder_path) : '';
|
|
1282
|
+
const folderExisted = !normFolder || existsSync(join(graph.getRepoPath(), normFolder));
|
|
631
1283
|
const entity = await graph.create({
|
|
632
1284
|
entityType: entityType,
|
|
633
1285
|
entityId,
|
|
634
1286
|
data: { entity_type: entityType, entity_id: entityId, ...data },
|
|
635
1287
|
content,
|
|
1288
|
+
folderPath: folder_path,
|
|
636
1289
|
commitMessage: `Create ${entityType}/${entityId} from web UI`,
|
|
637
1290
|
user: reqGitUser(req),
|
|
638
1291
|
});
|
|
1292
|
+
const actor = req.user?.displayName ?? 'API';
|
|
1293
|
+
const ts = new Date().toISOString();
|
|
639
1294
|
wsHub?.broadcast({
|
|
640
1295
|
type: 'entity_change', action: 'created', repo, entityType, entityId,
|
|
1296
|
+
actor, source: 'api', timestamp: ts,
|
|
1297
|
+
});
|
|
1298
|
+
if (!folderExisted) {
|
|
1299
|
+
wsHub?.broadcast({
|
|
1300
|
+
type: 'entity_change', action: 'created', repo, entityType: '_folder', entityId: normFolder,
|
|
1301
|
+
actor, source: 'api', timestamp: ts,
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
return reply.status(201).send(entity);
|
|
1305
|
+
}
|
|
1306
|
+
catch (err) {
|
|
1307
|
+
return reply.status(400).send({ error: err.message });
|
|
1308
|
+
}
|
|
1309
|
+
});
|
|
1310
|
+
// POST /api/repos/:repo/move — move entry to a different folder within the
|
|
1311
|
+
// same repo. Distinct from /transfer (cross-repo) and /rename (id change).
|
|
1312
|
+
// The entry's type and id are unchanged; only its folder_path moves.
|
|
1313
|
+
fastify.post('/api/repos/:repo/move', async (req, reply) => {
|
|
1314
|
+
const { repo } = req.params;
|
|
1315
|
+
const { entityType, entityId, folder_path } = req.body ?? {};
|
|
1316
|
+
if (!entityType || !entityId || folder_path === undefined) {
|
|
1317
|
+
return reply.status(400).send({ error: 'entityType, entityId, and folder_path are required' });
|
|
1318
|
+
}
|
|
1319
|
+
// Access enforced by graphForWrite (WorkspaceAccessService).
|
|
1320
|
+
const graph = graphForWrite(req, reply, repo);
|
|
1321
|
+
if (!graph)
|
|
1322
|
+
return;
|
|
1323
|
+
try {
|
|
1324
|
+
if (sessionManager)
|
|
1325
|
+
await sessionManager.flush(repo);
|
|
1326
|
+
// If the destination folder doesn't exist yet, the move creates it —
|
|
1327
|
+
// emit a _folder event (alongside the entry update) so clients refetch
|
|
1328
|
+
// the folder tree, not just the entry list. Mirrors MCP move_entity.
|
|
1329
|
+
const normTarget = folder_path ? validateFolderPath(folder_path) : '';
|
|
1330
|
+
const targetExisted = !normTarget || existsSync(join(graph.getRepoPath(), normTarget));
|
|
1331
|
+
const moved = await graph.move(entityType, entityId, folder_path, undefined, reqGitUser(req));
|
|
1332
|
+
const actor = req.user?.displayName ?? 'API';
|
|
1333
|
+
const ts = new Date().toISOString();
|
|
1334
|
+
wsHub?.broadcast({
|
|
1335
|
+
type: 'entity_change', action: 'updated', repo, entityType, entityId,
|
|
1336
|
+
actor, source: 'api', timestamp: ts,
|
|
1337
|
+
});
|
|
1338
|
+
if (!targetExisted) {
|
|
1339
|
+
wsHub?.broadcast({
|
|
1340
|
+
type: 'entity_change', action: 'created', repo, entityType: '_folder', entityId: normTarget,
|
|
1341
|
+
actor, source: 'api', timestamp: ts,
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
return reply.send({ success: true, entity: moved });
|
|
1345
|
+
}
|
|
1346
|
+
catch (err) {
|
|
1347
|
+
const status = err?.name === 'InvalidFolderPathError' ? 400
|
|
1348
|
+
: err.message?.includes('already occupied') ? 409
|
|
1349
|
+
: err.message?.includes('not found') ? 404
|
|
1350
|
+
: 400;
|
|
1351
|
+
return reply.status(status).send({ error: err.message });
|
|
1352
|
+
}
|
|
1353
|
+
});
|
|
1354
|
+
// POST /api/repos/:repo/folders — create a folder (with .gitkeep). Auto-creates parents.
|
|
1355
|
+
// Optional display_name is written to the folder's .studiograph-folder.json
|
|
1356
|
+
// sidecar when set; otherwise the client falls back to toDisplayName(slug).
|
|
1357
|
+
fastify.post('/api/repos/:repo/folders', async (req, reply) => {
|
|
1358
|
+
const { repo } = req.params;
|
|
1359
|
+
const { path, display_name } = req.body ?? {};
|
|
1360
|
+
if (!path || typeof path !== 'string') {
|
|
1361
|
+
return reply.status(400).send({ error: 'path is required' });
|
|
1362
|
+
}
|
|
1363
|
+
// Access enforced by graphForWrite (WorkspaceAccessService).
|
|
1364
|
+
const graph = graphForWrite(req, reply, repo);
|
|
1365
|
+
if (!graph)
|
|
1366
|
+
return;
|
|
1367
|
+
try {
|
|
1368
|
+
await graph.createFolder(path, undefined, reqGitUser(req), {
|
|
1369
|
+
displayName: display_name,
|
|
1370
|
+
});
|
|
1371
|
+
// Emit the _folder sentinel so OTHER connected clients refetch the
|
|
1372
|
+
// folder tree (sidebar + list/gallery folder rows). The local creator
|
|
1373
|
+
// already updated optimistically via workspace.addFolder.
|
|
1374
|
+
wsHub?.broadcast({
|
|
1375
|
+
type: 'entity_change', action: 'created', repo, entityType: '_folder', entityId: validateFolderPath(path),
|
|
641
1376
|
actor: req.user?.displayName ?? 'API',
|
|
642
1377
|
source: 'api', timestamp: new Date().toISOString(),
|
|
643
1378
|
});
|
|
644
|
-
return reply.status(201).send(
|
|
1379
|
+
return reply.status(201).send({ success: true, path });
|
|
645
1380
|
}
|
|
646
1381
|
catch (err) {
|
|
647
1382
|
return reply.status(400).send({ error: err.message });
|
|
648
1383
|
}
|
|
649
1384
|
});
|
|
1385
|
+
// PATCH /api/repos/:repo/folders — rename / move a folder within the repo,
|
|
1386
|
+
// optionally updating its display_name in the same commit. Three shapes:
|
|
1387
|
+
// { from, to } — slug-only rename / move
|
|
1388
|
+
// { from, to, display_name } — slug change + display update
|
|
1389
|
+
// { from, to: from, display_name } — display-only update (no FS move)
|
|
1390
|
+
// `display_name: ""` (or null) clears the override and reverts to the slug
|
|
1391
|
+
// fallback.
|
|
1392
|
+
fastify.patch('/api/repos/:repo/folders', async (req, reply) => {
|
|
1393
|
+
const { repo } = req.params;
|
|
1394
|
+
const { from, to, display_name } = req.body ?? {};
|
|
1395
|
+
if (!from || !to) {
|
|
1396
|
+
return reply.status(400).send({ error: 'from and to are required' });
|
|
1397
|
+
}
|
|
1398
|
+
// Access enforced by graphForWrite (WorkspaceAccessService).
|
|
1399
|
+
const graph = graphForWrite(req, reply, repo);
|
|
1400
|
+
if (!graph)
|
|
1401
|
+
return;
|
|
1402
|
+
try {
|
|
1403
|
+
if (sessionManager)
|
|
1404
|
+
await sessionManager.flush(repo);
|
|
1405
|
+
await graph.renameFolder(from, to, undefined, reqGitUser(req), {
|
|
1406
|
+
displayName: display_name === undefined ? undefined : display_name,
|
|
1407
|
+
});
|
|
1408
|
+
// Rename/move relocates entries on disk, so the _folder sentinel's
|
|
1409
|
+
// refreshRepoAfterFolderMutation (tree + entity summaries) is exactly
|
|
1410
|
+
// what other clients need. Carries the new path as the entityId.
|
|
1411
|
+
wsHub?.broadcast({
|
|
1412
|
+
type: 'entity_change', action: 'updated', repo, entityType: '_folder', entityId: validateFolderPath(to),
|
|
1413
|
+
actor: req.user?.displayName ?? 'API',
|
|
1414
|
+
source: 'api', timestamp: new Date().toISOString(),
|
|
1415
|
+
});
|
|
1416
|
+
return reply.send({ success: true, from, to });
|
|
1417
|
+
}
|
|
1418
|
+
catch (err) {
|
|
1419
|
+
const status = err?.name === 'InvalidFolderPathError' ? 400
|
|
1420
|
+
: err.message?.includes('not found') ? 404
|
|
1421
|
+
: err.message?.includes('already exists') ? 409
|
|
1422
|
+
: 400;
|
|
1423
|
+
return reply.status(status).send({ error: err.message });
|
|
1424
|
+
}
|
|
1425
|
+
});
|
|
1426
|
+
// DELETE /api/repos/:repo/folders?path=X&recursive=true — delete a folder.
|
|
1427
|
+
// Without recursive=true, non-empty folders return 409 with blast-radius
|
|
1428
|
+
// counts so the UI can confirm before retrying.
|
|
1429
|
+
fastify.delete('/api/repos/:repo/folders', async (req, reply) => {
|
|
1430
|
+
const { repo } = req.params;
|
|
1431
|
+
const { path, recursive } = req.query;
|
|
1432
|
+
if (!path) {
|
|
1433
|
+
return reply.status(400).send({ error: 'path query parameter is required' });
|
|
1434
|
+
}
|
|
1435
|
+
// Access enforced by graphForWrite (WorkspaceAccessService).
|
|
1436
|
+
const graph = graphForWrite(req, reply, repo);
|
|
1437
|
+
if (!graph)
|
|
1438
|
+
return;
|
|
1439
|
+
// Workspace Assets store gate (codex P1 / D2). A recursive delete in a
|
|
1440
|
+
// workspace_wide store destroys every asset under the folder, bypassing
|
|
1441
|
+
// the per-asset uploader/owner rule — restrict it to a workspace owner.
|
|
1442
|
+
// (Non-recursive delete is blocked on non-empty folders by FOLDER_NOT_EMPTY,
|
|
1443
|
+
// so it can't take assets with it.)
|
|
1444
|
+
if (recursive === 'true' && workspaceManager.getRepoConfig(repo)?.workspace_wide && !isWorkspaceOwner(reqUser(req)?.role ?? '')) {
|
|
1445
|
+
return reply.status(403).send({ error: 'Only a workspace owner can recursively delete folders in the shared Assets store.' });
|
|
1446
|
+
}
|
|
1447
|
+
try {
|
|
1448
|
+
if (sessionManager)
|
|
1449
|
+
await sessionManager.flush(repo);
|
|
1450
|
+
const result = await graph.deleteFolder(path, {
|
|
1451
|
+
recursive: recursive === 'true',
|
|
1452
|
+
user: reqGitUser(req),
|
|
1453
|
+
});
|
|
1454
|
+
// Emit the _folder sentinel so other clients drop the folder (and any
|
|
1455
|
+
// entries it held, via the entity-summary refresh) from their view.
|
|
1456
|
+
wsHub?.broadcast({
|
|
1457
|
+
type: 'entity_change', action: 'deleted', repo, entityType: '_folder', entityId: validateFolderPath(path),
|
|
1458
|
+
actor: req.user?.displayName ?? 'API',
|
|
1459
|
+
source: 'api', timestamp: new Date().toISOString(),
|
|
1460
|
+
});
|
|
1461
|
+
return reply.send({ success: true, ...result });
|
|
1462
|
+
}
|
|
1463
|
+
catch (err) {
|
|
1464
|
+
if (err?.code === 'FOLDER_NOT_EMPTY') {
|
|
1465
|
+
return reply.status(409).send({
|
|
1466
|
+
error: err.message,
|
|
1467
|
+
code: 'FOLDER_NOT_EMPTY',
|
|
1468
|
+
blast_radius: err.blastRadius,
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
const status = err?.name === 'InvalidFolderPathError' ? 400
|
|
1472
|
+
: err.message?.includes('not found') ? 404
|
|
1473
|
+
: 400;
|
|
1474
|
+
return reply.status(status).send({ error: err.message });
|
|
1475
|
+
}
|
|
1476
|
+
});
|
|
650
1477
|
// POST /api/repos/:repo/commit — flush deferred commits (navigate-away signal)
|
|
651
1478
|
fastify.post('/api/repos/:repo/commit', async (req, reply) => {
|
|
652
1479
|
if (!sessionManager) {
|
|
653
1480
|
return reply.send({ committed: false });
|
|
654
1481
|
}
|
|
655
1482
|
const { repo } = req.params;
|
|
656
|
-
|
|
657
|
-
|
|
1483
|
+
// Access enforced via accessService.canRead \u2014 flush operates on the
|
|
1484
|
+
// SessionManager (no graph instance needed), so a boolean check is
|
|
1485
|
+
// sufficient. Pending commits were already authored by an authenticated
|
|
1486
|
+
// writer; this endpoint just flushes them on navigate-away.
|
|
1487
|
+
if (!accessService.canRead(reqUser(req), repo)) {
|
|
1488
|
+
return reply.status(404).send({ error: `Folder \u201C${repo}\u201D not found` });
|
|
658
1489
|
}
|
|
659
1490
|
const committed = await sessionManager.flush(repo);
|
|
660
1491
|
return reply.send({ committed });
|
|
661
1492
|
});
|
|
1493
|
+
// POST /api/repos/:repo/import — import files into a folder.
|
|
1494
|
+
//
|
|
1495
|
+
// Routing by extension:
|
|
1496
|
+
// .md / .txt / .csv → direct write via importFile() — no LLM
|
|
1497
|
+
// round-trip, preserves the source faithfully. Markdown frontmatter
|
|
1498
|
+
// (entity_type, entity_id) is honored when present; otherwise
|
|
1499
|
+
// sensible defaults.
|
|
1500
|
+
// .pdf + opaque binaries → asset wrapper via uploadAsset() —
|
|
1501
|
+
// creates an `asset` entity, stores the binary in R2 or the local
|
|
1502
|
+
// volume, writes a PDF text sidecar for vector indexing.
|
|
1503
|
+
// .docx → text extraction + agent capture
|
|
1504
|
+
// pipeline (until the mammoth → markdown workstream lands).
|
|
1505
|
+
fastify.post('/api/repos/:repo/import', async (req, reply) => {
|
|
1506
|
+
const { repo } = req.params;
|
|
1507
|
+
// Access enforced by graphForWrite — import creates entities, so write
|
|
1508
|
+
// is required. Graph captured here is reused by the inner closures so
|
|
1509
|
+
// they don't need to re-resolve (which would also bypass the gate).
|
|
1510
|
+
const repoGraph = graphForWrite(req, reply, repo);
|
|
1511
|
+
if (!repoGraph)
|
|
1512
|
+
return;
|
|
1513
|
+
const body = req.body;
|
|
1514
|
+
if (!body?.files || !Array.isArray(body.files)) {
|
|
1515
|
+
return reply.status(400).send({ error: 'Expected { files: [{ filename, content }] }' });
|
|
1516
|
+
}
|
|
1517
|
+
// Reject paths with traversal segments; importFile slugifies the rest.
|
|
1518
|
+
// Empty / undefined = repo root (existing behavior).
|
|
1519
|
+
const targetFolderPath = (body.targetFolderPath ?? '').trim();
|
|
1520
|
+
if (targetFolderPath.split('/').some(seg => seg === '..' || seg === '.')) {
|
|
1521
|
+
return reply.status(400).send({ error: 'Invalid targetFolderPath: contains traversal segments' });
|
|
1522
|
+
}
|
|
1523
|
+
const captureRepo = body.agentDecides ? undefined : repo;
|
|
1524
|
+
const gitUser = reqGitUser(req);
|
|
1525
|
+
// Cancel support (CANCEL): the web client aborts the fetch via an
|
|
1526
|
+
// AbortController. Listen on the RESPONSE stream's `close` — it fires when
|
|
1527
|
+
// the connection drops, and `writableEnded` is false iff we haven't
|
|
1528
|
+
// finished sending the response yet, i.e. a genuine mid-import client
|
|
1529
|
+
// abort (vs the normal post-response close, where writableEnded is true).
|
|
1530
|
+
// `reply.raw` is the ServerResponse, the canonical place to detect that
|
|
1531
|
+
// the peer went away while we're still working; `req.raw` (the parsed
|
|
1532
|
+
// request) is a less reliable signal for an abort during processing.
|
|
1533
|
+
// Best-effort: the batch runner only reads this between batches, so a
|
|
1534
|
+
// single-batch import still commits its completed prefix (additive +
|
|
1535
|
+
// collision-safe). The listener is on this request's per-request
|
|
1536
|
+
// ServerResponse, so it's GC'd with the request — no cross-request leak.
|
|
1537
|
+
let clientGone = false;
|
|
1538
|
+
reply.raw.on('close', () => {
|
|
1539
|
+
if (!reply.raw.writableEnded)
|
|
1540
|
+
clientGone = true;
|
|
1541
|
+
});
|
|
1542
|
+
try {
|
|
1543
|
+
const { extractText, importFile } = await import('../../services/import-service.js');
|
|
1544
|
+
const { importAssetAsEntity } = await import('../../services/asset-import-service.js');
|
|
1545
|
+
const { resolveFormat, isIndexable, FALLBACK_ASSET_ENTRY } = await import('../../core/format-registry.js');
|
|
1546
|
+
const results = [];
|
|
1547
|
+
const CONCURRENCY = 3;
|
|
1548
|
+
async function processDirect(file) {
|
|
1549
|
+
// Reuse the access-checked graph captured in the outer scope. Do
|
|
1550
|
+
// NOT re-resolve via workspaceManager.getGraph here — the audit's
|
|
1551
|
+
// boundary is the outer graphForWrite call.
|
|
1552
|
+
const graph = repoGraph;
|
|
1553
|
+
const result = await importFile(graph, file.filename, file.content, {
|
|
1554
|
+
user: gitUser,
|
|
1555
|
+
// Caller-chosen destination subfolder; importFile combines it
|
|
1556
|
+
// with each file's own relative directory.
|
|
1557
|
+
folderPath: targetFolderPath || undefined,
|
|
1558
|
+
// Defer commits — we batch them into one transaction at the
|
|
1559
|
+
// end of the request so a 500-file import is one git commit
|
|
1560
|
+
// instead of 500. Per-file commits dominated wall time
|
|
1561
|
+
// (~50-200ms each) before this; batching collapses that to
|
|
1562
|
+
// one commit for the whole batch.
|
|
1563
|
+
skipCommit: true,
|
|
1564
|
+
});
|
|
1565
|
+
if (result.status === 'created' && result.path) {
|
|
1566
|
+
return {
|
|
1567
|
+
filename: file.filename,
|
|
1568
|
+
status: 'created',
|
|
1569
|
+
entities: [{ entityId: result.entityId, entityType: result.entityType }],
|
|
1570
|
+
entry: {
|
|
1571
|
+
operation: 'create',
|
|
1572
|
+
entityType: result.entityType,
|
|
1573
|
+
entityId: result.entityId,
|
|
1574
|
+
filePath: result.path,
|
|
1575
|
+
},
|
|
1576
|
+
sidecarPaths: result.sidecarPaths,
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
return { filename: file.filename, status: result.status, error: result.error };
|
|
1580
|
+
}
|
|
1581
|
+
async function processAsset(file) {
|
|
1582
|
+
// Reuse the outer access-checked graph; re-resolving would bypass
|
|
1583
|
+
// the architectural boundary even though the closure captures repo.
|
|
1584
|
+
const graph = repoGraph;
|
|
1585
|
+
try {
|
|
1586
|
+
const buffer = Buffer.from(file.content, 'base64');
|
|
1587
|
+
// Asset-as-embed (Phase B): store the binary folder-owned + create a
|
|
1588
|
+
// properly-TYPED entity (document / dataset×N / material) that embeds
|
|
1589
|
+
// it via asset_ref. No self-owning `asset` entity, no text sidecar
|
|
1590
|
+
// (the seeded body is indexed natively). xlsx fans out to N datasets,
|
|
1591
|
+
// so this returns 1..N entities — `commitEntries` carries them all.
|
|
1592
|
+
const result = await importAssetAsEntity({
|
|
1593
|
+
graph,
|
|
1594
|
+
filename: file.filename,
|
|
1595
|
+
buffer,
|
|
1596
|
+
folderPath: targetFolderPath || undefined,
|
|
1597
|
+
user: gitUser,
|
|
1598
|
+
uploaderUserId: reqUser(req)?.id != null ? String(reqUser(req).id) : null,
|
|
1599
|
+
// Defer commits — the dispatcher's batched commitPending call at
|
|
1600
|
+
// the bottom of the route covers every entry in one transaction.
|
|
1601
|
+
skipCommit: true,
|
|
1602
|
+
});
|
|
1603
|
+
return {
|
|
1604
|
+
filename: file.filename,
|
|
1605
|
+
status: 'created',
|
|
1606
|
+
entities: result.entities.map(e => ({ entityId: e.entityId, entityType: e.entityType })),
|
|
1607
|
+
commitEntries: result.entities.map(e => ({
|
|
1608
|
+
operation: 'create',
|
|
1609
|
+
entityType: e.entityType,
|
|
1610
|
+
entityId: e.entityId,
|
|
1611
|
+
filePath: e.filePath,
|
|
1612
|
+
})),
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
catch (err) {
|
|
1616
|
+
return { filename: file.filename, status: 'error', error: err.message ?? 'Asset import failed' };
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
async function processViaAgent(file) {
|
|
1620
|
+
try {
|
|
1621
|
+
const text = await extractText(file.filename, file.content);
|
|
1622
|
+
if (!text) {
|
|
1623
|
+
log.warn(`[import] No text extracted from ${file.filename}`);
|
|
1624
|
+
return { filename: file.filename, status: 'error', error: 'No text content extracted' };
|
|
1625
|
+
}
|
|
1626
|
+
log.info(`[import] Extracted ${text.length} chars from ${file.filename}`);
|
|
1627
|
+
const captureRes = await fastify.inject({
|
|
1628
|
+
method: 'POST',
|
|
1629
|
+
url: '/api/capture',
|
|
1630
|
+
headers: {
|
|
1631
|
+
'content-type': 'application/json',
|
|
1632
|
+
...(req.headers.authorization ? { authorization: req.headers.authorization } : {}),
|
|
1633
|
+
...(req.headers.cookie ? { cookie: req.headers.cookie } : {}),
|
|
1634
|
+
},
|
|
1635
|
+
payload: JSON.stringify({ text: `[Imported from ${file.filename}]\n\n${text}`, ...(captureRepo ? { repo: captureRepo } : {}) }),
|
|
1636
|
+
});
|
|
1637
|
+
const captureBody = JSON.parse(captureRes.body);
|
|
1638
|
+
if (captureRes.statusCode !== 200 || !captureBody.success) {
|
|
1639
|
+
log.warn(`[import] Capture failed for ${file.filename}: ${captureRes.statusCode} ${captureBody.error ?? JSON.stringify(captureBody).slice(0, 200)}`);
|
|
1640
|
+
return { filename: file.filename, status: 'error', error: captureBody.error ?? 'Agent processing failed' };
|
|
1641
|
+
}
|
|
1642
|
+
return { filename: file.filename, status: 'created', entities: captureBody.entities };
|
|
1643
|
+
}
|
|
1644
|
+
catch (err) {
|
|
1645
|
+
return { filename: file.filename, status: 'error', error: err.message ?? 'Processing failed' };
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
// Route a single file off the format registry — the one source of
|
|
1649
|
+
// truth for every ingestion surface. P0 is a parity refactor: the
|
|
1650
|
+
// routing below reproduces today's behavior exactly. The only
|
|
1651
|
+
// intentional follow-ups are flagged inline (docx/svg transitional
|
|
1652
|
+
// paths; the FALLBACK reject that P0b flips to lenient).
|
|
1653
|
+
async function routeByFormat(file, entry) {
|
|
1654
|
+
// Transitional formats keep the current agent-extraction path until
|
|
1655
|
+
// their real handler lands. After P2 this is svg only — it routes
|
|
1656
|
+
// here until sanitize-html is wired (P3); routing it to uploadAsset
|
|
1657
|
+
// before then would store unsanitized svg (stored-XSS). docx flipped
|
|
1658
|
+
// to the asset path in P2 (uploads as asset, converts on demand).
|
|
1659
|
+
if (entry.transitional === 'agent-extract') {
|
|
1660
|
+
return processViaAgent(file);
|
|
1661
|
+
}
|
|
1662
|
+
switch (entry.handler) {
|
|
1663
|
+
case 'importDirect':
|
|
1664
|
+
return processDirect(file);
|
|
1665
|
+
case 'uploadAsset':
|
|
1666
|
+
// P0 parity: genuinely-unknown types are still rejected. The
|
|
1667
|
+
// lenient generic-asset fallback (accept unknowns as assets) is
|
|
1668
|
+
// P0b — a deliberate behavior change made by removing exactly
|
|
1669
|
+
// this guard.
|
|
1670
|
+
if (entry === FALLBACK_ASSET_ENTRY) {
|
|
1671
|
+
return { filename: file.filename, status: 'error', error: `Unsupported file: ${file.filename}` };
|
|
1672
|
+
}
|
|
1673
|
+
return processAsset(file);
|
|
1674
|
+
default:
|
|
1675
|
+
// importBookmarks is not routed through the dispatcher until
|
|
1676
|
+
// P8 (content-sniff); bookmark exports still use the dedicated
|
|
1677
|
+
// POST /api/repos/:repo/import-bookmarks endpoint, so a bare
|
|
1678
|
+
// .html lands here and is rejected — matching today.
|
|
1679
|
+
return { filename: file.filename, status: 'error', error: `Unsupported file: ${file.filename}` };
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
async function processFile(file) {
|
|
1683
|
+
if (!file.filename) {
|
|
1684
|
+
return { filename: file.filename, status: 'error', error: 'Missing filename' };
|
|
1685
|
+
}
|
|
1686
|
+
// Resolve the format once and thread it through routing.
|
|
1687
|
+
// `searchable` (OV3 server contract) reflects the registry's
|
|
1688
|
+
// DECLARED indexability for the file — the client (P5) renders the
|
|
1689
|
+
// honest "stored as-is / not searchable" callouts from it.
|
|
1690
|
+
// KNOWN LIMITATION: this is declared, not actual. A scanned/encrypted
|
|
1691
|
+
// PDF declares searchable:true (indexAs:'sidecar') but yields no text,
|
|
1692
|
+
// so it won't be flagged "stored as-is" even though its content is
|
|
1693
|
+
// effectively filename-only. Closing that gap needs the extractor to
|
|
1694
|
+
// report actual text-yield back into this flag (P3-indexing scope);
|
|
1695
|
+
// the negative claim stays accurate, the positive one is best-effort.
|
|
1696
|
+
const entry = resolveFormat(file.filename);
|
|
1697
|
+
const searchable = isIndexable(entry);
|
|
1698
|
+
const result = await routeByFormat(file, entry);
|
|
1699
|
+
return { ...result, searchable };
|
|
1700
|
+
}
|
|
1701
|
+
// Process files in cancellable batches of CONCURRENCY. The runner
|
|
1702
|
+
// checks the abort flag between batches: if the client disconnected
|
|
1703
|
+
// (web AbortController), it stops dispatching the remainder, lets the
|
|
1704
|
+
// in-flight batch settle, and returns the completed prefix — which the
|
|
1705
|
+
// commitPending step below commits in one transaction (no uncommitted
|
|
1706
|
+
// files left behind). Cancel is partial + honest: import is additive
|
|
1707
|
+
// and collision-safe.
|
|
1708
|
+
const { runImportBatches } = await import('./import-batch.js');
|
|
1709
|
+
const { results: batched, cancelled } = await runImportBatches(body.files, processFile, { concurrency: CONCURRENCY, isAborted: () => clientGone });
|
|
1710
|
+
results.push(...batched);
|
|
1711
|
+
// Direct-write path deferred git commits — flush them all in one
|
|
1712
|
+
// transaction now. Collapses N per-file commits into one batch
|
|
1713
|
+
// commit (~50ms vs N×50ms). Agent-path files committed inline via
|
|
1714
|
+
// /api/capture so they're not in this set.
|
|
1715
|
+
const pendingEntries = results.flatMap(r => {
|
|
1716
|
+
const pf = r;
|
|
1717
|
+
// Asset-import files carry commitEntries (1..N); direct-write files
|
|
1718
|
+
// carry a single entry. Prefer the plural, fall back to the singular.
|
|
1719
|
+
if (pf.commitEntries && pf.commitEntries.length > 0)
|
|
1720
|
+
return pf.commitEntries;
|
|
1721
|
+
return pf.entry ? [pf.entry] : [];
|
|
1722
|
+
});
|
|
1723
|
+
// Collect every sidecar path produced by the import — these stage in
|
|
1724
|
+
// the same commit as the entity files, so the folder display name
|
|
1725
|
+
// and the entries it labels land atomically. De-dup because two
|
|
1726
|
+
// imported files inside the same Schema_OS/widgets/ would otherwise
|
|
1727
|
+
// produce duplicate sidecar entries.
|
|
1728
|
+
const sidecarPaths = Array.from(new Set(results.flatMap(r => r.sidecarPaths ?? [])));
|
|
1729
|
+
// commitPending failure used to be swallowed and the route returned
|
|
1730
|
+
// success=true even though the files were never committed. The audit
|
|
1731
|
+
// (2026-05) flagged this: a 500-file import that failed mid-commit
|
|
1732
|
+
// looked successful to the client but left N untracked files. Now
|
|
1733
|
+
// the failure propagates as 500 with a partial-failure shape so the
|
|
1734
|
+
// caller knows which entries are stranded.
|
|
1735
|
+
let commitError = null;
|
|
1736
|
+
if (pendingEntries.length > 0) {
|
|
1737
|
+
try {
|
|
1738
|
+
await repoGraph.commitPending(pendingEntries, {
|
|
1739
|
+
commitMessage: `Import ${pendingEntries.length} ${pendingEntries.length === 1 ? 'entry' : 'entries'} from web UI`,
|
|
1740
|
+
user: gitUser,
|
|
1741
|
+
extraPaths: sidecarPaths.length > 0 ? sidecarPaths : undefined,
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
catch (err) {
|
|
1745
|
+
commitError = err instanceof Error ? err.message : String(err);
|
|
1746
|
+
log.warn(`[import] batch commit failed for ${repo}: ${commitError}`);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
const created = results.filter(r => r.status === 'created').length;
|
|
1750
|
+
const entityCount = results.reduce((sum, r) => sum + (r.entities?.length ?? 0), 0);
|
|
1751
|
+
// Single repo_sync broadcast for the whole import. The realtime
|
|
1752
|
+
// store's repo_sync handler refreshes the repo's entity list once
|
|
1753
|
+
// — vs. one entity_change per file, which would do N re-renders
|
|
1754
|
+
// on every connected client and freeze the UI for large imports.
|
|
1755
|
+
// Skip when commit failed: there's nothing for clients to refresh
|
|
1756
|
+
// beyond stranded uncommitted files, and the broadcast would lie
|
|
1757
|
+
// about persistence.
|
|
1758
|
+
if (created > 0 && !commitError) {
|
|
1759
|
+
wsHub?.broadcast({
|
|
1760
|
+
type: 'repo_sync', repo,
|
|
1761
|
+
actor: req.user?.displayName ?? 'API',
|
|
1762
|
+
source: 'api', timestamp: new Date().toISOString(),
|
|
1763
|
+
});
|
|
1764
|
+
// Asset caption/extract enqueue is no longer wired here — it fires
|
|
1765
|
+
// from the single VectorService.onAssetPending chokepoint when the
|
|
1766
|
+
// asset's index state is seeded (covers every upload path uniformly).
|
|
1767
|
+
}
|
|
1768
|
+
const errors = results.filter(r => r.status === 'error').map(r => r.error);
|
|
1769
|
+
if (commitError) {
|
|
1770
|
+
// Files are on disk but uncommitted — partial failure. Surface as
|
|
1771
|
+
// 500 with details so the client doesn't show "imported N entries"
|
|
1772
|
+
// for entries that won't survive a server restart.
|
|
1773
|
+
return reply.status(500).send({
|
|
1774
|
+
success: false,
|
|
1775
|
+
error: `Import wrote ${pendingEntries.length} entries to disk but the git commit failed: ${commitError}`,
|
|
1776
|
+
commitError,
|
|
1777
|
+
uncommittedEntries: pendingEntries.length,
|
|
1778
|
+
imported: created,
|
|
1779
|
+
entityCount,
|
|
1780
|
+
skipped: errors.length,
|
|
1781
|
+
errors,
|
|
1782
|
+
results,
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
return {
|
|
1786
|
+
success: true,
|
|
1787
|
+
imported: created,
|
|
1788
|
+
entityCount,
|
|
1789
|
+
skipped: errors.length,
|
|
1790
|
+
errors,
|
|
1791
|
+
results,
|
|
1792
|
+
// Cancel summary (CANCEL): when the client disconnected mid-import we
|
|
1793
|
+
// committed the completed prefix above and report N of M honestly.
|
|
1794
|
+
// The aborting client typically won't read this (its fetch rejected),
|
|
1795
|
+
// but it keeps the contract honest for any caller that does.
|
|
1796
|
+
cancelled,
|
|
1797
|
+
total: body.files.length,
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
catch (err) {
|
|
1801
|
+
return reply.status(400).send({
|
|
1802
|
+
success: false,
|
|
1803
|
+
error: err instanceof Error ? err.message : 'Unknown error',
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
});
|
|
1807
|
+
// POST /api/repos/:repo/import-bookmarks — import browser bookmark exports as reference entities
|
|
1808
|
+
fastify.post('/api/repos/:repo/import-bookmarks', async (req, reply) => {
|
|
1809
|
+
const { repo } = req.params;
|
|
1810
|
+
// Access enforced by graphForWrite — bookmark import creates entities.
|
|
1811
|
+
const graph = graphForWrite(req, reply, repo);
|
|
1812
|
+
if (!graph)
|
|
1813
|
+
return;
|
|
1814
|
+
const body = req.body;
|
|
1815
|
+
if (!body?.content || typeof body.content !== 'string') {
|
|
1816
|
+
return reply.status(400).send({ error: 'Expected { content: string }' });
|
|
1817
|
+
}
|
|
1818
|
+
try {
|
|
1819
|
+
const { isNetscapeBookmarkFile, importBookmarks } = await import('../../services/bookmark-import-service.js');
|
|
1820
|
+
if (!isNetscapeBookmarkFile(body.content)) {
|
|
1821
|
+
return reply.status(400).send({
|
|
1822
|
+
error: "This doesn't appear to be a browser bookmark export. Export bookmarks from your browser's bookmark manager to generate the correct format.",
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
const user = reqGitUser(req);
|
|
1826
|
+
const result = await importBookmarks(graph, body.content, user);
|
|
1827
|
+
// Single git commit for all created entities
|
|
1828
|
+
if (result.created > 0) {
|
|
1829
|
+
const git = graph.getGitService();
|
|
1830
|
+
if (git) {
|
|
1831
|
+
const commitUser = user ?? { id: 'import', name: 'Import', email: 'import@studiograph' };
|
|
1832
|
+
git.commitAll(`Import ${result.created} bookmark${result.created !== 1 ? 's' : ''} as references`, commitUser);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
return {
|
|
1836
|
+
success: true,
|
|
1837
|
+
created: result.created,
|
|
1838
|
+
skipped: result.skipped,
|
|
1839
|
+
errors: result.errors,
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
catch (err) {
|
|
1843
|
+
return reply.status(400).send({
|
|
1844
|
+
success: false,
|
|
1845
|
+
error: err instanceof Error ? err.message : 'Unknown error',
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
});
|
|
1849
|
+
// POST /api/repos/:repo/assets — generic asset upload.
|
|
1850
|
+
//
|
|
1851
|
+
// Multipart `file` field. Creates a wrapper `asset` entity whose
|
|
1852
|
+
// frontmatter points at the stored binary (R2 or local volume) via
|
|
1853
|
+
// `asset_url`. PDFs additionally get a text sidecar at
|
|
1854
|
+
// `<workspace>/.studiograph/assets-text/asset/<id>.txt` so the vector
|
|
1855
|
+
// indexer can RAG them without bloating the markdown body. See
|
|
1856
|
+
// docs/asset-architecture.md.
|
|
1857
|
+
fastify.post('/api/repos/:repo/assets', async (req, reply) => {
|
|
1858
|
+
const { repo } = req.params;
|
|
1859
|
+
// Access enforced by graphForWrite (WorkspaceAccessService).
|
|
1860
|
+
const graph = graphForWrite(req, reply, repo);
|
|
1861
|
+
if (!graph)
|
|
1862
|
+
return;
|
|
1863
|
+
// Per-route fileSize override — the global multipart limit (10MB) is
|
|
1864
|
+
// tuned for inline image embeds; full asset uploads accept up to the
|
|
1865
|
+
// 100MB cap enforced inside AssetService.upload().
|
|
1866
|
+
let file;
|
|
1867
|
+
try {
|
|
1868
|
+
file = await req.file({ limits: { fileSize: 100 * 1024 * 1024 } });
|
|
1869
|
+
}
|
|
1870
|
+
catch (err) {
|
|
1871
|
+
return reply.status(400).send({ error: err?.message ?? 'Invalid multipart upload' });
|
|
1872
|
+
}
|
|
1873
|
+
if (!file)
|
|
1874
|
+
return reply.status(400).send({ error: 'No file uploaded' });
|
|
1875
|
+
const buffer = await file.toBuffer();
|
|
1876
|
+
try {
|
|
1877
|
+
// Asset-as-embed (B2): uploading to the Assets store produces a
|
|
1878
|
+
// FREE-STANDING, folder-owned asset — NOT an `asset` entity. The asset
|
|
1879
|
+
// manager lists it directly; the homepage capture embeds its URL into
|
|
1880
|
+
// the entry it creates and deletes it by assetId if unused. Folder-owned
|
|
1881
|
+
// assets are repo-scoped, so the `folder` subpath doesn't apply here.
|
|
1882
|
+
const stem = file.filename.replace(/\.[^.]+$/, '');
|
|
1883
|
+
const displayName = req.query.name?.trim()
|
|
1884
|
+
|| stem.replace(/[_-]+/g, ' ').replace(/\b\w/g, c => c.toUpperCase())
|
|
1885
|
+
|| file.filename;
|
|
1886
|
+
const stored = await graph.uploadAsset('material', // ignored for ownership when `owner` is set
|
|
1887
|
+
'asset', // ignored
|
|
1888
|
+
file.filename, buffer, undefined, {
|
|
1889
|
+
owner: { type: 'folder', id: repo },
|
|
1890
|
+
uploaderUserId: reqUser(req)?.id != null ? String(reqUser(req).id) : null,
|
|
1891
|
+
name: displayName,
|
|
1892
|
+
});
|
|
1893
|
+
wsHub?.broadcast({
|
|
1894
|
+
type: 'repo_sync',
|
|
1895
|
+
repo,
|
|
1896
|
+
actor: req.user?.displayName ?? 'API',
|
|
1897
|
+
source: 'api',
|
|
1898
|
+
timestamp: new Date().toISOString(),
|
|
1899
|
+
});
|
|
1900
|
+
// Image caption / search indexing is derived by the asset-index sweep,
|
|
1901
|
+
// same as every other free-standing asset.
|
|
1902
|
+
return reply.send({
|
|
1903
|
+
success: true,
|
|
1904
|
+
assetId: stored.assetId,
|
|
1905
|
+
asset_url: stored.publicUrl,
|
|
1906
|
+
asset_kind: stored.mediaType,
|
|
1907
|
+
filename: stored.filename,
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
catch (err) {
|
|
1911
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1912
|
+
const status = msg.includes('not configured') ? 501
|
|
1913
|
+
: msg.includes('Unsupported asset extension') ? 415
|
|
1914
|
+
: 400;
|
|
1915
|
+
return reply.status(status).send({ success: false, error: msg });
|
|
1916
|
+
}
|
|
1917
|
+
});
|
|
662
1918
|
}
|
|
663
1919
|
//# sourceMappingURL=graph-api.js.map
|