@taskforcehq/taskforce 0.3.306 → 0.3.308

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.
Files changed (232) hide show
  1. package/README.md +0 -2
  2. package/dist/Taskforce.module.css +317 -27
  3. package/dist/TaskforceCore.js +1 -0
  4. package/dist/TaskforceCore.test.js +178 -0
  5. package/dist/components/activity/EntityActivityTimeline.d.ts +34 -0
  6. package/dist/components/activity/EntityActivityTimeline.js +81 -0
  7. package/dist/components/context/ContextAttachmentManager.js +15 -3
  8. package/dist/components/features/AgentsModule.d.ts +5 -1
  9. package/dist/components/features/AgentsModule.js +142 -39
  10. package/dist/components/features/AgentsModule.test.js +260 -11
  11. package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +3 -1
  12. package/dist/components/features/AnnotatedAttachmentWorkspace.js +118 -4
  13. package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +52 -0
  14. package/dist/components/features/DocumentIndex.d.ts +2 -2
  15. package/dist/components/features/DocumentIndex.js +3 -4
  16. package/dist/components/features/DocumentViewer.d.ts +3 -1
  17. package/dist/components/features/DocumentViewer.js +6 -5
  18. package/dist/components/features/DocumentWorkspace.d.ts +10 -2
  19. package/dist/components/features/DocumentWorkspace.js +15 -4
  20. package/dist/components/features/DocumentWorkspace.test.js +20 -3
  21. package/dist/components/features/TaskSettings.d.ts +2 -0
  22. package/dist/components/features/TaskSettings.js +83 -78
  23. package/dist/components/features/TaskSettings.test.js +171 -82
  24. package/dist/components/task/TaskActionHeader.js +11 -5
  25. package/dist/components/task/TaskActionHeader.test.js +10 -0
  26. package/dist/components/task/TaskCard.js +6 -2
  27. package/dist/components/task/TaskContextUpload.test.js +3 -3
  28. package/dist/components/task/TaskForm.js +14 -64
  29. package/dist/components/task/TaskKanban.js +0 -8
  30. package/dist/components/task/TaskList.js +5 -1
  31. package/dist/components/task/TaskList.test.js +4 -0
  32. package/dist/components/ui/HierarchyActionBadge.d.ts +10 -0
  33. package/dist/components/ui/HierarchyActionBadge.js +5 -0
  34. package/dist/components/views/AppShellHeader.d.ts +3 -1
  35. package/dist/components/views/AppShellHeader.js +27 -11
  36. package/dist/components/views/AppShellHeader.test.js +18 -1
  37. package/dist/components/views/PlanComparisonPage.d.ts +20 -15
  38. package/dist/components/views/PlanComparisonPage.js +221 -82
  39. package/dist/components/views/PlanComparisonPage.test.js +268 -14
  40. package/dist/components/views/PlansPage.js +16 -10
  41. package/dist/components/views/PlansPage.test.js +4 -2
  42. package/dist/components/views/StandaloneLayout.js +256 -69
  43. package/dist/components/views/WidgetView.js +10 -8
  44. package/dist/components/views/panels/PlanningDrawer.d.ts +19 -6
  45. package/dist/components/views/panels/PlanningDrawer.js +259 -51
  46. package/dist/components/views/panels/PlanningDrawer.test.js +392 -4
  47. package/dist/config/envSchema.js +1 -0
  48. package/dist/core/AiProfileService.d.ts +7 -0
  49. package/dist/core/AiProfileService.js +460 -35
  50. package/dist/core/AiProfiles.test.js +648 -2
  51. package/dist/core/AttachmentLinkService.d.ts +3 -0
  52. package/dist/core/AttachmentLinkService.js +58 -13
  53. package/dist/core/GlobalSettingsService.js +30 -0
  54. package/dist/core/GlobalSettingsService.test.d.ts +1 -0
  55. package/dist/core/GlobalSettingsService.test.js +45 -0
  56. package/dist/core/McpTokenService.d.ts +12 -0
  57. package/dist/core/McpTokenService.js +44 -1
  58. package/dist/core/PlanEntitlementService.d.ts +20 -0
  59. package/dist/core/PlanEntitlementService.js +215 -19
  60. package/dist/core/PlanFeatureCatalog.test.js +55 -0
  61. package/dist/core/PlanningEntities.test.js +366 -4
  62. package/dist/core/TaskActivityService.d.ts +28 -1
  63. package/dist/core/TaskActivityService.js +227 -46
  64. package/dist/core/TaskActivitySync.test.d.ts +1 -0
  65. package/dist/core/TaskActivitySync.test.js +90 -0
  66. package/dist/core/TaskAuditTimeline.test.js +96 -0
  67. package/dist/core/Taskforce.d.ts +204 -26
  68. package/dist/core/Taskforce.ids.test.js +58 -0
  69. package/dist/core/Taskforce.js +781 -71
  70. package/dist/core/Taskforce.mcpAuth.test.js +109 -0
  71. package/dist/core/types.d.ts +10 -0
  72. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +2 -1
  73. package/dist/hooks/bootstrapTimeouts.d.ts +4 -0
  74. package/dist/hooks/bootstrapTimeouts.js +8 -0
  75. package/dist/hooks/bootstrapTimeouts.test.d.ts +1 -0
  76. package/dist/hooks/bootstrapTimeouts.test.js +12 -0
  77. package/dist/hooks/sync/orchestratorShared.d.ts +3 -3
  78. package/dist/hooks/sync/orchestratorShared.js +61 -1
  79. package/dist/hooks/sync/recovery.js +8 -0
  80. package/dist/hooks/sync/recovery.test.js +5 -5
  81. package/dist/hooks/sync/transfers.d.ts +2 -1
  82. package/dist/hooks/sync/transfers.js +36 -9
  83. package/dist/hooks/sync/transfers.test.js +534 -2
  84. package/dist/hooks/useFilterSort.js +13 -13
  85. package/dist/hooks/useFilterSort.test.js +87 -1
  86. package/dist/hooks/useSyncOrchestrator.js +50 -5
  87. package/dist/hooks/useTaskforce.d.ts +85 -50
  88. package/dist/hooks/useTaskforce.js +44 -14
  89. package/dist/hooks/useTaskforce.runtime-routing.test.js +94 -0
  90. package/dist/hooks/useTaskforce.sync-behavior.test.js +99 -0
  91. package/dist/hooks/useWorkspaceSyncController.js +7 -1
  92. package/dist/hooks/useWorkspaceSyncController.test.js +48 -0
  93. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +14 -7
  94. package/dist/mcp/canonicalAssetHelpers.d.ts +16 -4
  95. package/dist/mcp/canonicalAssetHelpers.js +18 -10
  96. package/dist/mcp/canonicalAssetHelpers.test.d.ts +1 -0
  97. package/dist/mcp/canonicalAssetHelpers.test.js +67 -0
  98. package/dist/mcp/clientIntegrations.d.ts +1 -0
  99. package/dist/mcp/clientIntegrations.js +25 -0
  100. package/dist/mcp/clientIntegrations.test.js +14 -3
  101. package/dist/mcp/clientProfileDefaults.js +3 -3
  102. package/dist/mcp/collaborationRegistrar.js +7 -7
  103. package/dist/mcp/documentAssetRegistrar.js +113 -7
  104. package/dist/mcp/runtime.d.ts +1 -0
  105. package/dist/mcp/runtime.js +969 -122
  106. package/dist/mcp/runtime.test.js +1083 -11
  107. package/dist/mcp/taskAttachmentTypes.d.ts +1 -1
  108. package/dist/mcp/taskPlanningRegistrar.js +48 -0
  109. package/dist/mcp/toolRegistry.d.ts +56 -0
  110. package/dist/mcp/toolRegistry.js +8 -0
  111. package/dist/mcp/toolRegistry.test.js +4 -0
  112. package/dist/migrations/taskSchemaMigrations.js +58 -15
  113. package/dist/server/aiProfileAvatarAssets.d.ts +49 -0
  114. package/dist/server/aiProfileAvatarAssets.js +319 -0
  115. package/dist/server/aiProfileAvatarAssets.test.d.ts +1 -0
  116. package/dist/server/aiProfileAvatarAssets.test.js +213 -0
  117. package/dist/server/annotatedAttachmentsRoutes.test.js +66 -7
  118. package/dist/server/index.js +4 -0
  119. package/dist/server/index.test.js +6 -1
  120. package/dist/server/routes/admin.js +328 -9
  121. package/dist/server/routes/annotatedAttachments.js +51 -0
  122. package/dist/server/routes/auth.d.ts +11 -0
  123. package/dist/server/routes/auth.js +189 -27
  124. package/dist/server/routes/authSupport.d.ts +5 -0
  125. package/dist/server/routes/authSupport.js +13 -1
  126. package/dist/server/routes/billing.d.ts +1 -0
  127. package/dist/server/routes/billing.js +116 -63
  128. package/dist/server/routes/billing.test.js +162 -11
  129. package/dist/server/routes/documents.js +10 -0
  130. package/dist/server/routes/shared.js +0 -10
  131. package/dist/server/routes/sync.integration.test.js +221 -0
  132. package/dist/server/routes/sync.js +13 -3
  133. package/dist/server/routes/syncSnapshotRoutes.js +2 -0
  134. package/dist/server/routes/tasks.js +12 -9
  135. package/dist/server/routes.js +87 -8
  136. package/dist/server/routes.test.js +972 -23
  137. package/dist/shared/aiProfileSurfaceType.d.ts +1 -1
  138. package/dist/shared/aiProfileSurfaceType.js +10 -8
  139. package/dist/storage/objectStorage.d.ts +1 -0
  140. package/dist/storage/objectStorage.js +2 -1
  141. package/dist/storage/objectStorageClient.d.ts +1 -0
  142. package/dist/storage/objectStorageClient.js +41 -8
  143. package/dist/storage/objectStorageClient.test.js +26 -1
  144. package/dist/storage/objectStorageConfig.js +3 -1
  145. package/dist/storage/objectStorageConfig.test.js +13 -1
  146. package/dist/sync/collaborationSyncPayload.js +2 -0
  147. package/dist/sync/collaborationSyncPayload.test.js +1 -0
  148. package/dist/sync/collaborationSyncState.d.ts +3 -3
  149. package/dist/sync/collaborationSyncState.js +19 -12
  150. package/dist/sync/contentSyncPayload.d.ts +5 -1
  151. package/dist/sync/contentSyncPayload.js +9 -0
  152. package/dist/sync/contentSyncPayload.test.js +39 -0
  153. package/dist/sync/contentSyncState.d.ts +5 -1
  154. package/dist/sync/contentSyncState.js +231 -37
  155. package/dist/sync/planningSyncPayload.js +5 -0
  156. package/dist/sync/planningSyncPayload.test.js +24 -0
  157. package/dist/sync/syncApplyHandlers.d.ts +4 -4
  158. package/dist/sync/syncApplyHandlers.js +89 -31
  159. package/dist/sync/syncApplyHandlers.test.js +184 -4
  160. package/dist/sync/syncResourceAdapters.d.ts +2 -0
  161. package/dist/sync/syncResourceAdapters.js +2 -0
  162. package/dist/sync/syncService.d.ts +1 -0
  163. package/dist/sync/syncService.js +13 -1
  164. package/dist/sync/syncService.test.js +39 -0
  165. package/dist/sync/workspacePullFeed.d.ts +4 -0
  166. package/dist/sync/workspacePullFeed.js +129 -70
  167. package/dist/sync/workspacePullFeed.test.js +49 -0
  168. package/dist/sync/workspaceSyncModel.d.ts +28 -6
  169. package/dist/sync/workspaceSyncModel.js +159 -45
  170. package/dist/sync/workspaceSyncModel.test.js +269 -3
  171. package/dist/types.d.ts +33 -2
  172. package/dist/ui/agent-logos/ChatGPT.png +0 -0
  173. package/dist/ui/agent-logos/Gemini CLI.jpeg +0 -0
  174. package/dist/ui/agent-logos/antigravity.jpeg +0 -0
  175. package/dist/ui/agent-logos/claude-code.jpeg +0 -0
  176. package/dist/ui/agent-logos/codex.jpeg +0 -0
  177. package/dist/ui/agent-logos/cursor.png +0 -0
  178. package/dist/ui/agent-logos/openclaw.jpeg +0 -0
  179. package/dist/ui/agent-logos/windsurf.png +0 -0
  180. package/dist/ui/assets/AgentsModule-BS4Pi_nC.js +1 -0
  181. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BVKew-mH.css +1 -0
  182. package/dist/ui/assets/AnnotatedAttachmentWorkspace-CCckzWYZ.js +3 -0
  183. package/dist/ui/assets/ContextAttachmentManager-CfG_ER7C.js +3 -0
  184. package/dist/ui/assets/DocumentWorkspace-BVaWPA25.js +252 -0
  185. package/dist/ui/assets/DocumentWorkspace-D4rJKXzd.css +1 -0
  186. package/dist/ui/assets/EntityActivityTimeline-DSj0ThaK.js +1 -0
  187. package/dist/ui/assets/EntityActivityTimeline-DyqvvhuF.css +1 -0
  188. package/dist/ui/assets/{InitiativesModule-9E6ParrY.js → InitiativesModule-AxMkWtxH.js} +1 -1
  189. package/dist/ui/assets/PlansPage-B_z-D6Nh.css +1 -0
  190. package/dist/ui/assets/PlansPage-Ndt7mYfo.js +1 -0
  191. package/dist/ui/assets/TaskContextUpload-Dw4rTF4i.js +1 -0
  192. package/dist/ui/assets/TaskSettings-f2ga42_V.js +9 -0
  193. package/dist/ui/assets/{WorkflowsModule-BA7jcEpH.js → WorkflowsModule-E4UL3mov.js} +4 -4
  194. package/dist/ui/assets/documentReferences-BOUcJm6-.js +1 -0
  195. package/dist/ui/assets/index-BvAbqx5Q.css +1 -0
  196. package/dist/ui/assets/index-MAHKvFqp.js +4 -0
  197. package/dist/ui/assets/{vendor-icons-DuEd_65c.js → vendor-icons-B32hGuF2.js} +1 -1
  198. package/dist/ui/index.html +3 -3
  199. package/dist/ui/og-image.png +0 -0
  200. package/dist/ui/product/agent manager.png +0 -0
  201. package/dist/ui/product/agent_manager_02.png +0 -0
  202. package/dist/utils/aiProfileEvents.d.ts +7 -0
  203. package/dist/utils/aiProfileEvents.js +6 -0
  204. package/dist/utils/assignees.d.ts +6 -0
  205. package/dist/utils/assignees.js +8 -1
  206. package/dist/utils/assignees.test.d.ts +1 -0
  207. package/dist/utils/assignees.test.js +25 -0
  208. package/dist/utils/avatarDisplay.d.ts +1 -0
  209. package/dist/utils/avatarDisplay.js +15 -0
  210. package/dist/utils/contextFiles.d.ts +3 -1
  211. package/dist/utils/contextFiles.js +2 -1
  212. package/dist/utils/taskActivity.d.ts +7 -1
  213. package/dist/utils/taskActivity.js +63 -1
  214. package/dist/utils/taskActivity.test.js +23 -1
  215. package/dist/utils/taskSearch.d.ts +22 -0
  216. package/dist/utils/taskSearch.js +87 -0
  217. package/dist/utils/taskSearch.test.d.ts +1 -0
  218. package/dist/utils/taskSearch.test.js +69 -0
  219. package/dist/utils/taskUtils.js +10 -3
  220. package/dist/utils/taskUtils.test.d.ts +1 -0
  221. package/dist/utils/taskUtils.test.js +19 -0
  222. package/package.json +1 -1
  223. package/dist/ui/assets/AgentsModule-DrLawwNl.js +0 -1
  224. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +0 -1
  225. package/dist/ui/assets/AnnotatedAttachmentWorkspace-Z9_whf7F.js +0 -3
  226. package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +0 -252
  227. package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +0 -1
  228. package/dist/ui/assets/PlansPage-CvfnMiWq.css +0 -1
  229. package/dist/ui/assets/PlansPage-DRXscYxH.js +0 -1
  230. package/dist/ui/assets/TaskSettings-DZX4jk7e.js +0 -9
  231. package/dist/ui/assets/index-9eT8e0Lu.css +0 -1
  232. package/dist/ui/assets/index-C6k75jr8.js +0 -6
