@taskforcehq/taskforce 0.3.306 → 0.3.307

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 (215) 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.js +9 -1
  38. package/dist/components/views/PlanComparisonPage.test.js +19 -0
  39. package/dist/components/views/PlansPage.js +1 -1
  40. package/dist/components/views/StandaloneLayout.js +256 -69
  41. package/dist/components/views/WidgetView.js +10 -8
  42. package/dist/components/views/panels/PlanningDrawer.d.ts +19 -6
  43. package/dist/components/views/panels/PlanningDrawer.js +259 -51
  44. package/dist/components/views/panels/PlanningDrawer.test.js +392 -4
  45. package/dist/config/envSchema.js +1 -0
  46. package/dist/core/AiProfileService.d.ts +7 -0
  47. package/dist/core/AiProfileService.js +460 -35
  48. package/dist/core/AiProfiles.test.js +648 -2
  49. package/dist/core/AttachmentLinkService.d.ts +3 -0
  50. package/dist/core/AttachmentLinkService.js +58 -13
  51. package/dist/core/GlobalSettingsService.js +12 -0
  52. package/dist/core/McpTokenService.d.ts +12 -0
  53. package/dist/core/McpTokenService.js +44 -1
  54. package/dist/core/PlanningEntities.test.js +366 -4
  55. package/dist/core/TaskActivityService.d.ts +28 -1
  56. package/dist/core/TaskActivityService.js +227 -46
  57. package/dist/core/TaskActivitySync.test.d.ts +1 -0
  58. package/dist/core/TaskActivitySync.test.js +90 -0
  59. package/dist/core/TaskAuditTimeline.test.js +96 -0
  60. package/dist/core/Taskforce.d.ts +190 -26
  61. package/dist/core/Taskforce.ids.test.js +58 -0
  62. package/dist/core/Taskforce.js +775 -71
  63. package/dist/core/Taskforce.mcpAuth.test.js +109 -0
  64. package/dist/core/types.d.ts +7 -0
  65. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +2 -1
  66. package/dist/hooks/bootstrapTimeouts.d.ts +4 -0
  67. package/dist/hooks/bootstrapTimeouts.js +8 -0
  68. package/dist/hooks/bootstrapTimeouts.test.d.ts +1 -0
  69. package/dist/hooks/bootstrapTimeouts.test.js +12 -0
  70. package/dist/hooks/sync/orchestratorShared.d.ts +3 -3
  71. package/dist/hooks/sync/orchestratorShared.js +61 -1
  72. package/dist/hooks/sync/recovery.js +8 -0
  73. package/dist/hooks/sync/recovery.test.js +5 -5
  74. package/dist/hooks/sync/transfers.d.ts +2 -1
  75. package/dist/hooks/sync/transfers.js +36 -9
  76. package/dist/hooks/sync/transfers.test.js +534 -2
  77. package/dist/hooks/useFilterSort.js +13 -13
  78. package/dist/hooks/useFilterSort.test.js +87 -1
  79. package/dist/hooks/useSyncOrchestrator.js +50 -5
  80. package/dist/hooks/useTaskforce.d.ts +85 -50
  81. package/dist/hooks/useTaskforce.js +44 -14
  82. package/dist/hooks/useTaskforce.runtime-routing.test.js +94 -0
  83. package/dist/hooks/useTaskforce.sync-behavior.test.js +99 -0
  84. package/dist/hooks/useWorkspaceSyncController.js +7 -1
  85. package/dist/hooks/useWorkspaceSyncController.test.js +48 -0
  86. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +14 -7
  87. package/dist/mcp/canonicalAssetHelpers.d.ts +16 -4
  88. package/dist/mcp/canonicalAssetHelpers.js +18 -10
  89. package/dist/mcp/canonicalAssetHelpers.test.d.ts +1 -0
  90. package/dist/mcp/canonicalAssetHelpers.test.js +67 -0
  91. package/dist/mcp/clientIntegrations.d.ts +1 -0
  92. package/dist/mcp/clientIntegrations.js +25 -0
  93. package/dist/mcp/clientIntegrations.test.js +14 -3
  94. package/dist/mcp/clientProfileDefaults.js +3 -3
  95. package/dist/mcp/collaborationRegistrar.js +7 -7
  96. package/dist/mcp/documentAssetRegistrar.js +113 -7
  97. package/dist/mcp/runtime.d.ts +1 -0
  98. package/dist/mcp/runtime.js +969 -122
  99. package/dist/mcp/runtime.test.js +1083 -11
  100. package/dist/mcp/taskAttachmentTypes.d.ts +1 -1
  101. package/dist/mcp/taskPlanningRegistrar.js +48 -0
  102. package/dist/mcp/toolRegistry.d.ts +56 -0
  103. package/dist/mcp/toolRegistry.js +8 -0
  104. package/dist/mcp/toolRegistry.test.js +4 -0
  105. package/dist/migrations/taskSchemaMigrations.js +21 -0
  106. package/dist/server/aiProfileAvatarAssets.d.ts +49 -0
  107. package/dist/server/aiProfileAvatarAssets.js +319 -0
  108. package/dist/server/aiProfileAvatarAssets.test.d.ts +1 -0
  109. package/dist/server/aiProfileAvatarAssets.test.js +213 -0
  110. package/dist/server/annotatedAttachmentsRoutes.test.js +66 -7
  111. package/dist/server/index.js +4 -0
  112. package/dist/server/index.test.js +6 -1
  113. package/dist/server/routes/admin.js +187 -9
  114. package/dist/server/routes/annotatedAttachments.js +51 -0
  115. package/dist/server/routes/auth.d.ts +11 -0
  116. package/dist/server/routes/auth.js +189 -27
  117. package/dist/server/routes/authSupport.d.ts +4 -0
  118. package/dist/server/routes/authSupport.js +12 -1
  119. package/dist/server/routes/billing.js +11 -0
  120. package/dist/server/routes/billing.test.js +3 -0
  121. package/dist/server/routes/documents.js +10 -0
  122. package/dist/server/routes/shared.js +0 -10
  123. package/dist/server/routes/sync.integration.test.js +221 -0
  124. package/dist/server/routes/sync.js +13 -3
  125. package/dist/server/routes/syncSnapshotRoutes.js +2 -0
  126. package/dist/server/routes/tasks.js +12 -9
  127. package/dist/server/routes.js +87 -8
  128. package/dist/server/routes.test.js +890 -22
  129. package/dist/shared/aiProfileSurfaceType.d.ts +1 -1
  130. package/dist/shared/aiProfileSurfaceType.js +10 -8
  131. package/dist/storage/objectStorage.d.ts +1 -0
  132. package/dist/storage/objectStorage.js +2 -1
  133. package/dist/storage/objectStorageClient.d.ts +1 -0
  134. package/dist/storage/objectStorageClient.js +41 -8
  135. package/dist/storage/objectStorageClient.test.js +26 -1
  136. package/dist/storage/objectStorageConfig.js +3 -1
  137. package/dist/storage/objectStorageConfig.test.js +13 -1
  138. package/dist/sync/collaborationSyncPayload.js +2 -0
  139. package/dist/sync/collaborationSyncPayload.test.js +1 -0
  140. package/dist/sync/collaborationSyncState.d.ts +3 -3
  141. package/dist/sync/collaborationSyncState.js +19 -12
  142. package/dist/sync/contentSyncPayload.d.ts +5 -1
  143. package/dist/sync/contentSyncPayload.js +9 -0
  144. package/dist/sync/contentSyncPayload.test.js +39 -0
  145. package/dist/sync/contentSyncState.d.ts +5 -1
  146. package/dist/sync/contentSyncState.js +231 -37
  147. package/dist/sync/planningSyncPayload.js +5 -0
  148. package/dist/sync/planningSyncPayload.test.js +24 -0
  149. package/dist/sync/syncApplyHandlers.d.ts +4 -4
  150. package/dist/sync/syncApplyHandlers.js +89 -31
  151. package/dist/sync/syncApplyHandlers.test.js +184 -4
  152. package/dist/sync/syncResourceAdapters.d.ts +2 -0
  153. package/dist/sync/syncResourceAdapters.js +2 -0
  154. package/dist/sync/syncService.d.ts +1 -0
  155. package/dist/sync/syncService.js +13 -1
  156. package/dist/sync/syncService.test.js +39 -0
  157. package/dist/sync/workspacePullFeed.d.ts +4 -0
  158. package/dist/sync/workspacePullFeed.js +129 -70
  159. package/dist/sync/workspacePullFeed.test.js +49 -0
  160. package/dist/sync/workspaceSyncModel.d.ts +28 -6
  161. package/dist/sync/workspaceSyncModel.js +159 -45
  162. package/dist/sync/workspaceSyncModel.test.js +269 -3
  163. package/dist/types.d.ts +33 -2
  164. package/dist/ui/assets/AgentsModule-2y82_3DO.js +1 -0
  165. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BTLZZCiQ.js +3 -0
  166. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BVKew-mH.css +1 -0
  167. package/dist/ui/assets/ContextAttachmentManager-D2epuNnG.js +3 -0
  168. package/dist/ui/assets/DocumentWorkspace-D4rJKXzd.css +1 -0
  169. package/dist/ui/assets/DocumentWorkspace-sVa-3Do6.js +252 -0
  170. package/dist/ui/assets/EntityActivityTimeline-DyqvvhuF.css +1 -0
  171. package/dist/ui/assets/EntityActivityTimeline-Zods27y_.js +1 -0
  172. package/dist/ui/assets/{InitiativesModule-9E6ParrY.js → InitiativesModule-D4XseTwW.js} +1 -1
  173. package/dist/ui/assets/PlansPage-D5AcbO0L.js +1 -0
  174. package/dist/ui/assets/PlansPage-Dvqsz5zc.css +1 -0
  175. package/dist/ui/assets/TaskContextUpload-uC5qx4Bv.js +1 -0
  176. package/dist/ui/assets/TaskSettings-CqvPAsTv.js +9 -0
  177. package/dist/ui/assets/{WorkflowsModule-BA7jcEpH.js → WorkflowsModule-oOA9bcdK.js} +4 -4
  178. package/dist/ui/assets/documentReferences-DPZuTgHh.js +1 -0
  179. package/dist/ui/assets/index-607WPo_X.js +4 -0
  180. package/dist/ui/assets/index-CYyodXsg.css +1 -0
  181. package/dist/ui/assets/{vendor-icons-DuEd_65c.js → vendor-icons-B32hGuF2.js} +1 -1
  182. package/dist/ui/index.html +3 -3
  183. package/dist/ui/product/agent manager.png +0 -0
  184. package/dist/ui/product/agent_manager_02.png +0 -0
  185. package/dist/utils/aiProfileEvents.d.ts +7 -0
  186. package/dist/utils/aiProfileEvents.js +6 -0
  187. package/dist/utils/assignees.d.ts +6 -0
  188. package/dist/utils/assignees.js +8 -1
  189. package/dist/utils/assignees.test.d.ts +1 -0
  190. package/dist/utils/assignees.test.js +25 -0
  191. package/dist/utils/avatarDisplay.d.ts +1 -0
  192. package/dist/utils/avatarDisplay.js +15 -0
  193. package/dist/utils/contextFiles.d.ts +3 -1
  194. package/dist/utils/contextFiles.js +2 -1
  195. package/dist/utils/taskActivity.d.ts +7 -1
  196. package/dist/utils/taskActivity.js +63 -1
  197. package/dist/utils/taskActivity.test.js +23 -1
  198. package/dist/utils/taskSearch.d.ts +22 -0
  199. package/dist/utils/taskSearch.js +87 -0
  200. package/dist/utils/taskSearch.test.d.ts +1 -0
  201. package/dist/utils/taskSearch.test.js +69 -0
  202. package/dist/utils/taskUtils.js +10 -3
  203. package/dist/utils/taskUtils.test.d.ts +1 -0
  204. package/dist/utils/taskUtils.test.js +19 -0
  205. package/package.json +1 -1
  206. package/dist/ui/assets/AgentsModule-DrLawwNl.js +0 -1
  207. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +0 -1
  208. package/dist/ui/assets/AnnotatedAttachmentWorkspace-Z9_whf7F.js +0 -3
  209. package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +0 -252
  210. package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +0 -1
  211. package/dist/ui/assets/PlansPage-CvfnMiWq.css +0 -1
  212. package/dist/ui/assets/PlansPage-DRXscYxH.js +0 -1
  213. package/dist/ui/assets/TaskSettings-DZX4jk7e.js +0 -9
  214. package/dist/ui/assets/index-9eT8e0Lu.css +0 -1
  215. package/dist/ui/assets/index-C6k75jr8.js +0 -6
