@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
@@ -6,7 +6,7 @@ export type TaskAttachmentRecord = {
6
6
  displayName?: string;
7
7
  originalFilename?: string;
8
8
  timestamp?: string;
9
- assetId?: string;
9
+ assetId?: string | null;
10
10
  referenceNumber?: number | null;
11
11
  referenceLabel?: string | null;
12
12
  taskId?: string | null;
@@ -87,6 +87,18 @@ export function registerTaskPlanningTools(registerTool, executeTool) {
87
87
  description: stringProperty('Optional initiative description.'),
88
88
  ownerId: stringProperty(`${context.assigneeHint} Call list_assignees to inspect the current eligible IDs.`),
89
89
  order: numberProperty('Optional sort order among initiatives.'),
90
+ attachments: arrayProperty('Optional attachment descriptors to store on the initiative. Prefer save_initiative_attachment for generated content or files.', {
91
+ type: 'object',
92
+ properties: {
93
+ path: { type: 'string' },
94
+ fsPath: { type: 'string' },
95
+ caption: { type: 'string' },
96
+ displayName: { type: 'string' },
97
+ originalFilename: { type: 'string' },
98
+ assetId: { type: 'string' },
99
+ },
100
+ required: ['path'],
101
+ }),
90
102
  },
91
103
  required: ['title'],
92
104
  },
@@ -100,6 +112,18 @@ export function registerTaskPlanningTools(registerTool, executeTool) {
100
112
  description: stringProperty('Updated initiative description.'),
101
113
  ownerId: stringProperty(`${context.assigneeHint} Call list_assignees to inspect the current eligible IDs.`),
102
114
  order: numberProperty('Optional sort order among initiatives.'),
115
+ attachments: arrayProperty('Replacement attachment descriptors for the initiative. Prefer save_initiative_attachment and delete_initiative_attachment for generated content or files.', {
116
+ type: 'object',
117
+ properties: {
118
+ path: { type: 'string' },
119
+ fsPath: { type: 'string' },
120
+ caption: { type: 'string' },
121
+ displayName: { type: 'string' },
122
+ originalFilename: { type: 'string' },
123
+ assetId: { type: 'string' },
124
+ },
125
+ required: ['path'],
126
+ }),
103
127
  },
104
128
  required: ['id'],
105
129
  },
@@ -144,6 +168,18 @@ export function registerTaskPlanningTools(registerTool, executeTool) {
144
168
  ownerId: stringProperty(`${context.assigneeHint} Call list_assignees to inspect the current eligible IDs.`),
145
169
  initiativeId: stringProperty('Optional initiative ID or initiative reference such as IN-123.'),
146
170
  order: numberProperty('Optional sort order among workstreams.'),
171
+ attachments: arrayProperty('Optional attachment descriptors to store on the workstream. Prefer save_workstream_attachment for generated content or files.', {
172
+ type: 'object',
173
+ properties: {
174
+ path: { type: 'string' },
175
+ fsPath: { type: 'string' },
176
+ caption: { type: 'string' },
177
+ displayName: { type: 'string' },
178
+ originalFilename: { type: 'string' },
179
+ assetId: { type: 'string' },
180
+ },
181
+ required: ['path'],
182
+ }),
147
183
  },
148
184
  required: ['title'],
149
185
  },
@@ -158,6 +194,18 @@ export function registerTaskPlanningTools(registerTool, executeTool) {
158
194
  ownerId: stringProperty(`${context.assigneeHint} Call list_assignees to inspect the current eligible IDs.`),
159
195
  initiativeId: stringProperty('Optional initiative ID or initiative reference such as IN-123. Pass an empty string to unlink.'),
160
196
  order: numberProperty('Optional sort order among workstreams.'),
197
+ attachments: arrayProperty('Replacement attachment descriptors for the workstream. Prefer save_workstream_attachment and delete_workstream_attachment for generated content or files.', {
198
+ type: 'object',
199
+ properties: {
200
+ path: { type: 'string' },
201
+ fsPath: { type: 'string' },
202
+ caption: { type: 'string' },
203
+ displayName: { type: 'string' },
204
+ originalFilename: { type: 'string' },
205
+ assetId: { type: 'string' },
206
+ },
207
+ required: ['path'],
208
+ }),
161
209
  },
162
210
  required: ['id'],