@@ -72,6 +72,7 @@ import { buildCloudMcpRuntimeAuth, resolveErrorStatusCode } from './shared.js';
72
72
  import { buildAuthBehavior, buildSiteBehavior, getRouteEmailConfig, resolvePublicAuthBaseUrl, resolveRuntimeConfigBaseUrl, } from './authSupport.js';
73
73
  import { applyWorkspaceRepair, scanWorkspaceRepair } from '../../sync/workspaceRepair.js';
74
74
  import { testMintAppleClientSecret } from '../auth/providers/appleClientSecret.js';
75
+ const PRICING_PAGE_DESCRIPTION_MAX_LENGTH = 2000;
75
76
  // === SECTION: Registration ===
76
77
  export function registerAdminRoutes(deps) {
77
78
  const { addRoute, core, context, requestWorkspaceId, requestUserId, requestRole, requestHeader, requestRuntimeMode, ensureAppAccess, ensureAdminRole, ensureCloudPlanData, requireCloudRuntime, resolveAuthenticatedUserId, resolveAdminRequestActor, auditAdminAction, resolveEffectiveObjectStorageConfig, getObjectStorageClient, taskAssetStore, normalizeDocumentPathInput, ensureWithinDir, getAssetMetadata, markAssetDeleted, clearAssetDeleted, removeAssetMetadata, getStorageTelemetry, appendStorageTelemetry, getStorageIntegrityHistory, appendStorageIntegrityHistory, getDocumentPurgeHistory, appendDocumentPurgeHistory, getCollaborationTelemetry, isCollaborationPolicyEnabled, normalizeMcpOauthScopeString, MCP_TOKEN_SCOPE_VALUES, MCP_CLOUD_TOOL_SCOPE_REQUIREMENTS, MCP_WRITE_RATE_LIMIT_ENABLED, MCP_WRITE_RATE_LIMIT_ACTOR_MAX_REQUESTS, MCP_WRITE_RATE_LIMIT_WORKSPACE_MAX_REQUESTS, MCP_WRITE_RATE_LIMIT_WINDOW_MS, } = deps;
@@ -79,6 +80,26 @@ export function registerAdminRoutes(deps) {
79
80
  const workspaceAssetStore = typeof core.getDatabaseAdapter === 'function' && typeof core.getTenantId === 'function'
80
81
  ? new WorkspaceAssetStore(core.getDatabaseAdapter(), core.getTenantId())
81
82
  : null;
83
+ const isAiProfileAvatarStorageKey = (storageKey, workspaceId) => {
84
+ const normalized = String(storageKey || '').replace(/\\/g, '/');
85
+ return normalized.startsWith(`workspaces/${workspaceId}/assets/images/ai-profiles/`)
86
+ && normalized.includes('/avatar/');
87
+ };
88
+ const listReferencedAiProfileAvatarStorageKeys = (workspaceId) => {
89
+ const referenced = new Set();
90
+ const profiles = typeof core.listAiProfiles === 'function'
91
+ ? core.listAiProfiles(workspaceId, { includeArchived: true })
92
+ : [];
93
+ for (const profile of Array.isArray(profiles) ? profiles : []) {
94
+ for (const value of [profile?.avatarUrl, profile?.avatarSourceUrl]) {
95
+ const storageKey = normalizeDocumentPathInput(value);
96
+ if (!storageKey || !isAiProfileAvatarStorageKey(storageKey, workspaceId))
97
+ continue;
98
+ referenced.add(storageKey);
99
+ }
100
+ }
101
+ return referenced;
102
+ };
82
103
  // === SECTION: Email helpers ===
83
104
  const resolveEffectiveVirusScanSettings = () => {
84
105
  const settings = core.getGlobalSettings().objectStorage || {};
@@ -95,11 +116,15 @@ export function registerAdminRoutes(deps) {
95
116
  };
96
117
  // === SECTION: MCP helpers ===
97
118
  const MCP_CONNECTION_TEST_TIMEOUT_MS = 5000;
98
- const writeMcpHttpError = (res, statusCode, message) => {
119
+ const writeMcpHttpError = (res, statusCode, message, data) => {
99
120
  res.writeHead(statusCode, { 'Content-Type': 'application/json' });
100
121
  res.end(JSON.stringify({
101
122
  jsonrpc: '2.0',
102
- error: { code: -32000, message },
123
+ error: {
124
+ code: -32000,
125
+ message,
126
+ ...(data ? { data } : {}),
127
+ },
103
128
  id: null,
104
129
  }));
105
130
  };
@@ -394,6 +419,8 @@ export function registerAdminRoutes(deps) {
394
419
  authenticatedViaOAuth: oauthAuth || null,
395
420
  userId: null,
396
421
  ...bootstrap,
422
+ aiProfileToken: bootstrap.aiProfileToken || oauthAuth?.aiProfileToken || null,
423
+ aiProfileName: bootstrap.aiProfileName || oauthAuth?.aiProfileName || null,
397
424
  };
398
425
  }
399
426
  core.addMcpAuditLog({
@@ -506,6 +533,7 @@ export function registerAdminRoutes(deps) {
506
533
  return;
507
534
  }
508
535
  const authScopes = auth.authenticatedViaToken?.scopes || auth.authenticatedViaOAuth?.scopes || [];
536
+ const requestId = String(req.headers['x-request-id'] || req.headers['x-railway-request-id'] || '').trim() || randomUUID();
509
537
  const deniedToolName = findDeniedCloudMcpToolName(body, authScopes);
510
538
  if (deniedToolName) {
511
539
  core.addMcpAuditLog({
@@ -515,7 +543,7 @@ export function registerAdminRoutes(deps) {
515
543
  tokenId: auth.authenticatedViaToken?.tokenId || auth.authenticatedViaOAuth?.grantId || null,
516
544
  toolName: deniedToolName,
517
545
  details: {
518
- requestId: String(req.headers['x-request-id'] || req.headers['x-railway-request-id'] || '').trim() || randomUUID(),
546
+ requestId,
519
547
  ip: req.socket?.remoteAddress || null,
520
548
  userAgent: requestHeader(req, 'user-agent', '') || null,
521
549
  outcome: 'deny',
@@ -528,11 +556,13 @@ export function registerAdminRoutes(deps) {
528
556
  }
529
557
  if (!applyCloudMcpWriteRateLimit(req, res, auth, body))
530
558
  return;
559
+ let runtime = null;
531
560
  try {
532
- const runtime = createTaskforceMcpServer({
561
+ runtime = createTaskforceMcpServer({
533
562
  projectRoot: core.getPaths().projectRoot,
534
563
  tenantId: core.getTenantId(),
535
564
  workspaceId: auth.workspaceId,
565
+ core,
536
566
  serverName: 'taskforce-cloud',
537
567
  logStartup: false,
538
568
  runtimeMode: 'cloud',
@@ -540,7 +570,7 @@ export function registerAdminRoutes(deps) {
540
570
  req,
541
571
  auth,
542
572
  requestHeader,
543
- requestId: String(req.headers['x-request-id'] || req.headers['x-railway-request-id'] || '').trim() || randomUUID(),
573
+ requestId,
544
574
  authScopes,
545
575
  }),
546
576
  });
@@ -557,8 +587,27 @@ export function registerAdminRoutes(deps) {
557
587
  recordCloudMcpCredentialUsage(req, auth.authenticatedViaToken, auth.authenticatedViaOAuth);
558
588
  }
559
589
  catch (error) {
590
+ console.error('[Taskforce MCP] Cloud transport request failed:', {
591
+ requestId,
592
+ kind: 'tool_transport_failure',
593
+ message: error instanceof Error ? error.message : String(error),
594
+ });
560
595
  if (!res.headersSent) {
561
- writeMcpHttpError(res, 500, 'Internal server error.');
596
+ writeMcpHttpError(res, 500, 'MCP transport failure.', {
597
+ kind: 'tool_transport_failure',
598
+ retryable: true,
599
+ requestId,
600
+ });
601
+ }
602
+ }
603
+ finally {
604
+ if (runtime) {
605
+ try {
606
+ await runtime.server.close();
607
+ }
608
+ catch (closeError) {
609
+ console.warn('[Taskforce MCP] Cloud transport cleanup failed:', closeError);
610
+ }
562
611
  }
563
612
  }
564
613
  };
@@ -664,6 +713,88 @@ export function registerAdminRoutes(deps) {
664
713
  res.end(JSON.stringify({ success: false, error: String(error?.message || 'Unable to list feature catalog'), code: error?.code }));
665
714
  }
666
715
  });
716
+ addRoute('POST', '/api/platform/features', async (req, res) => {
717
+ if (!ensureCloudPlanData(req, res))
718
+ return;
719
+ if (!ensureAdminRole(req)) {
720
+ res.writeHead(403, { 'Content-Type': 'application/json' });
721
+ res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
722
+ return;
723
+ }
724
+ const body = await parseJsonBody(req);
725
+ const label = String(body?.label || '');
726
+ const description = body?.description === undefined || body?.description === null
727
+ ? body?.description
728
+ : String(body.description);
729
+ const publicLabel = body?.publicLabel === undefined || body?.publicLabel === null
730
+ ? body?.publicLabel
731
+ : String(body.publicLabel);
732
+ const publicDescription = body?.publicDescription === undefined || body?.publicDescription === null
733
+ ? body?.publicDescription
734
+ : String(body.publicDescription);
735
+ const publicDescriptionVisible = body?.publicDescriptionVisible === undefined || body?.publicDescriptionVisible === null
736
+ ? body?.publicDescriptionVisible
737
+ : (body.publicDescriptionVisible === false ? false : true);
738
+ const publicDisplayOrder = body?.publicDisplayOrder === undefined || body?.publicDisplayOrder === null || body?.publicDisplayOrder === ''
739
+ ? null
740
+ : Math.max(0, Math.floor(Number(body.publicDisplayOrder)));
741
+ if (!label.trim()) {
742
+ res.writeHead(400, { 'Content-Type': 'application/json' });
743
+ res.end(JSON.stringify({ success: false, error: 'Feature label is required.', code: 'PLAN_FEATURE_LABEL_REQUIRED' }));
744
+ return;
745
+ }
746
+ if (label.trim().length > 120) {
747
+ res.writeHead(400, { 'Content-Type': 'application/json' });
748
+ res.end(JSON.stringify({ success: false, error: 'Feature label must be 120 characters or fewer.', code: 'PLAN_FEATURE_LABEL_TOO_LONG' }));
749
+ return;
750
+ }
751
+ if (typeof description === 'string' && description.trim().length > 280) {
752
+ res.writeHead(400, { 'Content-Type': 'application/json' });
753
+ res.end(JSON.stringify({ success: false, error: 'Feature description must be 280 characters or fewer.', code: 'PLAN_FEATURE_DESCRIPTION_TOO_LONG' }));
754
+ return;
755
+ }
756
+ if (typeof publicLabel === 'string' && publicLabel.trim().length > 120) {
757
+ res.writeHead(400, { 'Content-Type': 'application/json' });
758
+ res.end(JSON.stringify({ success: false, error: 'Public label must be 120 characters or fewer.', code: 'PLAN_FEATURE_PUBLIC_LABEL_TOO_LONG' }));
759
+ return;
760
+ }
761
+ if (typeof publicDescription === 'string' && publicDescription.trim().length > 280) {
762
+ res.writeHead(400, { 'Content-Type': 'application/json' });
763
+ res.end(JSON.stringify({ success: false, error: 'Public description must be 280 characters or fewer.', code: 'PLAN_FEATURE_PUBLIC_DESCRIPTION_TOO_LONG' }));
764
+ return;
765
+ }
766
+ if (publicDisplayOrder !== null && !Number.isFinite(publicDisplayOrder)) {
767
+ res.writeHead(400, { 'Content-Type': 'application/json' });
768
+ res.end(JSON.stringify({ success: false, error: 'Public display order must be a non-negative number.', code: 'PLAN_FEATURE_PUBLIC_DISPLAY_ORDER_INVALID' }));
769
+ return;
770
+ }
771
+ try {
772
+ const feature = core.createCustomPlanFeatureCatalogEntry({
773
+ label,
774
+ description,
775
+ publicLabel,
776
+ publicDescription,
777
+ publicDescriptionVisible,
778
+ publicDisplayOrder
779
+ });
780
+ auditAdminAction(req, 'platform-plan-feature-catalog-created', {
781
+ featureKey: feature.featureKey,
782
+ label: feature.label,
783
+ description: feature.description,
784
+ publicLabel: feature.publicLabel ?? null,
785
+ publicDescription: feature.publicDescription ?? null,
786
+ publicDescriptionVisible: feature.publicDescriptionVisible ?? null,
787
+ publicDisplayOrder: feature.publicDisplayOrder ?? null
788
+ });
789
+ res.writeHead(200, { 'Content-Type': 'application/json' });
790
+ res.end(JSON.stringify({ success: true, feature }));
791
+ }
792
+ catch (error) {
793
+ const statusCode = resolveErrorStatusCode(error, 400);
794
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
795
+ res.end(JSON.stringify({ success: false, error: String(error?.message || 'Unable to create feature catalog entry'), code: error?.code }));
796
+ }
797
+ });
667
798
  addRoute('PATCH', '/api/platform/features/:featureKey', async (req, res, params) => {
668
799
  if (!ensureCloudPlanData(req, res))
669
800
  return;
@@ -674,15 +805,34 @@ export function registerAdminRoutes(deps) {
674
805
  }
675
806
  const body = await parseJsonBody(req);
676
807
  const featureKey = String(params.featureKey || '').trim();
808
+ const label = body?.label === undefined || body?.label === null
809
+ ? body?.label
810
+ : String(body.label);
811
+ const description = body?.description === undefined || body?.description === null
812
+ ? body?.description
813
+ : String(body.description);
677
814
  const publicLabel = body?.publicLabel === undefined || body?.publicLabel === null
678
815
  ? body?.publicLabel
679
816
  : String(body.publicLabel);
680
817
  const publicDescription = body?.publicDescription === undefined || body?.publicDescription === null
681
818
  ? body?.publicDescription
682
819
  : String(body.publicDescription);
820
+ const publicDescriptionVisible = body?.publicDescriptionVisible === undefined || body?.publicDescriptionVisible === null
821
+ ? body?.publicDescriptionVisible
822
+ : (body.publicDescriptionVisible === false ? false : true);
683
823
  const publicDisplayOrder = body?.publicDisplayOrder === undefined || body?.publicDisplayOrder === null || body?.publicDisplayOrder === ''
684
824
  ? null
685
825
  : Math.max(0, Math.floor(Number(body.publicDisplayOrder)));
826
+ if (typeof label === 'string' && label.trim().length > 120) {
827
+ res.writeHead(400, { 'Content-Type': 'application/json' });
828
+ res.end(JSON.stringify({ success: false, error: 'Feature label must be 120 characters or fewer.', code: 'PLAN_FEATURE_LABEL_TOO_LONG' }));
829
+ return;
830
+ }
831
+ if (typeof description === 'string' && description.trim().length > 280) {
832
+ res.writeHead(400, { 'Content-Type': 'application/json' });
833
+ res.end(JSON.stringify({ success: false, error: 'Feature description must be 280 characters or fewer.', code: 'PLAN_FEATURE_DESCRIPTION_TOO_LONG' }));
834
+ return;
835
+ }
686
836
  if (typeof publicLabel === 'string' && publicLabel.trim().length > 120) {
687
837
  res.writeHead(400, { 'Content-Type': 'application/json' });
688
838
  res.end(JSON.stringify({ success: false, error: 'Public label must be 120 characters or fewer.', code: 'PLAN_FEATURE_PUBLIC_LABEL_TOO_LONG' }));
@@ -701,14 +851,20 @@ export function registerAdminRoutes(deps) {
701
851
  try {
702
852
  const feature = core.updatePlanFeatureCatalogMarketingCopy({
703
853
  featureKey,
854
+ label,
855
+ description,
704
856
  publicLabel,
705
857
  publicDescription,
858
+ publicDescriptionVisible,
706
859
  publicDisplayOrder
707
860
  });
708
861
  auditAdminAction(req, 'platform-plan-feature-catalog-updated', {
709
862
  featureKey,
863
+ label: feature.label,
864
+ description: feature.description,
710
865
  publicLabel: feature.publicLabel ?? null,
711
866
  publicDescription: feature.publicDescription ?? null,
867
+ publicDescriptionVisible: feature.publicDescriptionVisible ?? null,
712
868
  publicDisplayOrder: feature.publicDisplayOrder ?? null
713
869
  });
714
870
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -720,6 +876,27 @@ export function registerAdminRoutes(deps) {
720
876
  res.end(JSON.stringify({ success: false, error: String(error?.message || 'Unable to update feature catalog entry'), code: error?.code }));
721
877
  }
722
878
  });
879
+ addRoute('DELETE', '/api/platform/features/:featureKey', async (req, res, params) => {
880
+ if (!ensureCloudPlanData(req, res))
881
+ return;
882
+ if (!ensureAdminRole(req)) {
883
+ res.writeHead(403, { 'Content-Type': 'application/json' });
884
+ res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
885
+ return;
886
+ }
887
+ const featureKey = String(params.featureKey || '').trim();
888
+ try {
889
+ core.deleteCustomPlanFeatureCatalogEntry(featureKey);
890
+ auditAdminAction(req, 'platform-plan-feature-catalog-deleted', { featureKey });
891
+ res.writeHead(200, { 'Content-Type': 'application/json' });
892
+ res.end(JSON.stringify({ success: true }));
893
+ }
894
+ catch (error) {
895
+ const statusCode = resolveErrorStatusCode(error, 400);
896
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
897
+ res.end(JSON.stringify({ success: false, error: String(error?.message || 'Unable to delete feature catalog entry'), code: error?.code }));
898
+ }
899
+ });
723
900
  addRoute('POST', '/api/platform/plans/:planId/versions', async (req, res, params) => {
724
901
  if (!ensureCloudPlanData(req, res))
725
902
  return;
@@ -1231,8 +1408,10 @@ export function registerAdminRoutes(deps) {
1231
1408
  }
1232
1409
  const globalSettings = core.getGlobalSettings();
1233
1410
  const authSettings = globalSettings.auth || {};
1411
+ const siteSettings = globalSettings.site || {};
1234
1412
  const onboardingPolicy = core.getOnboardingPolicy();
1235
1413
  const effective = buildAuthBehavior(core);
1414
+ const effectiveSite = buildSiteBehavior(core);
1236
1415
  res.writeHead(200, { 'Content-Type': 'application/json' });
1237
1416
  res.end(JSON.stringify({
1238
1417
  settings: {
@@ -1242,6 +1421,13 @@ export function registerAdminRoutes(deps) {
1242
1421
  ...(typeof authSettings.authRateLimitEnabled === 'boolean' ? { authRateLimitEnabled: authSettings.authRateLimitEnabled } : {}),
1243
1422
  ...(typeof authSettings.authRateLimitMaxRequests === 'number' ? { authRateLimitMaxRequests: authSettings.authRateLimitMaxRequests } : {}),
1244
1423
  ...(typeof authSettings.authRateLimitWindowMs === 'number' ? { authRateLimitWindowMs: authSettings.authRateLimitWindowMs } : {}),
1424
+ ...(typeof siteSettings.showRunTaskforceLocally === 'boolean' ? { showRunTaskforceLocally: siteSettings.showRunTaskforceLocally } : {}),
1425
+ ...(typeof siteSettings.pricingPageEnabled === 'boolean' ? { pricingPageEnabled: siteSettings.pricingPageEnabled } : {}),
1426
+ ...(typeof siteSettings.animatedBackgroundEnabled === 'boolean' ? { animatedBackgroundEnabled: siteSettings.animatedBackgroundEnabled } : {}),
1427
+ ...(siteSettings.siteTheme === 'light' || siteSettings.siteTheme === 'dark' ? { siteTheme: siteSettings.siteTheme } : {}),
1428
+ ...(typeof siteSettings.pricingPageTitle === 'string' ? { pricingPageTitle: siteSettings.pricingPageTitle } : {}),
1429
+ ...(typeof siteSettings.pricingPageDescription === 'string' ? { pricingPageDescription: siteSettings.pricingPageDescription } : {}),
1430
+ ...(typeof siteSettings.pricingPageShowPublicDescriptions === 'boolean' ? { pricingPageShowPublicDescriptions: siteSettings.pricingPageShowPublicDescriptions } : {}),
1245
1431
  onboardingPolicyMode: onboardingPolicy.mode,
1246
1432
  onboardingPlanId: onboardingPolicy.planId
1247
1433
  },
@@ -1252,6 +1438,13 @@ export function registerAdminRoutes(deps) {
1252
1438
  authRateLimitEnabled: effective.authRateLimitEnabled,
1253
1439
  authRateLimitMaxRequests: effective.authRateLimitMaxRequests,
1254
1440
  authRateLimitWindowMs: effective.authRateLimitWindowMs,
1441
+ showRunTaskforceLocally: effectiveSite.showRunTaskforceLocally,
1442
+ pricingPageEnabled: effectiveSite.pricingPageEnabled,
1443
+ animatedBackgroundEnabled: effectiveSite.animatedBackgroundEnabled,
1444
+ siteTheme: effectiveSite.siteTheme,
1445
+ pricingPageTitle: effectiveSite.pricingPageTitle,
1446
+ pricingPageDescription: effectiveSite.pricingPageDescription,
1447
+ pricingPageShowPublicDescriptions: effectiveSite.pricingPageShowPublicDescriptions,
1255
1448
  onboardingPolicyMode: effective.onboardingPolicyMode,
1256
1449
  onboardingPlanId: effective.onboardingPlanId
1257
1450
  }
@@ -1273,11 +1466,31 @@ export function registerAdminRoutes(deps) {
1273
1466
  : {}),
1274
1467
  ...(typeof siteSettings.pricingPageEnabled === 'boolean'
1275
1468
  ? { pricingPageEnabled: siteSettings.pricingPageEnabled }
1469
+ : {}),
1470
+ ...(typeof siteSettings.animatedBackgroundEnabled === 'boolean'
1471
+ ? { animatedBackgroundEnabled: siteSettings.animatedBackgroundEnabled }
1472
+ : {}),
1473
+ ...(siteSettings.siteTheme === 'light' || siteSettings.siteTheme === 'dark'
1474
+ ? { siteTheme: siteSettings.siteTheme }
1475
+ : {}),
1476
+ ...(typeof siteSettings.pricingPageTitle === 'string'
1477
+ ? { pricingPageTitle: siteSettings.pricingPageTitle }
1478
+ : {}),
1479
+ ...(typeof siteSettings.pricingPageDescription === 'string'
1480
+ ? { pricingPageDescription: siteSettings.pricingPageDescription }
1481
+ : {}),
1482
+ ...(typeof siteSettings.pricingPageShowPublicDescriptions === 'boolean'
1483
+ ? { pricingPageShowPublicDescriptions: siteSettings.pricingPageShowPublicDescriptions }
1276
1484
  : {})
1277
1485
  },
1278
1486
  status: {
1279
1487
  showRunTaskforceLocally: siteBehavior.showRunTaskforceLocally,
1280
- pricingPageEnabled: siteBehavior.pricingPageEnabled
1488
+ pricingPageEnabled: siteBehavior.pricingPageEnabled,
1489
+ animatedBackgroundEnabled: siteBehavior.animatedBackgroundEnabled,
1490
+ siteTheme: siteBehavior.siteTheme,
1491
+ pricingPageTitle: siteBehavior.pricingPageTitle,
1492
+ pricingPageDescription: siteBehavior.pricingPageDescription,
1493
+ pricingPageShowPublicDescriptions: siteBehavior.pricingPageShowPublicDescriptions
1281
1494
  }
1282
1495
  }));
1283
1496
  };
@@ -1448,9 +1661,11 @@ export function registerAdminRoutes(deps) {
1448
1661
  }
1449
1662
  const reqUrl = new URL(req.url || '/api/taskforce/admin/orphaned-assets', 'http://localhost');
1450
1663
  const workspaceId = String(reqUrl.searchParams.get('workspaceId') || requestWorkspaceId(req) || '').trim();
1664
+ const aiProfileAvatarStorageKeys = listReferencedAiProfileAvatarStorageKeys(workspaceId);
1451
1665
  const assets = workspaceAssetStore.listAllByWorkspace(workspaceId, {
1452
1666
  includeDeleted: false
1453
- }).filter((asset) => workspaceAssetStore.listLinksForAsset(asset.assetId, workspaceId).every((link) => !!link.deletedAt))
1667
+ }).filter((asset) => (!aiProfileAvatarStorageKeys.has(asset.storageKey)
1668
+ && workspaceAssetStore.listLinksForAsset(asset.assetId, workspaceId).every((link) => !!link.deletedAt)))
1454
1669
  .map((asset) => ({
1455
1670
  assetId: asset.assetId,
1456
1671
  workspaceId: asset.workspaceId,
@@ -1529,6 +1744,11 @@ export function registerAdminRoutes(deps) {
1529
1744
  res.end(JSON.stringify({ error: 'Asset not found.' }));
1530
1745
  return;
1531
1746
  }
1747
+ if (listReferencedAiProfileAvatarStorageKeys(workspaceId).has(asset.storageKey)) {
1748
+ res.writeHead(409, { 'Content-Type': 'application/json' });
1749
+ res.end(JSON.stringify({ error: 'Asset is still referenced by an AI profile avatar.' }));
1750
+ return;
1751
+ }
1532
1752
  const objectStorageClient = getObjectStorageClient();
1533
1753
  const { basePath } = core.getPaths();
1534
1754
  try {
@@ -1891,6 +2111,25 @@ export function registerAdminRoutes(deps) {
1891
2111
  : body?.onboardingPlanId === null
1892
2112
  ? null
1893
2113
  : currentOnboardingPolicy.planId;
2114
+ const pricingPageTitle = typeof body?.pricingPageTitle === 'string'
2115
+ ? body.pricingPageTitle.trim()
2116
+ : undefined;
2117
+ const pricingPageDescription = typeof body?.pricingPageDescription === 'string'
2118
+ ? body.pricingPageDescription.trim()
2119
+ : undefined;
2120
+ const siteTheme = body?.siteTheme === 'light' || body?.siteTheme === 'dark'
2121
+ ? body.siteTheme
2122
+ : undefined;
2123
+ if (pricingPageTitle !== undefined && pricingPageTitle.length > 120) {
2124
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2125
+ res.end(JSON.stringify({ error: 'Pricing page title must be 120 characters or fewer.', code: 'PRICING_PAGE_TITLE_TOO_LONG' }));
2126
+ return;
2127
+ }
2128
+ if (pricingPageDescription !== undefined && pricingPageDescription.length > PRICING_PAGE_DESCRIPTION_MAX_LENGTH) {
2129
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2130
+ res.end(JSON.stringify({ error: `Pricing page description must be ${PRICING_PAGE_DESCRIPTION_MAX_LENGTH} characters or fewer.`, code: 'PRICING_PAGE_DESCRIPTION_TOO_LONG' }));
2131
+ return;
2132
+ }
1894
2133
  const validation = core.validateOnboardingPolicy({
1895
2134
  mode: onboardingPolicyMode,
1896
2135
  planId: onboardingPlanId
@@ -1900,6 +2139,29 @@ export function registerAdminRoutes(deps) {
1900
2139
  res.end(JSON.stringify({ error: validation.message || 'Invalid onboarding policy.', code: validation.code, details: validation.details }));
1901
2140
  return;
1902
2141
  }
2142
+ const siteSettingsUpdate = {
2143
+ ...(typeof body?.showRunTaskforceLocally === 'boolean'
2144
+ ? { showRunTaskforceLocally: body.showRunTaskforceLocally }
2145
+ : {}),
2146
+ ...(typeof body?.pricingPageEnabled === 'boolean'
2147
+ ? { pricingPageEnabled: body.pricingPageEnabled }
2148
+ : {}),
2149
+ ...(typeof body?.animatedBackgroundEnabled === 'boolean'
2150
+ ? { animatedBackgroundEnabled: body.animatedBackgroundEnabled }
2151
+ : {}),
2152
+ ...(siteTheme !== undefined
2153
+ ? { siteTheme }
2154
+ : {}),
2155
+ ...(pricingPageTitle !== undefined
2156
+ ? { pricingPageTitle }
2157
+ : {}),
2158
+ ...(pricingPageDescription !== undefined
2159
+ ? { pricingPageDescription }
2160
+ : {}),
2161
+ ...(typeof body?.pricingPageShowPublicDescriptions === 'boolean'
2162
+ ? { pricingPageShowPublicDescriptions: body.pricingPageShowPublicDescriptions }
2163
+ : {})
2164
+ };
1903
2165
  core.updateGlobalSettings({
1904
2166
  auth: {
1905
2167
  ...(typeof body?.allowSelfRegistration === 'boolean' ? { allowSelfRegistration: body.allowSelfRegistration } : {}),
@@ -1916,7 +2178,10 @@ export function registerAdminRoutes(deps) {
1916
2178
  mode: onboardingPolicyMode,
1917
2179
  planId: onboardingPlanId ?? null
1918
2180
  }
1919
- }
2181
+ },
2182
+ ...(Object.keys(siteSettingsUpdate).length > 0
2183
+ ? { site: siteSettingsUpdate }
2184
+ : {})
1920
2185
  });
1921
2186
  res.writeHead(200, { 'Content-Type': 'application/json' });
1922
2187
  res.end(JSON.stringify({ success: true }));
@@ -1928,6 +2193,25 @@ export function registerAdminRoutes(deps) {
1928
2193
  return;
1929
2194
  }
1930
2195
  const body = await parseJsonBody(req);
2196
+ const pricingPageTitle = typeof body?.pricingPageTitle === 'string'
2197
+ ? body.pricingPageTitle.trim()
2198
+ : undefined;
2199
+ const pricingPageDescription = typeof body?.pricingPageDescription === 'string'
2200
+ ? body.pricingPageDescription.trim()
2201
+ : undefined;
2202
+ const siteTheme = body?.siteTheme === 'light' || body?.siteTheme === 'dark'
2203
+ ? body.siteTheme
2204
+ : undefined;
2205
+ if (pricingPageTitle !== undefined && pricingPageTitle.length > 120) {
2206
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2207
+ res.end(JSON.stringify({ error: 'Pricing page title must be 120 characters or fewer.', code: 'PRICING_PAGE_TITLE_TOO_LONG' }));
2208
+ return;
2209
+ }
2210
+ if (pricingPageDescription !== undefined && pricingPageDescription.length > PRICING_PAGE_DESCRIPTION_MAX_LENGTH) {
2211
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2212
+ res.end(JSON.stringify({ error: `Pricing page description must be ${PRICING_PAGE_DESCRIPTION_MAX_LENGTH} characters or fewer.`, code: 'PRICING_PAGE_DESCRIPTION_TOO_LONG' }));
2213
+ return;
2214
+ }
1931
2215
  core.updateGlobalSettings({
1932
2216
  site: {
1933
2217
  ...(typeof body?.showRunTaskforceLocally === 'boolean'
@@ -1935,6 +2219,21 @@ export function registerAdminRoutes(deps) {
1935
2219
  : {}),
1936
2220
  ...(typeof body?.pricingPageEnabled === 'boolean'
1937
2221
  ? { pricingPageEnabled: body.pricingPageEnabled }
2222
+ : {}),
2223
+ ...(typeof body?.animatedBackgroundEnabled === 'boolean'
2224
+ ? { animatedBackgroundEnabled: body.animatedBackgroundEnabled }
2225
+ : {}),
2226
+ ...(siteTheme !== undefined
2227
+ ? { siteTheme }
2228
+ : {}),
2229
+ ...(pricingPageTitle !== undefined
2230
+ ? { pricingPageTitle }
2231
+ : {}),
2232
+ ...(pricingPageDescription !== undefined
2233
+ ? { pricingPageDescription }
2234
+ : {}),
2235
+ ...(typeof body?.pricingPageShowPublicDescriptions === 'boolean'
2236
+ ? { pricingPageShowPublicDescriptions: body.pricingPageShowPublicDescriptions }
1938
2237
  : {})
1939
2238
  }
1940
2239
  });
@@ -2509,6 +2808,26 @@ export function registerAdminRoutes(deps) {
2509
2808
  res.end(JSON.stringify({ success: false, error: error.message, code: error?.code || 'MCP_TOKEN_REVOKE_FAILED' }));
2510
2809
  }
2511
2810
  });
2811
+ addRoute('DELETE', '/api/taskforce/settings/mcp/tokens/:id', async (req, res, params) => {
2812
+ if (!ensureAppAccess(req, res))
2813
+ return;
2814
+ const userId = resolveAuthenticatedUserId(req);
2815
+ if (!userId || userId === 'anonymous') {
2816
+ res.writeHead(401, { 'Content-Type': 'application/json' });
2817
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
2818
+ return;
2819
+ }
2820
+ try {
2821
+ const deleted = core.deleteMcpAccessToken({ actorUserId: userId, workspaceId: requestWorkspaceId(req), tokenId: params.id });
2822
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2823
+ res.end(JSON.stringify({ success: true, deleted }));
2824
+ }
2825
+ catch (error) {
2826
+ const statusCode = resolveErrorStatusCode(error);
2827
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
2828
+ res.end(JSON.stringify({ success: false, error: error.message, code: error?.code || 'MCP_TOKEN_DELETE_FAILED' }));
2829
+ }
2830
+ });
2512
2831
  addRoute('POST', '/api/taskforce/settings/mcp/tokens/:id/regenerate', async (req, res, params) => {
2513
2832
  if (!ensureAppAccess(req, res))
2514
2833
  return;
@@ -40,6 +40,57 @@ export function registerAnnotatedAttachmentRoutes(deps) {
40
40
  });
41
41
  return false;
42
42
  };
43
+ addRoute('GET', '/api/taskforce/annotated-attachments/images', async (req, res) => {
44
+ const workspaceId = requestWorkspaceId(req);
45
+ if (!ensureAnnotatedAttachmentAccess(req, res, workspaceId))
46
+ return;
47
+ try {
48
+ const assetStore = core.getWorkspaceAssetStore();
49
+ const imageAssets = assetStore
50
+ .listAllByWorkspace(workspaceId, { kind: 'image' })
51
+ .filter((entry) => !entry.deletedAt);
52
+ const images = imageAssets.map((asset) => {
53
+ const activeLinks = assetStore
54
+ .listLinksForAsset(asset.assetId, workspaceId)
55
+ .filter((entry) => !entry.deletedAt)
56
+ .filter((entry) => entry.targetType !== 'task' || (!!entry.taskId && !!core.getTask(entry.taskId, workspaceId)));
57
+ const activeTaskLinks = activeLinks
58
+ .filter((entry) => entry.targetType === 'task' && !!entry.taskId)
59
+ .filter((entry) => !!core.getTask(entry.taskId, workspaceId));
60
+ const firstTaskLink = activeTaskLinks[0] || null;
61
+ const linkedTask = firstTaskLink?.taskId
62
+ ? core.getTask(firstTaskLink.taskId, workspaceId)
63
+ : null;
64
+ const sessions = core.listAnnotatedAttachmentSessions({
65
+ workspaceId,
66
+ imageAssetId: asset.assetId,
67
+ });
68
+ const displayName = String(asset.logicalName || asset.originalFilename || asset.assetId).trim() || asset.assetId;
69
+ return {
70
+ assetId: asset.assetId,
71
+ referenceNumber: typeof asset.referenceNumber === 'number' ? asset.referenceNumber : null,
72
+ imageReferenceLabel: getImageReferenceLabel(asset) || null,
73
+ path: `/api/taskforce/context/${encodeURIComponent(asset.storageKey)}`,
74
+ displayName,
75
+ originalFilename: asset.originalFilename || null,
76
+ mimeType: asset.mimeType,
77
+ sizeBytes: asset.sizeBytes,
78
+ updatedAt: asset.updatedAt || null,
79
+ attachmentCount: activeTaskLinks.length,
80
+ sessionCount: sessions.length,
81
+ taskId: firstTaskLink?.taskId || null,
82
+ taskReferenceLabel: linkedTask?.referenceLabel || null,
83
+ };
84
+ });
85
+ res.setHeader('Cache-Control', 'no-store');
86
+ writeJson(res, 200, { images });
87
+ }
88
+ catch (error) {
89
+ writeJson(res, resolveErrorStatusCode(error, 400), {
90
+ error: String(error?.message || 'Failed to list annotated attachment images.'),
91
+ });
92
+ }
93
+ });
43
94
  addRoute('GET', '/api/taskforce/annotated-attachments/images/:reference', async (req, res, params) => {
44
95
  const workspaceId = requestWorkspaceId(req);
45
96
  if (!ensureAnnotatedAttachmentAccess(req, res, workspaceId))
@@ -38,6 +38,8 @@
38
38
  * GET /api/taskforce/auth/workspace-members
39
39
  * GET /api/taskforce/workspace/assignee-options
40
40
  * POST /api/taskforce/workspace/ai-profiles/avatar
41
+ * POST /api/taskforce/workspace/ai-profiles/avatar/upload
42
+ * POST /api/taskforce/workspace/ai-profiles/surface-type
41
43
  * GET /api/taskforce/admin/users
42
44
  * POST /api/taskforce/admin/users/invite
43
45
  * PATCH /api/taskforce/admin/users/:id/role
@@ -52,6 +54,7 @@ import type { TaskforceCore } from '../../core/Taskforce.js';
52
54
  import { type RouteHandler, type RouteContext } from './primitives.js';
53
55
  import type { ResolvedObjectStorageConfig } from '../../storage/objectStorage.js';
54
56
  import { ObjectStorageClient } from '../../storage/objectStorageClient.js';
57
+ import type { WorkspaceAssetStore } from '../../storage/workspaceAssetStore.js';
55
58
  export interface AuthRouteDependencies {
56
59
  addRoute: (method: string, path: string, handler: RouteHandler) => void;
57
60
  core: TaskforceCore;
@@ -73,5 +76,13 @@ export interface AuthRouteDependencies {
73
76
  };
74
77
  resolveEffectiveObjectStorageConfig: () => ResolvedObjectStorageConfig;
75
78
  getObjectStorageClient: () => ObjectStorageClient | null;
79
+ workspaceAssetStore: WorkspaceAssetStore | null;
80
+ ensureWithinDir: (candidatePath: string, rootDir: string) => string | null;
81
+ markAssetDeleted: (pathValue: string) => unknown;
82
+ appendStorageTelemetry: (event: string, payload?: {
83
+ path?: string;
84
+ status?: string;
85
+ provider?: string;
86
+ }) => void;
76
87
  }
77
88
  export declare function registerAuthRoutes(deps: AuthRouteDependencies): void;