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