163
211
  },
@@ -185,6 +185,62 @@ export declare const MCP_TOOL_REGISTRY: Readonly<{
185
185
  };
186
186
  readonly mutating: false;
187
187
  };
188
+ readonly get_workstream_attachments: {
189
+ readonly permission: {
190
+ readonly kind: "scope";
191
+ readonly scope: "tasks:read";
192
+ };
193
+ readonly mutating: false;
194
+ };
195
+ readonly read_workstream_attachment: {
196
+ readonly permission: {
197
+ readonly kind: "scope";
198
+ readonly scope: "tasks:read";
199
+ };
200
+ readonly mutating: false;
201
+ };
202
+ readonly save_workstream_attachment: {
203
+ readonly permission: {
204
+ readonly kind: "scope";
205
+ readonly scope: "tasks:write";
206
+ };
207
+ readonly mutating: true;
208
+ };
209
+ readonly delete_workstream_attachment: {
210
+ readonly permission: {
211
+ readonly kind: "scope";
212
+ readonly scope: "tasks:write";
213
+ };
214
+ readonly mutating: true;
215
+ };
216
+ readonly get_initiative_attachments: {
217
+ readonly permission: {
218
+ readonly kind: "scope";
219
+ readonly scope: "tasks:read";
220
+ };
221
+ readonly mutating: false;
222
+ };
223
+ readonly read_initiative_attachment: {
224
+ readonly permission: {
225
+ readonly kind: "scope";
226
+ readonly scope: "tasks:read";
227
+ };
228
+ readonly mutating: false;
229
+ };
230
+ readonly save_initiative_attachment: {
231
+ readonly permission: {
232
+ readonly kind: "scope";
233
+ readonly scope: "tasks:write";
234
+ };
235
+ readonly mutating: true;
236
+ };
237
+ readonly delete_initiative_attachment: {
238
+ readonly permission: {
239
+ readonly kind: "scope";
240
+ readonly scope: "tasks:write";
241
+ };
242
+ readonly mutating: true;
243
+ };
188
244
  readonly search_images: {
189
245
  readonly permission: {
190
246
  readonly kind: "scope";
@@ -26,6 +26,14 @@ export const MCP_TOOL_REGISTRY = Object.freeze({
26
26
  replace_task_checklist: { permission: { kind: 'scope', scope: 'tasks:write' }, mutating: true },
27
27
  get_task_attachments: { permission: { kind: 'scope', scope: 'tasks:read' }, mutating: false },
28
28
  read_task_attachment: { permission: { kind: 'scope', scope: 'tasks:read' }, mutating: false },
29
+ get_workstream_attachments: { permission: { kind: 'scope', scope: 'tasks:read' }, mutating: false },
30
+ read_workstream_attachment: { permission: { kind: 'scope', scope: 'tasks:read' }, mutating: false },
31
+ save_workstream_attachment: { permission: { kind: 'scope', scope: 'tasks:write' }, mutating: true },
32
+ delete_workstream_attachment: { permission: { kind: 'scope', scope: 'tasks:write' }, mutating: true },
33
+ get_initiative_attachments: { permission: { kind: 'scope', scope: 'tasks:read' }, mutating: false },
34
+ read_initiative_attachment: { permission: { kind: 'scope', scope: 'tasks:read' }, mutating: false },
35
+ save_initiative_attachment: { permission: { kind: 'scope', scope: 'tasks:write' }, mutating: true },
36
+ delete_initiative_attachment: { permission: { kind: 'scope', scope: 'tasks:write' }, mutating: true },
29
37
  search_images: { permission: { kind: 'scope', scope: 'tasks:read' }, mutating: false },
30
38
  get_image: { permission: { kind: 'scope', scope: 'tasks:read' }, mutating: false },
31
39
  search_documents: { permission: { kind: 'scope', scope: 'tasks:read' }, mutating: false },
@@ -11,6 +11,8 @@ describe('MCP tool registry', () => {
11
11
  expect(MCP_CLOUD_TOOL_SCOPE_REQUIREMENTS.search_documents).toBe('tasks:read');
12
12
  expect(MCP_CLOUD_TOOL_SCOPE_REQUIREMENTS.read_task_attachment).toBe('tasks:read');
13
13
  expect(MCP_CLOUD_TOOL_SCOPE_REQUIREMENTS.get_task_attachments).toBe('tasks:read');
14
+ expect(MCP_CLOUD_TOOL_SCOPE_REQUIREMENTS.read_workstream_attachment).toBe('tasks:read');
15
+ expect(MCP_CLOUD_TOOL_SCOPE_REQUIREMENTS.get_initiative_attachments).toBe('tasks:read');
14
16
  expect(MCP_CLOUD_TOOL_SCOPE_REQUIREMENTS.export_resources).toBeUndefined();
15
17
  });
16
18
  it('derives mutating tool names from the shared registry', () => {
@@ -20,6 +22,8 @@ describe('MCP tool registry', () => {
20
22
  expect(MCP_WRITE_TOOL_NAMES).toEqual(expected);
21
23
  expect(MCP_WRITE_TOOL_NAMES.has('read_task_attachment')).toBe(false);
22
24
  expect(MCP_WRITE_TOOL_NAMES.has('save_task_attachment')).toBe(true);
25
+ expect(MCP_WRITE_TOOL_NAMES.has('save_workstream_attachment')).toBe(true);
26
+ expect(MCP_WRITE_TOOL_NAMES.has('delete_initiative_attachment')).toBe(true);
23
27
  expect(MCP_WRITE_TOOL_NAMES.has('finish_task')).toBe(true);
24
28
  expect(MCP_WRITE_TOOL_NAMES.has('review_document')).toBe(true);
25
29
  });
@@ -198,6 +198,25 @@ export function ensureWorkspaceSchema(db) {
198
198
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
199
199
  );
200
200
 
201
+ CREATE TABLE IF NOT EXISTS task_attachment_upload_drafts (
202
+ draft_id TEXT PRIMARY KEY,
203
+ tenant_id TEXT NOT NULL DEFAULT 'default',
204
+ workspace_id TEXT NOT NULL,
205
+ task_id TEXT NOT NULL,
206
+ actor_ref TEXT NOT NULL,
207
+ storage_key TEXT NOT NULL,
208
+ mime_type TEXT NOT NULL,
209
+ original_filename TEXT NOT NULL,
210
+ size_bytes INTEGER NOT NULL DEFAULT 0,
211
+ storage_provider TEXT NOT NULL DEFAULT 'local',
212
+ consumed_at TEXT,
213
+ discarded_at TEXT,
214
+ created_at TEXT NOT NULL,
215
+ updated_at TEXT NOT NULL,
216
+ FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE,
217
+ FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE
218
+ );
219
+
201
220
  CREATE TABLE IF NOT EXISTS auth_tokens (
202
221
  id TEXT PRIMARY KEY,
203
222
  tenant_id TEXT NOT NULL DEFAULT 'default',
@@ -388,6 +407,8 @@ export function ensureWorkspaceSchema(db) {
388
407
  CREATE INDEX IF NOT EXISTS idx_auth_tokens_user_purpose ON auth_tokens(user_id, purpose);
389
408
  CREATE INDEX IF NOT EXISTS idx_auth_tokens_workspace_purpose ON auth_tokens(workspace_id, purpose);
390
409
  CREATE INDEX IF NOT EXISTS idx_auth_tokens_expires ON auth_tokens(expires_at);
410
+ CREATE INDEX IF NOT EXISTS idx_task_attachment_upload_drafts_workspace_task ON task_attachment_upload_drafts(workspace_id, task_id);
411
+ CREATE INDEX IF NOT EXISTS idx_task_attachment_upload_drafts_actor_created ON task_attachment_upload_drafts(actor_ref, created_at);
391
412
  CREATE UNIQUE INDEX IF NOT EXISTS idx_mcp_access_tokens_prefix ON mcp_access_tokens(token_prefix);
392
413
  CREATE INDEX IF NOT EXISTS idx_mcp_access_tokens_workspace_user ON mcp_access_tokens(workspace_id, user_id);
393
414
  CREATE INDEX IF NOT EXISTS idx_mcp_access_tokens_status ON mcp_access_tokens(status);
@@ -0,0 +1,49 @@
1
+ import type { TaskforceCore } from '../core/Taskforce.js';
2
+ import type { ObjectStorageClient } from '../storage/objectStorageClient.js';
3
+ import type { WorkspaceAssetStore } from '../storage/workspaceAssetStore.js';
4
+ type AiAvatarSlot = 'display' | 'source';
5
+ export interface AiProfileAvatarPayload {
6
+ buffer: Buffer;
7
+ mimeType: string;
8
+ originalName?: string | null;
9
+ }
10
+ export interface AiProfileAvatarAssetDeps {
11
+ core: TaskforceCore;
12
+ workspaceAssetStore: WorkspaceAssetStore | null;
13
+ getObjectStorageClient: () => ObjectStorageClient | null;
14
+ ensureWithinDir: (candidatePath: string, rootDir: string) => string | null;
15
+ markAssetDeleted: (pathValue: string) => unknown;
16
+ appendStorageTelemetry: (event: string, payload?: {
17
+ path?: string;
18
+ status?: string;
19
+ provider?: string;
20
+ }) => void;
21
+ }
22
+ export interface SaveAiProfileAvatarInput {
23
+ workspaceId: string;
24
+ profileId: string;
25
+ display: AiProfileAvatarPayload;
26
+ source?: AiProfileAvatarPayload | null;
27
+ actorId?: string | null;
28
+ }
29
+ export declare function buildAiProfileAvatarStorageKey(input: {
30
+ workspaceId: string;
31
+ profileId: string;
32
+ slot: AiAvatarSlot;
33
+ mimeType: string;
34
+ }): string;
35
+ export declare function storageKeyFromContextUrl(url: unknown): string | null;
36
+ export declare function saveAiProfileAvatarAssets(deps: AiProfileAvatarAssetDeps, input: SaveAiProfileAvatarInput): Promise<{
37
+ profile: import("../core/Taskforce.js").AiProfileRecord;
38
+ displayAsset: import("../storage/workspaceAssetStore.js").WorkspaceAssetEntry;
39
+ sourceAsset: import("../storage/workspaceAssetStore.js").WorkspaceAssetEntry;
40
+ displayStorageKey: string;
41
+ sourceStorageKey: string;
42
+ }>;
43
+ export declare function clearAiProfileAvatarAssets(deps: AiProfileAvatarAssetDeps, input: {
44
+ workspaceId: string;
45
+ profileId: string;
46
+ }): Promise<{
47
+ profile: import("../core/Taskforce.js").AiProfileRecord;
48
+ }>;
49
+ export {};
@@ -0,0 +1,319 @@
1
+ import { createHash } from 'crypto';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { normalizeTaskDataRelativePath } from '../storage/objectStorageClient.js';
5
+ const SUPPORTED_IMAGE_MIME_TYPES = new Set([
6
+ 'image/gif',
7
+ 'image/jpeg',
8
+ 'image/png',
9
+ 'image/webp',
10
+ ]);
11
+ const AVATAR_MAX_BYTES = 5 * 1024 * 1024;
12
+ function normalizeId(value) {
13
+ return String(value || '').trim();
14
+ }
15
+ function sanitizeStorageSegment(value, fallback) {
16
+ const normalized = normalizeId(value)
17
+ .replace(/[^a-zA-Z0-9._-]/g, '-')
18
+ .replace(/-+/g, '-')
19
+ .replace(/^-+|-+$/g, '');
20
+ return normalized || fallback;
21
+ }
22
+ function normalizeImageMimeType(value) {
23
+ return normalizeId(value).toLowerCase();
24
+ }
25
+ function extensionForMimeType(mimeType) {
26
+ if (mimeType === 'image/gif')
27
+ return 'gif';
28
+ if (mimeType === 'image/jpeg')
29
+ return 'jpg';
30
+ if (mimeType === 'image/png')
31
+ return 'png';
32
+ return 'webp';
33
+ }
34
+ function stableAssetId(workspaceId, profileId, slot) {
35
+ const digest = createHash('sha256')
36
+ .update(`${workspaceId}:${profileId}:${slot}`)
37
+ .digest('hex')
38
+ .slice(0, 32);
39
+ return `asset-${digest}`;
40
+ }
41
+ export function buildAiProfileAvatarStorageKey(input) {
42
+ const workspaceId = sanitizeStorageSegment(input.workspaceId, 'default');
43
+ const profileId = sanitizeStorageSegment(input.profileId, 'ai-profile');
44
+ const ext = extensionForMimeType(input.mimeType);
45
+ return `workspaces/${workspaceId}/assets/images/ai-profiles/${profileId}/avatar/${input.slot}.${ext}`;
46
+ }
47
+ function contextUrlForStorageKey(storageKey) {
48
+ return `/api/taskforce/context/${encodeURIComponent(storageKey)}`;
49
+ }
50
+ export function storageKeyFromContextUrl(url) {
51
+ const raw = normalizeId(url);
52
+ if (!raw)
53
+ return null;
54
+ const contextPrefix = '/api/taskforce/context/';
55
+ if (!raw.startsWith(contextPrefix))
56
+ return null;
57
+ const encodedPath = raw.slice(contextPrefix.length).split('?')[0] || '';
58
+ try {
59
+ return normalizeTaskDataRelativePath(decodeURIComponent(encodedPath));
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ }
65
+ function validatePayload(payload, label) {
66
+ const mimeType = normalizeImageMimeType(payload.mimeType);
67
+ if (!SUPPORTED_IMAGE_MIME_TYPES.has(mimeType)) {
68
+ throw new Error(`${label} must be a PNG, JPEG, WebP, or GIF image.`);
69
+ }
70
+ if (!Buffer.isBuffer(payload.buffer) || payload.buffer.length === 0) {
71
+ throw new Error(`${label} is empty.`);
72
+ }
73
+ if (payload.buffer.length > AVATAR_MAX_BYTES) {
74
+ throw new Error(`${label} must be 5 MB or smaller.`);
75
+ }
76
+ return mimeType;
77
+ }
78
+ function storageKeyBelongsToAiProfileAvatar(workspaceId, profileId, storageKey) {
79
+ const workspaceIdSafe = sanitizeStorageSegment(workspaceId, 'default');
80
+ const profileIdSafe = sanitizeStorageSegment(profileId, 'ai-profile');
81
+ return storageKey.startsWith(`workspaces/${workspaceIdSafe}/assets/images/ai-profiles/${profileIdSafe}/avatar/`);
82
+ }
83
+ async function writeAvatarObject(deps, storageKey, payload, provider) {
84
+ if (provider === 'r2') {
85
+ const objectStorageClient = deps.getObjectStorageClient();
86
+ if (!objectStorageClient)
87
+ throw new Error('Object storage is not configured.');
88
+ await objectStorageClient.putObject(storageKey, payload.buffer, payload.mimeType);
89
+ return;
90
+ }
91
+ const { basePath } = deps.core.getPaths();
92
+ const destination = deps.ensureWithinDir(path.join(basePath, storageKey), basePath);
93
+ if (!destination)
94
+ throw new Error('Invalid AI profile avatar storage path.');
95
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
96
+ fs.writeFileSync(destination, payload.buffer);
97
+ }
98
+ async function deleteAvatarObject(deps, storageKey, provider) {
99
+ try {
100
+ deps.markAssetDeleted(storageKey);
101
+ }
102
+ catch {
103
+ // Best-effort legacy asset index cleanup should not make the canonical
104
+ // workspace asset/profile update fail after the new slots are written.
105
+ }
106
+ if (provider === 'r2') {
107
+ const objectStorageClient = deps.getObjectStorageClient();
108
+ if (objectStorageClient) {
109
+ await objectStorageClient.deleteObject(storageKey).catch(() => false);
110
+ }
111
+ return;
112
+ }
113
+ const { basePath } = deps.core.getPaths();
114
+ const destination = deps.ensureWithinDir(path.join(basePath, storageKey), basePath);
115
+ if (destination && fs.existsSync(destination)) {
116
+ fs.rmSync(destination, { force: true });
117
+ }
118
+ }
119
+ function upsertAvatarAsset(input) {
120
+ if (!input.deps.workspaceAssetStore) {
121
+ throw new Error('Workspace asset storage is required for AI profile avatars.');
122
+ }
123
+ const contentSha256 = createHash('sha256').update(input.payload.buffer).digest('hex');
124
+ const existing = input.deps.workspaceAssetStore.getByStorageKey(input.storageKey, input.workspaceId);
125
+ return input.deps.workspaceAssetStore.upsertAsset({
126
+ assetId: existing?.assetId || stableAssetId(input.workspaceId, input.profileId, input.slot),
127
+ workspaceId: input.workspaceId,
128
+ kind: 'image',
129
+ logicalName: `AI profile avatar ${input.slot}`,
130
+ originalFilename: normalizeId(input.payload.originalName) || `${input.slot}.${extensionForMimeType(input.payload.mimeType)}`,
131
+ mimeType: input.payload.mimeType,
132
+ storageProvider: input.provider,
133
+ storageKey: input.storageKey,
134
+ contentSha256,
135
+ sizeBytes: input.payload.buffer.length,
136
+ documentId: null,
137
+ version: null,
138
+ isLatest: true,
139
+ scanStatus: 'clean',
140
+ scanEngine: null,
141
+ scanDetails: null,
142
+ createdByUserId: existing?.createdByUserId || normalizeId(input.actorId) || null,
143
+ createdAt: existing?.createdAt || input.nowIso,
144
+ updatedAt: input.nowIso,
145
+ deletedAt: null,
146
+ purgeAfter: null,
147
+ });
148
+ }
149
+ async function cleanupAiProfileAvatarAssets(input) {
150
+ const keep = new Set(Array.from(input.keepStorageKeys || []).filter(Boolean));
151
+ const candidates = new Map();
152
+ for (const storageKey of input.explicitStorageKeys || []) {
153
+ const normalized = normalizeTaskDataRelativePath(normalizeId(storageKey));
154
+ if (!normalized || keep.has(normalized))
155
+ continue;
156
+ if (storageKeyBelongsToAiProfileAvatar(input.workspaceId, input.profileId, normalized)) {
157
+ candidates.set(normalized, input.providerFallback);
158
+ }
159
+ }
160
+ if (input.deps.workspaceAssetStore) {
161
+ const assets = input.deps.workspaceAssetStore.listAllByWorkspace(input.workspaceId, {
162
+ includeDeleted: false,
163
+ kind: 'image',
164
+ });
165
+ for (const asset of assets) {
166
+ if (!storageKeyBelongsToAiProfileAvatar(input.workspaceId, input.profileId, asset.storageKey))
167
+ continue;
168
+ if (keep.has(asset.storageKey))
169
+ continue;
170
+ candidates.set(asset.storageKey, asset.storageProvider === 'r2' ? 'r2' : 'local');
171
+ }
172
+ }
173
+ for (const [storageKey, provider] of candidates) {
174
+ try {
175
+ const asset = input.deps.workspaceAssetStore?.getByStorageKey(storageKey, input.workspaceId);
176
+ if (asset && !asset.deletedAt) {
177
+ input.deps.workspaceAssetStore?.markDeleted(asset.assetId, input.workspaceId);
178
+ }
179
+ }
180
+ catch {
181
+ input.deps.appendStorageTelemetry('ai_avatar.cleanup', {
182
+ path: storageKey,
183
+ provider,
184
+ status: 'metadata-delete-failed',
185
+ });
186
+ }
187
+ try {
188
+ await deleteAvatarObject(input.deps, storageKey, provider);
189
+ input.deps.appendStorageTelemetry('ai_avatar.cleanup', {
190
+ path: storageKey,
191
+ provider,
192
+ status: 'deleted',
193
+ });
194
+ }
195
+ catch {
196
+ input.deps.appendStorageTelemetry('ai_avatar.cleanup', {
197
+ path: storageKey,
198
+ provider,
199
+ status: 'object-delete-failed',
200
+ });
201
+ }
202
+ }
203
+ }
204
+ export async function saveAiProfileAvatarAssets(deps, input) {
205
+ const workspaceId = normalizeId(input.workspaceId);
206
+ const profileId = normalizeId(input.profileId);
207
+ if (!workspaceId)
208
+ throw new Error('workspaceId is required.');
209
+ if (!profileId)
210
+ throw new Error('profileId is required.');
211
+ const existingProfile = deps.core.listAiProfiles(workspaceId)
212
+ .find((profile) => profile.id === profileId);
213
+ if (!existingProfile)
214
+ throw new Error(`Profile ${profileId} not found.`);
215
+ const displayMimeType = validatePayload(input.display, 'Display avatar');
216
+ const sourcePayload = input.source || input.display;
217
+ const sourceMimeType = validatePayload(sourcePayload, 'Source avatar');
218
+ const displayPayload = { ...input.display, mimeType: displayMimeType };
219
+ const normalizedSourcePayload = { ...sourcePayload, mimeType: sourceMimeType };
220
+ const displayStorageKey = buildAiProfileAvatarStorageKey({
221
+ workspaceId,
222
+ profileId,
223
+ slot: 'display',
224
+ mimeType: displayMimeType,
225
+ });
226
+ const sourceStorageKey = buildAiProfileAvatarStorageKey({
227
+ workspaceId,
228
+ profileId,
229
+ slot: 'source',
230
+ mimeType: sourceMimeType,
231
+ });
232
+ const objectStorageClient = deps.getObjectStorageClient();
233
+ const provider = objectStorageClient ? 'r2' : 'local';
234
+ const nowIso = new Date().toISOString();
235
+ await writeAvatarObject(deps, displayStorageKey, displayPayload, provider);
236
+ await writeAvatarObject(deps, sourceStorageKey, normalizedSourcePayload, provider);
237
+ const displayAsset = upsertAvatarAsset({
238
+ deps,
239
+ workspaceId,
240
+ profileId,
241
+ slot: 'display',
242
+ storageKey: displayStorageKey,
243
+ payload: displayPayload,
244
+ provider,
245
+ actorId: input.actorId,
246
+ nowIso,
247
+ });
248
+ const sourceAsset = upsertAvatarAsset({
249
+ deps,
250
+ workspaceId,
251
+ profileId,
252
+ slot: 'source',
253
+ storageKey: sourceStorageKey,
254
+ payload: normalizedSourcePayload,
255
+ provider,
256
+ actorId: input.actorId,
257
+ nowIso,
258
+ });
259
+ deps.core.notifyDocumentMetadataMutation(workspaceId, [displayStorageKey, sourceStorageKey], 'upsert');
260
+ deps.appendStorageTelemetry('ai_avatar.upload', {
261
+ path: displayStorageKey,
262
+ provider,
263
+ status: 'stored',
264
+ });
265
+ const profile = deps.core.updateAiProfileAvatar({
266
+ workspaceId,
267
+ profileId,
268
+ avatarUrl: contextUrlForStorageKey(displayStorageKey),
269
+ avatarSourceUrl: contextUrlForStorageKey(sourceStorageKey),
270
+ });
271
+ await cleanupAiProfileAvatarAssets({
272
+ deps,
273
+ workspaceId,
274
+ profileId,
275
+ keepStorageKeys: [displayStorageKey, sourceStorageKey],
276
+ explicitStorageKeys: [
277
+ storageKeyFromContextUrl(existingProfile.avatarUrl),
278
+ storageKeyFromContextUrl(existingProfile.avatarSourceUrl),
279
+ ],
280
+ providerFallback: provider,
281
+ });
282
+ return {
283
+ profile,
284
+ displayAsset,
285
+ sourceAsset,
286
+ displayStorageKey,
287
+ sourceStorageKey,
288
+ };
289
+ }
290
+ export async function clearAiProfileAvatarAssets(deps, input) {
291
+ const workspaceId = normalizeId(input.workspaceId);
292
+ const profileId = normalizeId(input.profileId);
293
+ if (!workspaceId)
294
+ throw new Error('workspaceId is required.');
295
+ if (!profileId)
296
+ throw new Error('profileId is required.');
297
+ const existingProfile = deps.core.listAiProfiles(workspaceId)
298
+ .find((profile) => profile.id === profileId);
299
+ const objectStorageClient = deps.getObjectStorageClient();
300
+ const provider = objectStorageClient ? 'r2' : 'local';
301
+ const profile = deps.core.updateAiProfileAvatar({
302
+ workspaceId,
303
+ profileId,
304
+ avatarUrl: null,
305
+ avatarSourceUrl: null,
306
+ });
307
+ await cleanupAiProfileAvatarAssets({
308
+ deps,
309
+ workspaceId,
310
+ profileId,
311
+ keepStorageKeys: [],
312
+ explicitStorageKeys: [
313
+ storageKeyFromContextUrl(existingProfile?.avatarUrl),
314
+ storageKeyFromContextUrl(existingProfile?.avatarSourceUrl),
315
+ ],
316
+ providerFallback: provider,
317
+ });
318
+ return { profile };
319
+ }
@@ -0,0 +1 @@
1
+ export {};