@@ -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
@@ -61,9 +63,11 @@ import { resolveRuntimeModeContract } from '../../shared/runtimeContract.js';
61
63
  import { getRouteTimingNow, logSlowBootstrapRoute, requestBootstrapTraceId, requestHeader, resolveErrorStatusCode, serializeJsonWithTiming, setServerTimingHeader } from './shared.js';
62
64
  import { buildAuthFlowBehavior, buildSiteBehavior, getRouteEmailConfig, resolvePublicAuthBaseUrl, resolveRuntimeConfigBaseUrl, shouldExposeDevAuthTokens } from './authSupport.js';
63
65
  import { normalizeTaskDataRelativePath } from '../../storage/objectStorageClient.js';
66
+ import { clearAiProfileAvatarAssets, saveAiProfileAvatarAssets } from '../aiProfileAvatarAssets.js';
67
+ const AI_PROFILE_AVATAR_UPLOAD_MAX_JSON_BYTES = 16 * 1024 * 1024;
64
68
  // === SECTION: Registration ===
65
69
  export function registerAuthRoutes(deps) {
66
- const { addRoute, core, context, requestWorkspaceId, requestUserId, resolveAuthenticatedUserId, ensureAppAccess, ensureAuthenticatedUser, ensureWorkspaceAdminRole, ensureCloudPlanData, requireCloudRuntime, auditAuthAction, resolveAdminRequestActor, resolveEffectiveObjectStorageConfig, getObjectStorageClient, } = deps;
70
+ const { addRoute, core, context, requestWorkspaceId, requestUserId, resolveAuthenticatedUserId, ensureAppAccess, ensureAuthenticatedUser, ensureWorkspaceAdminRole, ensureCloudPlanData, requireCloudRuntime, auditAuthAction, resolveAdminRequestActor, resolveEffectiveObjectStorageConfig, getObjectStorageClient, workspaceAssetStore, ensureWithinDir, markAssetDeleted, appendStorageTelemetry, } = deps;
67
71
  const normalizeTeamWorkspaceRole = (value) => {
68
72
  const role = String(value || '').trim().toLowerCase();
69
73
  if (role === 'owner' || role === 'admin' || role === 'member' || role === 'read-only')
@@ -76,6 +80,56 @@ export function registerAuthRoutes(deps) {
76
80
  const stateAllows = state === 'active' || state === 'trialing' || state === 'grace';
77
81
  return teamAllowed && stateAllows ? 'team' : 'personal';
78
82
  };
83
+ const serializeAiProfilePublicRecord = (profile) => ({
84
+ id: profile.id,
85
+ workspaceId: profile.workspaceId,
86
+ name: profile.name,
87
+ username: profile.username,
88
+ icon: profile.icon,
89
+ color: profile.color,
90
+ avatarUrl: profile.avatarUrl ?? null,
91
+ avatarSourceUrl: profile.avatarSourceUrl ?? null,
92
+ avatarRevision: profile.avatarRevision ?? 0,
93
+ avatarUpdatedAt: profile.avatarUpdatedAt ?? null,
94
+ description: profile.description ?? null,
95
+ role: profile.role ?? null,
96
+ provider: profile.provider ?? null,
97
+ model: profile.model ?? null,
98
+ surfaceType: profile.surfaceType ?? null,
99
+ seatScope: profile.seatScope ?? null,
100
+ archivedAt: profile.archivedAt ?? null,
101
+ archivedBy: profile.archivedBy ?? null,
102
+ archivedReason: profile.archivedReason ?? null,
103
+ mergedIntoProfileId: profile.mergedIntoProfileId ?? null,
104
+ rosterStateUpdatedAt: profile.rosterStateUpdatedAt ?? null,
105
+ createdAt: profile.createdAt,
106
+ updatedAt: profile.updatedAt,
107
+ lastActiveAt: profile.lastActiveAt ?? null
108
+ });
109
+ const parseAiAvatarDataUrlPayload = (value, fallbackOriginalName) => {
110
+ if (!value || typeof value !== 'object')
111
+ return null;
112
+ const record = value;
113
+ const dataUrl = String(record.dataUrl || record.file || '').trim();
114
+ const dataMatch = dataUrl.match(/^data:([^;]+);base64,(.+)$/);
115
+ if (!dataMatch)
116
+ return null;
117
+ return {
118
+ mimeType: String(record.mimeType || dataMatch[1] || 'application/octet-stream').trim().toLowerCase(),
119
+ originalName: typeof record.originalName === 'string' && record.originalName.trim().length > 0
120
+ ? record.originalName.trim()
121
+ : fallbackOriginalName,
122
+ buffer: Buffer.from(dataMatch[2], 'base64')
123
+ };
124
+ };
125
+ const aiProfileAvatarAssetDeps = {
126
+ core,
127
+ workspaceAssetStore,
128
+ getObjectStorageClient,
129
+ ensureWithinDir,
130
+ markAssetDeleted,
131
+ appendStorageTelemetry,
132
+ };
79
133
  const resolveBillingConflictCodeForUser = (userIdRaw, gateRaw) => {
80
134
  const userId = String(userIdRaw || '').trim();
81
135
  const gate = String(gateRaw || '').trim().toLowerCase();
@@ -1062,6 +1116,8 @@ export function registerAuthRoutes(deps) {
1062
1116
  }
1063
1117
  const payload = {
1064
1118
  authenticated,
1119
+ authRequiredForApi: Boolean(authConfig?.enabled && authConfig.requiredForApi),
1120
+ runtimeMode: authConfig?.runtimeMode || 'local',
1065
1121
  userId,
1066
1122
  workspaceId,
1067
1123
  role,
@@ -2189,6 +2245,7 @@ export function registerAuthRoutes(deps) {
2189
2245
  res.end(JSON.stringify({ users }));
2190
2246
  });
2191
2247
  // GET /api/taskforce/workspace/assignee-options - Read active workspace humans and registered AI collaborators for task assignee selection.
2248
+ // Use ?kind=agent for Agent Manager roster loads that do not need human workspace members.
2192
2249
  addRoute('GET', '/api/taskforce/workspace/assignee-options', async (req, res) => {
2193
2250
  if (!ensureAppAccess(req, res))
2194
2251
  return;
@@ -2200,26 +2257,30 @@ export function registerAuthRoutes(deps) {
2200
2257
  const startedAt = getRouteTimingNow();
2201
2258
  const wid = requestWorkspaceId(req);
2202
2259
  const traceId = requestBootstrapTraceId(req);
2203
- const adminRoleStartedAt = getRouteTimingNow();
2204
- const includeProfileToken = context.authConfig?.enabled !== true || ensureWorkspaceAdminRole(req);
2205
- const adminRoleResolvedAt = getRouteTimingNow();
2260
+ const reqUrl = new URL(req.url || '/api/taskforce/workspace/assignee-options', 'http://localhost');
2261
+ const kindFilter = String(reqUrl.searchParams.get('kind') || '').trim().toLowerCase();
2262
+ const agentsOnly = kindFilter === 'agent' || kindFilter === 'agents' || kindFilter === 'ai-profile' || kindFilter === 'ai-profiles';
2206
2263
  const usersStartedAt = getRouteTimingNow();
2207
- const activeWorkspaceUsers = core.listWorkspaceUsers(wid)
2208
- .filter((user) => String(user.status || '').trim().toLowerCase() === 'active' && user.disabled !== true);
2264
+ const activeWorkspaceUsers = agentsOnly
2265
+ ? []
2266
+ : core.listWorkspaceUsers(wid)
2267
+ .filter((user) => String(user.status || '').trim().toLowerCase() === 'active' && user.disabled !== true);
2209
2268
  const usersLoadedAt = getRouteTimingNow();
2210
- const workspaceUsers = activeWorkspaceUsers.map((user) => ({
2211
- value: user.userId,
2212
- label: String(user.displayName || user.email || user.userId).trim() || user.userId,
2213
- icon: 'User',
2214
- color: '#22c55e',
2215
- kind: 'member',
2216
- userId: user.userId,
2217
- email: user.email,
2218
- displayName: user.displayName ?? null,
2219
- avatarUrl: user.avatarUrl ?? null,
2220
- status: user.status,
2221
- disabled: user.disabled === true
2222
- }));
2269
+ const workspaceUsers = agentsOnly
2270
+ ? []
2271
+ : activeWorkspaceUsers.map((user) => ({
2272
+ value: user.userId,
2273
+ label: String(user.displayName || user.email || user.userId).trim() || user.userId,
2274
+ icon: 'User',
2275
+ color: '#22c55e',
2276
+ kind: 'member',
2277
+ userId: user.userId,
2278
+ email: user.email,
2279
+ displayName: user.displayName ?? null,
2280
+ avatarUrl: user.avatarUrl ?? null,
2281
+ status: user.status,
2282
+ disabled: user.disabled === true
2283
+ }));
2223
2284
  const usersMappedAt = getRouteTimingNow();
2224
2285
  const aiProfileDiagnostics = { metadataBackfillCount: 0 };
2225
2286
  const aiProfilesStartedAt = getRouteTimingNow();
@@ -2228,12 +2289,13 @@ export function registerAuthRoutes(deps) {
2228
2289
  .map((profile) => ({
2229
2290
  value: profile.id,
2230
2291
  label: profile.name,
2231
- ...(includeProfileToken ? { profileToken: profile.profileToken } : {}),
2232
2292
  username: profile.username,
2233
2293
  icon: profile.icon,
2234
2294
  color: profile.color,
2235
2295
  avatarUrl: profile.avatarUrl ?? null,
2236
2296
  avatarSourceUrl: profile.avatarSourceUrl ?? null,
2297
+ avatarRevision: profile.avatarRevision ?? 0,
2298
+ avatarUpdatedAt: profile.avatarUpdatedAt ?? null,
2237
2299
  kind: 'agent',
2238
2300
  description: profile.description ?? null,
2239
2301
  role: profile.role ?? null,
@@ -2256,7 +2318,6 @@ export function registerAuthRoutes(deps) {
2256
2318
  const totalDurationMs = Math.max(0, getRouteTimingNow() - startedAt);
2257
2319
  setServerTimingHeader(res, [
2258
2320
  { name: 'assignee-options', durationMs: totalDurationMs },
2259
- { name: 'assignee-options-admin-role', durationMs: adminRoleResolvedAt - adminRoleStartedAt },
2260
2321
  { name: 'assignee-options-users', durationMs: usersLoadedAt - usersStartedAt },
2261
2322
  { name: 'assignee-options-users-map', durationMs: usersMappedAt - usersLoadedAt },
2262
2323
  { name: 'assignee-options-ai-profiles', durationMs: aiProfilesLoadedAt - aiProfilesStartedAt },
@@ -2272,9 +2333,9 @@ export function registerAuthRoutes(deps) {
2272
2333
  humanAssigneeCount: workspaceUsers.length,
2273
2334
  workspaceUserCount: activeWorkspaceUsers.length,
2274
2335
  aiProfileCount: aiProfiles.length,
2336
+ agentsOnly,
2275
2337
  metadataBackfillCount: aiProfileDiagnostics.metadataBackfillCount,
2276
2338
  responseBytes: serialized.byteLength,
2277
- adminRoleMs: Math.max(0, adminRoleResolvedAt - adminRoleStartedAt),
2278
2339
  listWorkspaceUsersMs: Math.max(0, usersLoadedAt - usersStartedAt),
2279
2340
  workspaceUsersMapMs: Math.max(0, usersMappedAt - usersLoadedAt),
2280
2341
  listAiProfilesMs: Math.max(0, aiProfilesLoadedAt - aiProfilesStartedAt),
@@ -2285,7 +2346,7 @@ export function registerAuthRoutes(deps) {
2285
2346
  res.writeHead(200, { 'Content-Type': 'application/json' });
2286
2347
  res.end(serialized.body);
2287
2348
  });
2288
- // POST /api/taskforce/workspace/ai-profiles/avatar - Assign or clear an AI profile avatar image.
2349
+ // POST /api/taskforce/workspace/ai-profiles/avatar - Clear an AI profile avatar image.
2289
2350
  addRoute('POST', '/api/taskforce/workspace/ai-profiles/avatar', async (req, res) => {
2290
2351
  if (!ensureWorkspaceAdminRole(req)) {
2291
2352
  res.writeHead(403, { 'Content-Type': 'application/json' });
@@ -2307,9 +2368,19 @@ export function registerAuthRoutes(deps) {
2307
2368
  return;
2308
2369
  }
2309
2370
  try {
2310
- const profile = core.updateAiProfileAvatar({ workspaceId: wid, profileId, avatarUrl, avatarSourceUrl });
2371
+ const shouldClearAvatar = avatarUrl === null && (avatarSourceUrl === null || avatarSourceUrl === undefined);
2372
+ if (!shouldClearAvatar) {
2373
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2374
+ res.end(JSON.stringify({
2375
+ error: 'AI profile avatar updates must use the stable avatar upload endpoint.',
2376
+ code: 'AI_PROFILE_AVATAR_STABLE_UPLOAD_REQUIRED'
2377
+ }));
2378
+ return;
2379
+ }
2380
+ const result = await clearAiProfileAvatarAssets(aiProfileAvatarAssetDeps, { workspaceId: wid, profileId });
2381
+ const profile = result.profile;
2311
2382
  res.writeHead(200, { 'Content-Type': 'application/json' });
2312
- res.end(JSON.stringify({ success: true, profile }));
2383
+ res.end(JSON.stringify({ success: true, profile: serializeAiProfilePublicRecord(profile) }));
2313
2384
  }
2314
2385
  catch (err) {
2315
2386
  const status = err?.statusCode ?? 500;
@@ -2317,6 +2388,58 @@ export function registerAuthRoutes(deps) {
2317
2388
  res.end(JSON.stringify({ error: err?.message ?? 'Failed to update AI profile avatar.' }));
2318
2389
  }
2319
2390
  });
2391
+ // POST /api/taskforce/workspace/ai-profiles/avatar/upload - Upload stable AI profile avatar slots.
2392
+ addRoute('POST', '/api/taskforce/workspace/ai-profiles/avatar/upload', async (req, res) => {
2393
+ if (!ensureWorkspaceAdminRole(req)) {
2394
+ res.writeHead(403, { 'Content-Type': 'application/json' });
2395
+ res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
2396
+ return;
2397
+ }
2398
+ const body = await parseJsonBody(req, AI_PROFILE_AVATAR_UPLOAD_MAX_JSON_BYTES);
2399
+ const wid = requestWorkspaceId(req);
2400
+ const profileId = String(body?.profileId || '').trim();
2401
+ if (!profileId) {
2402
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2403
+ res.end(JSON.stringify({ error: 'profileId is required.' }));
2404
+ return;
2405
+ }
2406
+ const display = parseAiAvatarDataUrlPayload(body?.displayImage || body?.display, 'display-avatar');
2407
+ if (!display) {
2408
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2409
+ res.end(JSON.stringify({ error: 'displayImage data URL is required.' }));
2410
+ return;
2411
+ }
2412
+ const source = parseAiAvatarDataUrlPayload(body?.sourceImage || body?.source, 'source-avatar');
2413
+ try {
2414
+ const result = await saveAiProfileAvatarAssets(aiProfileAvatarAssetDeps, {
2415
+ workspaceId: wid,
2416
+ profileId,
2417
+ display,
2418
+ source,
2419
+ actorId: resolveAuthenticatedUserId(req)
2420
+ });
2421
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2422
+ res.end(JSON.stringify({
2423
+ success: true,
2424
+ profile: serializeAiProfilePublicRecord(result.profile),
2425
+ assets: {
2426
+ display: {
2427
+ assetId: result.displayAsset.assetId,
2428
+ storageKey: result.displayStorageKey,
2429
+ },
2430
+ source: {
2431
+ assetId: result.sourceAsset.assetId,
2432
+ storageKey: result.sourceStorageKey,
2433
+ }
2434
+ }
2435
+ }));
2436
+ }
2437
+ catch (err) {
2438
+ const status = err?.statusCode ?? 500;
2439
+ res.writeHead(status, { 'Content-Type': 'application/json' });
2440
+ res.end(JSON.stringify({ error: err?.message ?? 'Failed to upload AI profile avatar.' }));
2441
+ }
2442
+ });
2320
2443
  // POST /api/taskforce/workspace/ai-profiles/merge - Merge a duplicate AI profile into another, reassigning all references.
2321
2444
  addRoute('POST', '/api/taskforce/workspace/ai-profiles/merge', async (req, res) => {
2322
2445
  if (!ensureWorkspaceAdminRole(req)) {
@@ -2336,7 +2459,7 @@ export function registerAuthRoutes(deps) {
2336
2459
  try {
2337
2460
  const profile = core.mergeAiProfiles({ workspaceId: wid, keepId, mergeId });
2338
2461
  res.writeHead(200, { 'Content-Type': 'application/json' });
2339
- res.end(JSON.stringify({ profile }));
2462
+ res.end(JSON.stringify({ profile: serializeAiProfilePublicRecord(profile) }));
2340
2463
  }
2341
2464
  catch (err) {
2342
2465
  const status = err?.statusCode ?? 500;
@@ -2344,6 +2467,45 @@ export function registerAuthRoutes(deps) {
2344
2467
  res.end(JSON.stringify({ error: err?.message ?? 'Merge failed.' }));
2345
2468
  }
2346
2469
  });
2470
+ // POST /api/taskforce/workspace/ai-profiles/surface-type - Manually re-categorize an active AI profile.
2471
+ addRoute('POST', '/api/taskforce/workspace/ai-profiles/surface-type', async (req, res) => {
2472
+ if (!ensureWorkspaceAdminRole(req)) {
2473
+ res.writeHead(403, { 'Content-Type': 'application/json' });
2474
+ res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
2475
+ return;
2476
+ }
2477
+ const body = await parseJsonBody(req);
2478
+ const wid = requestWorkspaceId(req);
2479
+ const profileId = String(body.profileId || '').trim();
2480
+ const hasSurfaceType = Boolean(body && Object.prototype.hasOwnProperty.call(body, 'surfaceType'));
2481
+ if (!profileId) {
2482
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2483
+ res.end(JSON.stringify({ error: 'profileId is required.' }));
2484
+ return;
2485
+ }
2486
+ if (!hasSurfaceType) {
2487
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2488
+ res.end(JSON.stringify({ error: 'surfaceType is required.' }));
2489
+ return;
2490
+ }
2491
+ try {
2492
+ const profile = core.updateAiProfileSurfaceType({
2493
+ workspaceId: wid,
2494
+ profileId,
2495
+ surfaceType: body.surfaceType
2496
+ });
2497
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2498
+ res.end(JSON.stringify({ success: true, profile: serializeAiProfilePublicRecord(profile) }));
2499
+ }
2500
+ catch (err) {
2501
+ const status = err?.statusCode ?? 500;
2502
+ res.writeHead(status, { 'Content-Type': 'application/json' });
2503
+ res.end(JSON.stringify({
2504
+ error: err?.message ?? 'Failed to update AI profile category.',
2505
+ code: err?.code
2506
+ }));
2507
+ }
2508
+ });
2347
2509
  // POST /api/taskforce/workspace/ai-profiles/archive - Remove an AI profile from the active roster without deleting its history.
2348
2510
  addRoute('POST', '/api/taskforce/workspace/ai-profiles/archive', async (req, res) => {
2349
2511
  if (!ensureWorkspaceAdminRole(req)) {
@@ -2368,7 +2530,7 @@ export function registerAuthRoutes(deps) {
2368
2530
  reason
2369
2531
  });
2370
2532
  res.writeHead(200, { 'Content-Type': 'application/json' });
2371
- res.end(JSON.stringify({ profile }));
2533
+ res.end(JSON.stringify({ profile: serializeAiProfilePublicRecord(profile) }));
2372
2534
  }
2373
2535
  catch (err) {
2374
2536
  const status = err?.statusCode ?? 500;
@@ -24,6 +24,10 @@ export declare function buildAuthBehavior(core: TaskforceCore): {
24
24
  export declare function buildSiteBehavior(core: TaskforceCore): {
25
25
  showRunTaskforceLocally: boolean;
26
26
  pricingPageEnabled: boolean;
27
+ animatedBackgroundEnabled: boolean;
28
+ siteTheme: string;
29
+ pricingPageTitle: string;
30
+ pricingPageDescription: string;
27
31
  };
28
32
  export declare function shouldExposeDevAuthTokens(): boolean;
29
33
  export declare function resolveRuntimeConfigBaseUrl(req: IncomingMessage): string;
@@ -51,9 +51,20 @@ export function buildAuthBehavior(core) {
51
51
  export function buildSiteBehavior(core) {
52
52
  const globalSettings = core.getGlobalSettings();
53
53
  const siteSettings = globalSettings.site || {};
54
+ const pricingPageTitle = typeof siteSettings.pricingPageTitle === 'string'
55
+ ? (siteSettings.pricingPageTitle.trim() || 'Subscription Plans')
56
+ : 'Subscription Plans';
57
+ const pricingPageDescription = typeof siteSettings.pricingPageDescription === 'string'
58
+ ? (siteSettings.pricingPageDescription.trim() || 'Manage your subscription and explore available features. Your current plan is highlighted below.')
59
+ : 'Manage your subscription and explore available features. Your current plan is highlighted below.';
60
+ const siteTheme = siteSettings.siteTheme === 'light' ? 'light' : 'dark';
54
61
  return {
55
62
  showRunTaskforceLocally: resolveBooleanSetting(siteSettings.showRunTaskforceLocally, true),
56
- pricingPageEnabled: resolveBooleanSetting(siteSettings.pricingPageEnabled, true)
63
+ pricingPageEnabled: resolveBooleanSetting(siteSettings.pricingPageEnabled, true),
64
+ animatedBackgroundEnabled: resolveBooleanSetting(siteSettings.animatedBackgroundEnabled, false),
65
+ siteTheme,
66
+ pricingPageTitle,
67
+ pricingPageDescription
57
68
  };
58
69
  }
59
70
  export function shouldExposeDevAuthTokens() {
@@ -2553,6 +2553,15 @@ export function registerBillingRoutes(deps) {
2553
2553
  const db = core.getDatabaseAdapter();
2554
2554
  const tenantId = core.getTenantId();
2555
2555
  const environment = requestPricingEnvironment(req);
2556
+ const siteSettings = typeof core.getGlobalSettings === 'function'
2557
+ ? (core.getGlobalSettings().site || {})
2558
+ : {};
2559
+ const pageTitle = typeof siteSettings.pricingPageTitle === 'string'
2560
+ ? (siteSettings.pricingPageTitle.trim() || 'Subscription Plans')
2561
+ : 'Subscription Plans';
2562
+ const pageDescription = typeof siteSettings.pricingPageDescription === 'string'
2563
+ ? (siteSettings.pricingPageDescription.trim() || 'Manage your subscription and explore available features. Your current plan is highlighted below.')
2564
+ : 'Manage your subscription and explore available features. Your current plan is highlighted below.';
2556
2565
  const cacheKey = `${tenantId}:${environment}`;
2557
2566
  const cacheVersion = buildPublicPricingCacheVersion(core);
2558
2567
  const cached = publicPricingCache.get(cacheKey);
@@ -2664,6 +2673,8 @@ export function registerBillingRoutes(deps) {
2664
2673
  }).filter(Boolean);
2665
2674
  const payload = {
2666
2675
  asOf: new Date().toISOString(),
2676
+ pageTitle,
2677
+ pageDescription,
2667
2678
  pricingSource: {
2668
2679
  environment,
2669
2680
  runtimeMode: 'cloud'
@@ -2733,6 +2733,8 @@ describe('Billing Webhook Routes', () => {
2733
2733
  expect(res.statusCode).toBe(200);
2734
2734
  const body = JSON.parse(res.body);
2735
2735
  expect(body.plans).toBeInstanceOf(Array);
2736
+ expect(body.pageTitle).toBe('Subscription Plans');
2737
+ expect(body.pageDescription).toBe('Manage your subscription and explore available features. Your current plan is highlighted below.');
2736
2738
  expect(body.pricingSource).toEqual({
2737
2739
  environment: 'production',
2738
2740
  runtimeMode: 'cloud'
@@ -2949,6 +2951,7 @@ describe('Billing Webhook Routes', () => {
2949
2951
  await pricingRoute(req, res, {});
2950
2952
  expect(res.statusCode).toBe(200);
2951
2953
  const body = JSON.parse(res.body);
2954
+ expect(body.pageTitle).toBe('Subscription Plans');
2952
2955
  expect(body.pricingSource).toEqual({
2953
2956
  environment: 'performance',
2954
2957
  runtimeMode: 'cloud'
@@ -474,6 +474,14 @@ export function registerDocumentRoutes(deps) {
474
474
  if (shouldProxyInlineUserAvatar && await proxyCloudUserProfileAvatar(req, res, normalizedRelative, contentTypeForExt)) {
475
475
  return;
476
476
  }
477
+ if (shouldProxyInlineAiAvatar && filePath && fs.existsSync(filePath)) {
478
+ res.writeHead(200, {
479
+ 'Content-Type': contentTypeForExt(ext),
480
+ 'Content-Disposition': contentDisposition
481
+ });
482
+ res.end(fs.readFileSync(filePath));
483
+ return;
484
+ }
477
485
  res.writeHead(404, { 'Content-Type': 'text/plain' });
478
486
  res.end('Context file not found');
479
487
  return;
@@ -873,6 +881,7 @@ export function registerDocumentRoutes(deps) {
873
881
  }));
874
882
  return;
875
883
  }
884
+ const attachedAt = new Date().toISOString();
876
885
  const payload = buildTaskAttachmentPayload(asset.storageKey, {
877
886
  caption: getCanonicalDocumentName({
878
887
  assetId: asset.assetId,
@@ -883,6 +892,7 @@ export function registerDocumentRoutes(deps) {
883
892
  }),
884
893
  assetId: asset.assetId,
885
894
  taskId: targetId,
895
+ timestamp: attachedAt,
886
896
  });
887
897
  if (!payload) {
888
898
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -216,16 +216,6 @@ export function makeResolveAuthenticatedMutationUserId(core, context, requestWor
216
216
  const explicitUserId = String(requestUserId(req) || '').trim();
217
217
  if (explicitUserId && explicitUserId !== 'anonymous')
218
218
  return explicitUserId;
219
- if (requestRuntimeMode(req) === 'local') {
220
- const workspaceId = String(requestWorkspaceId(req) || '').trim() || 'default';
221
- const fallbackUser = core.listWorkspaceUsers(workspaceId).find((user) => {
222
- const status = String(user.status || '').trim().toLowerCase();
223
- return status === 'active' && user.disabled !== true;
224
- });
225
- const fallbackUserId = String(fallbackUser?.userId || '').trim();
226
- if (fallbackUserId)
227
- return fallbackUserId;
228
- }
229
219
  throw new TaskforceRuleError('Authenticated user required for mutation attribution.', 401, 'MUTATION_AUTH_REQUIRED');
230
220
  };
231
221
  }