@taskforcehq/taskforce 0.3.300 → 0.3.302

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 (153) hide show
  1. package/README.md +19 -0
  2. package/dist/Taskforce.module.css +26 -2
  3. package/dist/TaskforceCore.filter-persistence.test.js +1 -1
  4. package/dist/TaskforceCore.js +13 -24
  5. package/dist/TaskforceCore.test.js +167 -152
  6. package/dist/components/features/TaskSettings.js +14 -82
  7. package/dist/components/features/TaskSettings.test.js +33 -70
  8. package/dist/components/features/settings/TaxonomyLibraryManager.js +8 -2
  9. package/dist/components/features/settings/TaxonomyLibraryManager.test.js +1 -1
  10. package/dist/components/task/TaskActionHeader.js +6 -3
  11. package/dist/components/task/TaskCard.js +6 -3
  12. package/dist/components/task/TaskForm.js +2 -2
  13. package/dist/components/task/TaskForm.test.js +41 -2
  14. package/dist/components/ui/ReferenceBadgeButton.js +1 -1
  15. package/dist/components/views/PlansPage.js +9 -8
  16. package/dist/components/views/StandaloneLayout.js +490 -207
  17. package/dist/components/views/panels/FilterToolbar.d.ts +2 -0
  18. package/dist/components/views/panels/FilterToolbarControls.d.ts +1 -0
  19. package/dist/components/views/panels/FilterToolbarControls.js +1 -1
  20. package/dist/components/views/panels/PlanningDrawer.js +19 -2
  21. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +43 -0
  22. package/dist/components/views/standalone/modals/AccountHubModal.js +22 -0
  23. package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.d.ts +9 -0
  24. package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.js +8 -0
  25. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +26 -0
  26. package/dist/components/views/standalone/modals/EditProfileModal.js +10 -0
  27. package/dist/components/views/standalone/modals/HelpModal.d.ts +9 -0
  28. package/dist/components/views/standalone/modals/HelpModal.js +8 -0
  29. package/dist/components/views/standalone/modals/ScheduleWarningModal.d.ts +13 -0
  30. package/dist/components/views/standalone/modals/ScheduleWarningModal.js +6 -0
  31. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +61 -0
  32. package/dist/components/views/standalone/modals/SyncStatusModal.js +38 -0
  33. package/dist/components/views/standalone/modals/TeamManagementModal.d.ts +64 -0
  34. package/dist/components/views/standalone/modals/TeamManagementModal.js +10 -0
  35. package/dist/components/views/standalone/modals/types.d.ts +2 -0
  36. package/dist/components/views/standalone/modals/types.js +1 -0
  37. package/dist/components/views/teamManagementAccess.d.ts +1 -1
  38. package/dist/components/views/teamManagementAccess.js +2 -2
  39. package/dist/components/views/teamManagementAccess.test.js +3 -3
  40. package/dist/config/deployment.js +2 -2
  41. package/dist/config/deployment.test.js +5 -3
  42. package/dist/core/AiProfileService.js +7 -2
  43. package/dist/core/AiProfiles.test.js +19 -0
  44. package/dist/core/SyncReconciliationService.d.ts +2 -0
  45. package/dist/core/SyncReconciliationService.js +3 -0
  46. package/dist/core/TaskAuditTimeline.test.js +40 -0
  47. package/dist/core/Taskforce.d.ts +49 -0
  48. package/dist/core/Taskforce.ids.test.js +133 -0
  49. package/dist/core/Taskforce.js +241 -78
  50. package/dist/core/UserProfileAvatarDrafts.test.d.ts +1 -0
  51. package/dist/core/UserProfileAvatarDrafts.test.js +102 -0
  52. package/dist/core/taskReferencePolicy.d.ts +28 -0
  53. package/dist/core/taskReferencePolicy.js +189 -0
  54. package/dist/core/types.d.ts +2 -0
  55. package/dist/hooks/tasks/useTaskFormActions.js +3 -0
  56. package/dist/hooks/useTaskData.js +16 -11
  57. package/dist/hooks/useTaskData.test.js +127 -0
  58. package/dist/hooks/useTaskforce.d.ts +5 -0
  59. package/dist/hooks/useTaskforce.js +63 -15
  60. package/dist/hooks/useTaskforce.sync-behavior.test.js +185 -6
  61. package/dist/mcp/aiProfileBootstrap.d.ts +29 -0
  62. package/dist/mcp/aiProfileBootstrap.js +80 -0
  63. package/dist/mcp/aiProfileBootstrap.test.d.ts +1 -0
  64. package/dist/mcp/aiProfileBootstrap.test.js +68 -0
  65. package/dist/mcp/clientIntegrations.d.ts +2 -8
  66. package/dist/mcp/clientIntegrations.js +21 -40
  67. package/dist/mcp/clientIntegrations.test.js +28 -2
  68. package/dist/mcp/clientProfileDefaults.d.ts +12 -0
  69. package/dist/mcp/clientProfileDefaults.js +37 -0
  70. package/dist/mcp/runtime.d.ts +10 -7
  71. package/dist/mcp/runtime.js +45 -23
  72. package/dist/mcp/runtime.test.js +56 -3
  73. package/dist/migrations/taskSchemaMigrations.js +53 -1
  74. package/dist/qaOpen.test.d.ts +1 -0
  75. package/dist/qaOpen.test.js +56 -0
  76. package/dist/server/index.cookieProxy.test.js +1 -0
  77. package/dist/server/index.d.ts +1 -0
  78. package/dist/server/index.js +15 -1
  79. package/dist/server/index.test.js +14 -1
  80. package/dist/server/routes/admin.js +25 -22
  81. package/dist/server/routes/auth.d.ts +5 -0
  82. package/dist/server/routes/auth.js +350 -16
  83. package/dist/server/routes/billing.js +11 -1
  84. package/dist/server/routes/billing.test.js +32 -2
  85. package/dist/server/routes/documents.js +6 -1
  86. package/dist/server/routes/shared.d.ts +1 -1
  87. package/dist/server/routes/shared.js +11 -3
  88. package/dist/server/routes/sync.integration.test.js +130 -0
  89. package/dist/server/routes/sync.js +226 -1924
  90. package/dist/server/routes/tasks.js +18 -4
  91. package/dist/server/routes.js +26 -19
  92. package/dist/server/routes.test.js +352 -6
  93. package/dist/shared/runtimeContract.d.ts +1 -1
  94. package/dist/sync/collaborationSyncPayload.d.ts +17 -0
  95. package/dist/sync/collaborationSyncPayload.js +131 -0
  96. package/dist/sync/collaborationSyncPayload.test.d.ts +1 -0
  97. package/dist/sync/collaborationSyncPayload.test.js +104 -0
  98. package/dist/sync/collaborationSyncState.d.ts +15 -0
  99. package/dist/sync/collaborationSyncState.js +60 -0
  100. package/dist/sync/contentSyncPayload.d.ts +45 -0
  101. package/dist/sync/contentSyncPayload.js +104 -0
  102. package/dist/sync/contentSyncPayload.test.d.ts +1 -0
  103. package/dist/sync/contentSyncPayload.test.js +104 -0
  104. package/dist/sync/contentSyncState.d.ts +88 -0
  105. package/dist/sync/contentSyncState.js +743 -0
  106. package/dist/sync/planningSyncPayload.d.ts +9 -0
  107. package/dist/sync/planningSyncPayload.js +82 -0
  108. package/dist/sync/planningSyncPayload.test.d.ts +1 -0
  109. package/dist/sync/planningSyncPayload.test.js +68 -0
  110. package/dist/sync/syncApplyHandlers.d.ts +83 -0
  111. package/dist/sync/syncApplyHandlers.js +445 -0
  112. package/dist/sync/taskSyncPayload.d.ts +33 -0
  113. package/dist/sync/taskSyncPayload.js +126 -0
  114. package/dist/sync/taskSyncPayload.test.d.ts +1 -0
  115. package/dist/sync/taskSyncPayload.test.js +54 -0
  116. package/dist/sync/workspacePullFeed.d.ts +28 -0
  117. package/dist/sync/workspacePullFeed.js +301 -0
  118. package/dist/sync/workspacePullFeed.test.d.ts +1 -0
  119. package/dist/sync/workspacePullFeed.test.js +166 -0
  120. package/dist/sync/workspaceSyncModel.d.ts +4 -1
  121. package/dist/sync/workspaceSyncModel.js +9 -2
  122. package/dist/sync/workspaceSyncModel.test.js +29 -0
  123. package/dist/test/setup.js +1 -1
  124. package/dist/types.d.ts +1 -0
  125. package/dist/ui/assets/{AgentsModule-TVmCgizS.js → AgentsModule-JG5jc3he.js} +1 -1
  126. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-CAyhnPeR.js → AnnotatedAttachmentWorkspace-Dvi02Y7q.js} +1 -1
  127. package/dist/ui/assets/{DocumentWorkspace-BOCw5e4D.js → DocumentWorkspace-iys3zfca.js} +2 -2
  128. package/dist/ui/assets/{InitiativesModule-CN0r02Oi.js → InitiativesModule-DJvLJI3A.js} +1 -1
  129. package/dist/ui/assets/PlansPage-BMPlpFpw.js +1 -0
  130. package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +8 -0
  131. package/dist/ui/assets/{TaskSettings-BwB9NtmJ.css → TaskSettings-CnIBL_Eb.css} +1 -1
  132. package/dist/ui/assets/{WorkflowsModule-CY9R2zPB.js → WorkflowsModule-BguHeQaV.js} +1 -1
  133. package/dist/ui/assets/index-MXeWoebC.css +1 -0
  134. package/dist/ui/assets/index-YJ4k5QNa.js +6 -0
  135. package/dist/ui/assets/{vendor-icons-QZyhEwgT.js → vendor-icons-C8EED3Oh.js} +1 -1
  136. package/dist/ui/index.html +3 -3
  137. package/dist/utils/billingStatusCache.d.ts +21 -0
  138. package/dist/utils/billingStatusCache.js +70 -0
  139. package/dist/utils/taskActivity.js +17 -4
  140. package/dist/utils/taskActivity.test.js +27 -0
  141. package/dist/utils/taskNormalization.js +10 -2
  142. package/dist/utils/taskReferences.d.ts +11 -1
  143. package/dist/utils/taskReferences.js +33 -1
  144. package/package.json +4 -1
  145. package/scripts/deploy-prod.sh +41 -0
  146. package/scripts/deploy-staging.sh +23 -0
  147. package/scripts/qa-open-lib.mjs +175 -0
  148. package/scripts/qa-open-lib.mjs.d.ts +49 -0
  149. package/scripts/qa-open.mjs +352 -0
  150. package/dist/ui/assets/PlansPage-FcQSNLZd.js +0 -1
  151. package/dist/ui/assets/TaskSettings-Cw5zYD1L.js +0 -8
  152. package/dist/ui/assets/index-Bzqy5QGm.js +0 -6
  153. package/dist/ui/assets/index-DxRiI3tY.css +0 -1
@@ -87,6 +87,7 @@ const archivedTaskRecord = (id, title) => ({
87
87
  describe('useTaskforce sync behavior', () => {
88
88
  beforeEach(() => {
89
89
  localStorage.clear();
90
+ sessionStorage.clear();
90
91
  });
91
92
  afterEach(() => {
92
93
  vi.useRealTimers();
@@ -97,7 +98,7 @@ describe('useTaskforce sync behavior', () => {
97
98
  it('merges a server-returned task into the active collection immediately', async () => {
98
99
  vi.stubGlobal('fetch', vi.fn((url, init) => {
99
100
  if (url.includes('/api/taskforce/auth/runtime-config')) {
100
- return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'local' } });
101
+ return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } });
101
102
  }
102
103
  if (url.includes('/api/taskforce/auth/session')) {
103
104
  return jsonResponse({
@@ -188,7 +189,7 @@ describe('useTaskforce sync behavior', () => {
188
189
  it('keeps the dawn theme when settings re-fetch configuration', async () => {
189
190
  const fetchMock = vi.fn((url) => {
190
191
  if (url.includes('/api/taskforce/auth/runtime-config')) {
191
- return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'local' } });
192
+ return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } });
192
193
  }
193
194
  if (url.includes('/api/taskforce/auth/session')) {
194
195
  return jsonResponse({
@@ -261,7 +262,7 @@ describe('useTaskforce sync behavior', () => {
261
262
  it('merges a server-returned archived task immediately instead of dropping it between collections', async () => {
262
263
  vi.stubGlobal('fetch', vi.fn((url, init) => {
263
264
  if (url.includes('/api/taskforce/auth/runtime-config')) {
264
- return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'local' } });
265
+ return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } });
265
266
  }
266
267
  if (url.includes('/api/taskforce/ui-state?key=app')) {
267
268
  return jsonResponse({ success: true, state: {} });
@@ -339,7 +340,7 @@ describe('useTaskforce sync behavior', () => {
339
340
  it('merges a server-returned completed task immediately when toggling done', async () => {
340
341
  vi.stubGlobal('fetch', vi.fn((url, init) => {
341
342
  if (url.includes('/api/taskforce/auth/runtime-config')) {
342
- return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'local' } });
343
+ return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } });
343
344
  }
344
345
  if (url.includes('/api/taskforce/ui-state?key=app')) {
345
346
  return jsonResponse({ success: true, state: {} });
@@ -416,7 +417,7 @@ describe('useTaskforce sync behavior', () => {
416
417
  vi.stubGlobal('confirm', vi.fn(() => true));
417
418
  vi.stubGlobal('fetch', vi.fn((url, init) => {
418
419
  if (url.includes('/api/taskforce/auth/runtime-config')) {
419
- return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'local' } });
420
+ return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } });
420
421
  }
421
422
  if (url.includes('/api/taskforce/ui-state?key=app')) {
422
423
  return jsonResponse({ success: true, state: {} });
@@ -503,7 +504,7 @@ describe('useTaskforce sync behavior', () => {
503
504
  config: {
504
505
  runtimeMode: 'local',
505
506
  workspaceMode: 'single-local',
506
- authSource: 'local',
507
+ authSource: 'cloud',
507
508
  baseUrl: 'http://localhost:5174',
508
509
  apiBaseUrl: 'http://localhost:5174',
509
510
  cloudAuthBaseUrl: 'http://localhost:5174'
@@ -918,6 +919,184 @@ describe('useTaskforce sync behavior', () => {
918
919
  expect(result.current.assigneeOptions.some((option) => option.value === 'ai-profile-codex' && option.label === 'Codex')).toBe(true);
919
920
  });
920
921
  });
922
+ it('keeps the local hydrated workspace role when workspace-members temporarily fails', async () => {
923
+ let workspaceMembersCalls = 0;
924
+ vi.stubGlobal('fetch', vi.fn((url) => {
925
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
926
+ return jsonResponse({
927
+ config: {
928
+ runtimeMode: 'local',
929
+ workspaceMode: 'single-local',
930
+ authSource: 'cloud',
931
+ workspaceSwitchingEnabled: false
932
+ }
933
+ });
934
+ }
935
+ if (url.includes('/api/taskforce/auth/session')) {
936
+ return jsonResponse({
937
+ authenticated: true,
938
+ authRequiredForApi: true,
939
+ betaAccess: true,
940
+ userId: 'user-1',
941
+ email: 'user@example.com',
942
+ workspaceId: 'workspace-cloud-1'
943
+ });
944
+ }
945
+ if (url.includes('/api/taskforce/ui-state?key=app'))
946
+ return jsonResponse({ success: true, state: {} });
947
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
948
+ return jsonResponse({
949
+ success: true,
950
+ state: { version: 2, enabled: false, phase: 'idle', pullCursor: null }
951
+ });
952
+ }
953
+ if (url.includes('/api/taskforce/config')) {
954
+ return jsonResponse({
955
+ projectName: 'Local Project',
956
+ workspaceId: 'workspace-cloud-1',
957
+ runtimeMode: 'local',
958
+ authRequiredForApi: true,
959
+ userId: 'user-1',
960
+ setupState: {
961
+ globalSetupState: 'complete',
962
+ workspaceSetupState: 'complete',
963
+ workspaceId: 'workspace-cloud-1'
964
+ }
965
+ });
966
+ }
967
+ if (url.includes('/api/taskforce/global-settings'))
968
+ return jsonResponse({});
969
+ if (url.includes('/api/taskforce/categories'))
970
+ return jsonResponse({ categories: [{ value: 'general', label: 'General' }] });
971
+ if (url.includes('/api/taskforce/types'))
972
+ return jsonResponse({ types: [] });
973
+ if (url.includes('/api/taskforce/priorities'))
974
+ return jsonResponse({ priorities: [] });
975
+ if (url.includes('/api/taskforce/approaches'))
976
+ return jsonResponse({ approaches: [] });
977
+ if (url.includes('/api/taskforce/specialists'))
978
+ return jsonResponse({ specialists: [] });
979
+ if (url.includes('/api/taskforce/tasks'))
980
+ return jsonResponse({ tasks: [activeTask] });
981
+ if (url.includes('/api/taskforce/archive'))
982
+ return jsonResponse({ archived: [] });
983
+ if (url.includes('/api/taskforce/workspace/assignee-options')) {
984
+ return jsonResponse({
985
+ assignees: [
986
+ { value: 'member-1', label: 'Member One', icon: 'User', color: '#22c55e', kind: 'member', userId: 'member-1', email: 'member1@example.com' }
987
+ ]
988
+ });
989
+ }
990
+ if (url.includes('/api/taskforce/auth/workspace-members')) {
991
+ workspaceMembersCalls += 1;
992
+ if (workspaceMembersCalls === 1) {
993
+ return jsonResponse({
994
+ users: [
995
+ {
996
+ userId: 'user-1',
997
+ email: 'user@example.com',
998
+ displayName: 'Signed In Owner',
999
+ role: 'owner',
1000
+ status: 'active',
1001
+ disabled: false
1002
+ }
1003
+ ]
1004
+ });
1005
+ }
1006
+ return Promise.reject(new Error('temporary workspace-members failure'));
1007
+ }
1008
+ if (url.includes('/api/taskforce/data-version'))
1009
+ return jsonResponse({ dataVersion: 1 });
1010
+ return jsonResponse({});
1011
+ }));
1012
+ const { result } = renderHook(() => useTaskforce({ config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }), { wrapper });
1013
+ await waitFor(() => {
1014
+ expect(result.current.currentWorkspaceRole).toBe('owner');
1015
+ });
1016
+ await act(async () => {
1017
+ await result.current.fetchAssigneeOptions();
1018
+ });
1019
+ expect(result.current.currentWorkspaceRole).toBe('owner');
1020
+ });
1021
+ it('seeds local auth state from the persisted loopback actor while localhost session bootstrap is in flight', async () => {
1022
+ sessionStorage.setItem('taskforce.localMutationActorUserId.v1', 'user-1');
1023
+ const fetchMock = vi.fn((url) => {
1024
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
1025
+ return jsonResponse({
1026
+ config: {
1027
+ runtimeMode: 'local',
1028
+ workspaceMode: 'single-local',
1029
+ authSource: 'cloud',
1030
+ workspaceSwitchingEnabled: false,
1031
+ cloudAuthBaseUrl: 'https://app.taskforcehq.ai',
1032
+ cloudAuthViaLocalProxy: true
1033
+ }
1034
+ });
1035
+ }
1036
+ if (url.includes('/api/taskforce/auth/session')) {
1037
+ return jsonResponse({
1038
+ authenticated: true,
1039
+ authRequiredForApi: true,
1040
+ betaAccess: true,
1041
+ userId: 'user-1',
1042
+ email: 'user@example.com',
1043
+ workspaceId: 'workspace-cloud-1'
1044
+ });
1045
+ }
1046
+ if (url.includes('/api/taskforce/ui-state?key=app'))
1047
+ return jsonResponse({ success: true, state: {} });
1048
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
1049
+ return jsonResponse({
1050
+ success: true,
1051
+ state: { version: 2, enabled: false, phase: 'idle', pullCursor: null }
1052
+ });
1053
+ }
1054
+ if (url.includes('/api/taskforce/config')) {
1055
+ return jsonResponse({
1056
+ projectName: 'Local Project',
1057
+ workspaceId: 'workspace-cloud-1',
1058
+ runtimeMode: 'local',
1059
+ setupState: {
1060
+ globalSetupState: 'complete',
1061
+ workspaceSetupState: 'complete',
1062
+ workspaceId: 'workspace-cloud-1'
1063
+ }
1064
+ });
1065
+ }
1066
+ if (url.includes('/api/taskforce/global-settings'))
1067
+ return jsonResponse({});
1068
+ if (url.includes('/api/taskforce/categories'))
1069
+ return jsonResponse({ categories: [{ value: 'general', label: 'General' }] });
1070
+ if (url.includes('/api/taskforce/types'))
1071
+ return jsonResponse({ types: [] });
1072
+ if (url.includes('/api/taskforce/priorities'))
1073
+ return jsonResponse({ priorities: [] });
1074
+ if (url.includes('/api/taskforce/approaches'))
1075
+ return jsonResponse({ approaches: [] });
1076
+ if (url.includes('/api/taskforce/specialists'))
1077
+ return jsonResponse({ specialists: [] });
1078
+ if (url.includes('/api/taskforce/tasks'))
1079
+ return jsonResponse({ tasks: [activeTask] });
1080
+ if (url.includes('/api/taskforce/archive'))
1081
+ return jsonResponse({ archived: [] });
1082
+ if (url.includes('/api/taskforce/workspace/assignee-options'))
1083
+ return jsonResponse({ assignees: [] });
1084
+ if (url.includes('/api/taskforce/data-version'))
1085
+ return jsonResponse({ dataVersion: 1 });
1086
+ return jsonResponse({});
1087
+ });
1088
+ vi.stubGlobal('fetch', fetchMock);
1089
+ const { result } = renderHook(() => useTaskforce({ config: { cloudAuthBaseUrl: 'https://app.taskforcehq.ai' } }), { wrapper });
1090
+ expect(result.current.isAuthenticated).toBe(true);
1091
+ expect(result.current.authSessionResolved).toBe(true);
1092
+ await waitFor(() => {
1093
+ expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/api/taskforce/auth/session'))).toBe(true);
1094
+ });
1095
+ await waitFor(() => {
1096
+ expect(result.current.isAuthenticated).toBe(true);
1097
+ expect(result.current.authSessionResolved).toBe(true);
1098
+ });
1099
+ });
921
1100
  it('enables cloud realtime refresh for authenticated sessions and refreshes open task comments from signals', async () => {
922
1101
  vi.stubGlobal('WebSocket', MockWebSocket);
923
1102
  let invalidationTaskFetchCount = 0;
@@ -0,0 +1,29 @@
1
+ import type { IncomingHttpHeaders } from 'http';
2
+ import type { ResolveAiProfileInput } from '../core/Taskforce.js';
3
+ import { type AiProfileSurfaceType } from '../shared/aiProfileSurfaceType.js';
4
+ export interface AiProfileBootstrapConfig {
5
+ aiProfileToken?: string | null;
6
+ aiProfileName?: string | null;
7
+ aiProfileIcon?: string | null;
8
+ aiProfileColor?: string | null;
9
+ aiProfileDescription?: string | null;
10
+ aiProfileRole?: string | null;
11
+ aiProfileProvider?: string | null;
12
+ aiProfileModel?: string | null;
13
+ aiProfileSurfaceType?: AiProfileSurfaceType | null;
14
+ }
15
+ export declare const AI_PROFILE_BOOTSTRAP_HEADER_NAMES: {
16
+ readonly aiProfileToken: "X-Taskforce-AI-Profile-Token";
17
+ readonly aiProfileName: "X-Taskforce-AI-Name";
18
+ readonly aiProfileIcon: "X-Taskforce-AI-Icon";
19
+ readonly aiProfileColor: "X-Taskforce-AI-Color";
20
+ readonly aiProfileDescription: "X-Taskforce-AI-Description";
21
+ readonly aiProfileRole: "X-Taskforce-AI-Role";
22
+ readonly aiProfileProvider: "X-Taskforce-AI-Provider";
23
+ readonly aiProfileModel: "X-Taskforce-AI-Model";
24
+ readonly aiProfileSurfaceType: "X-Taskforce-AI-Surface-Type";
25
+ };
26
+ export declare function normalizeAiProfileBootstrapConfig(input: AiProfileBootstrapConfig): AiProfileBootstrapConfig;
27
+ export declare function buildAiProfileBootstrapHeaders(input: AiProfileBootstrapConfig): Record<string, string>;
28
+ export declare function parseAiProfileBootstrapHeaders(headers: IncomingHttpHeaders | Record<string, string | string[] | undefined>): Required<AiProfileBootstrapConfig>;
29
+ export declare function buildResolveAiProfileInputFromBootstrap(workspaceId: string, bootstrap: AiProfileBootstrapConfig, includeToken?: boolean): Pick<ResolveAiProfileInput, 'workspaceId' | 'name' | 'profileToken' | 'icon' | 'color' | 'description' | 'role' | 'provider' | 'model' | 'surfaceType'> | null;
@@ -0,0 +1,80 @@
1
+ import { parseAiProfileSurfaceType } from '../shared/aiProfileSurfaceType.js';
2
+ export const AI_PROFILE_BOOTSTRAP_HEADER_NAMES = {
3
+ aiProfileToken: 'X-Taskforce-AI-Profile-Token',
4
+ aiProfileName: 'X-Taskforce-AI-Name',
5
+ aiProfileIcon: 'X-Taskforce-AI-Icon',
6
+ aiProfileColor: 'X-Taskforce-AI-Color',
7
+ aiProfileDescription: 'X-Taskforce-AI-Description',
8
+ aiProfileRole: 'X-Taskforce-AI-Role',
9
+ aiProfileProvider: 'X-Taskforce-AI-Provider',
10
+ aiProfileModel: 'X-Taskforce-AI-Model',
11
+ aiProfileSurfaceType: 'X-Taskforce-AI-Surface-Type',
12
+ };
13
+ function normalizeBootstrapValue(value) {
14
+ const normalized = String(value || '').trim();
15
+ return normalized || null;
16
+ }
17
+ function normalizeBootstrapSurfaceType(value) {
18
+ if (value === null || value === undefined)
19
+ return null;
20
+ return parseAiProfileSurfaceType(value);
21
+ }
22
+ export function normalizeAiProfileBootstrapConfig(input) {
23
+ return {
24
+ aiProfileToken: normalizeBootstrapValue(input.aiProfileToken),
25
+ aiProfileName: normalizeBootstrapValue(input.aiProfileName),
26
+ aiProfileIcon: normalizeBootstrapValue(input.aiProfileIcon),
27
+ aiProfileColor: normalizeBootstrapValue(input.aiProfileColor),
28
+ aiProfileDescription: normalizeBootstrapValue(input.aiProfileDescription),
29
+ aiProfileRole: normalizeBootstrapValue(input.aiProfileRole),
30
+ aiProfileProvider: normalizeBootstrapValue(input.aiProfileProvider),
31
+ aiProfileModel: normalizeBootstrapValue(input.aiProfileModel),
32
+ aiProfileSurfaceType: normalizeBootstrapSurfaceType(input.aiProfileSurfaceType),
33
+ };
34
+ }
35
+ export function buildAiProfileBootstrapHeaders(input) {
36
+ const normalized = normalizeAiProfileBootstrapConfig(input);
37
+ const headers = {};
38
+ for (const [key, headerName] of Object.entries(AI_PROFILE_BOOTSTRAP_HEADER_NAMES)) {
39
+ const value = normalized[key];
40
+ if (value)
41
+ headers[headerName] = value;
42
+ }
43
+ return headers;
44
+ }
45
+ export function parseAiProfileBootstrapHeaders(headers) {
46
+ const readHeader = (headerName) => {
47
+ const value = headers[headerName] ?? headers[headerName.toLowerCase()];
48
+ if (Array.isArray(value))
49
+ return normalizeBootstrapValue(value[0]);
50
+ return normalizeBootstrapValue(value);
51
+ };
52
+ return {
53
+ aiProfileToken: readHeader(AI_PROFILE_BOOTSTRAP_HEADER_NAMES.aiProfileToken),
54
+ aiProfileName: readHeader(AI_PROFILE_BOOTSTRAP_HEADER_NAMES.aiProfileName),
55
+ aiProfileIcon: readHeader(AI_PROFILE_BOOTSTRAP_HEADER_NAMES.aiProfileIcon),
56
+ aiProfileColor: readHeader(AI_PROFILE_BOOTSTRAP_HEADER_NAMES.aiProfileColor),
57
+ aiProfileDescription: readHeader(AI_PROFILE_BOOTSTRAP_HEADER_NAMES.aiProfileDescription),
58
+ aiProfileRole: readHeader(AI_PROFILE_BOOTSTRAP_HEADER_NAMES.aiProfileRole),
59
+ aiProfileProvider: readHeader(AI_PROFILE_BOOTSTRAP_HEADER_NAMES.aiProfileProvider),
60
+ aiProfileModel: readHeader(AI_PROFILE_BOOTSTRAP_HEADER_NAMES.aiProfileModel),
61
+ aiProfileSurfaceType: normalizeBootstrapSurfaceType(readHeader(AI_PROFILE_BOOTSTRAP_HEADER_NAMES.aiProfileSurfaceType)),
62
+ };
63
+ }
64
+ export function buildResolveAiProfileInputFromBootstrap(workspaceId, bootstrap, includeToken = true) {
65
+ const normalized = normalizeAiProfileBootstrapConfig(bootstrap);
66
+ if (!normalized.aiProfileName)
67
+ return null;
68
+ return {
69
+ workspaceId,
70
+ name: normalized.aiProfileName,
71
+ ...(includeToken && normalized.aiProfileToken ? { profileToken: normalized.aiProfileToken } : {}),
72
+ ...(normalized.aiProfileIcon ? { icon: normalized.aiProfileIcon } : {}),
73
+ ...(normalized.aiProfileColor ? { color: normalized.aiProfileColor } : {}),
74
+ ...(normalized.aiProfileDescription ? { description: normalized.aiProfileDescription } : {}),
75
+ ...(normalized.aiProfileRole ? { role: normalized.aiProfileRole } : {}),
76
+ ...(normalized.aiProfileProvider ? { provider: normalized.aiProfileProvider } : {}),
77
+ ...(normalized.aiProfileModel ? { model: normalized.aiProfileModel } : {}),
78
+ ...(normalized.aiProfileSurfaceType ? { surfaceType: normalized.aiProfileSurfaceType } : {}),
79
+ };
80
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,68 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { AI_PROFILE_BOOTSTRAP_HEADER_NAMES, buildAiProfileBootstrapHeaders, buildResolveAiProfileInputFromBootstrap, parseAiProfileBootstrapHeaders, } from './aiProfileBootstrap';
3
+ describe('aiProfileBootstrap', () => {
4
+ it('builds and parses bootstrap headers from a shared contract', () => {
5
+ const headers = buildAiProfileBootstrapHeaders({
6
+ aiProfileName: 'Automation Codex',
7
+ aiProfileToken: 'tfp_automation_codex',
8
+ aiProfileIcon: 'Code2',
9
+ aiProfileColor: '#10a37f',
10
+ aiProfileDescription: 'OpenAI Codex coding agent connected through MCP.',
11
+ aiProfileRole: 'Coding Agent',
12
+ aiProfileProvider: 'OpenAI',
13
+ aiProfileModel: 'Codex',
14
+ aiProfileSurfaceType: 'coding_tool',
15
+ });
16
+ expect(headers).toEqual({
17
+ [AI_PROFILE_BOOTSTRAP_HEADER_NAMES.aiProfileName]: 'Automation Codex',
18
+ [AI_PROFILE_BOOTSTRAP_HEADER_NAMES.aiProfileToken]: 'tfp_automation_codex',
19
+ [AI_PROFILE_BOOTSTRAP_HEADER_NAMES.aiProfileIcon]: 'Code2',
20
+ [AI_PROFILE_BOOTSTRAP_HEADER_NAMES.aiProfileColor]: '#10a37f',
21
+ [AI_PROFILE_BOOTSTRAP_HEADER_NAMES.aiProfileDescription]: 'OpenAI Codex coding agent connected through MCP.',
22
+ [AI_PROFILE_BOOTSTRAP_HEADER_NAMES.aiProfileRole]: 'Coding Agent',
23
+ [AI_PROFILE_BOOTSTRAP_HEADER_NAMES.aiProfileProvider]: 'OpenAI',
24
+ [AI_PROFILE_BOOTSTRAP_HEADER_NAMES.aiProfileModel]: 'Codex',
25
+ [AI_PROFILE_BOOTSTRAP_HEADER_NAMES.aiProfileSurfaceType]: 'coding_tool',
26
+ });
27
+ expect(parseAiProfileBootstrapHeaders(headers)).toEqual({
28
+ aiProfileName: 'Automation Codex',
29
+ aiProfileToken: 'tfp_automation_codex',
30
+ aiProfileIcon: 'Code2',
31
+ aiProfileColor: '#10a37f',
32
+ aiProfileDescription: 'OpenAI Codex coding agent connected through MCP.',
33
+ aiProfileRole: 'Coding Agent',
34
+ aiProfileProvider: 'OpenAI',
35
+ aiProfileModel: 'Codex',
36
+ aiProfileSurfaceType: 'coding_tool',
37
+ });
38
+ });
39
+ it('builds resolve_profile input from bootstrap metadata', () => {
40
+ expect(buildResolveAiProfileInputFromBootstrap('workspace-1', {
41
+ aiProfileName: 'Automation Codex',
42
+ aiProfileToken: 'tfp_automation_codex',
43
+ aiProfileIcon: 'Code2',
44
+ aiProfileColor: '#10a37f',
45
+ aiProfileDescription: 'OpenAI Codex coding agent connected through MCP.',
46
+ aiProfileRole: 'Coding Agent',
47
+ aiProfileProvider: 'OpenAI',
48
+ aiProfileModel: 'Codex',
49
+ aiProfileSurfaceType: 'coding_tool',
50
+ })).toEqual({
51
+ workspaceId: 'workspace-1',
52
+ name: 'Automation Codex',
53
+ profileToken: 'tfp_automation_codex',
54
+ icon: 'Code2',
55
+ color: '#10a37f',
56
+ description: 'OpenAI Codex coding agent connected through MCP.',
57
+ role: 'Coding Agent',
58
+ provider: 'OpenAI',
59
+ model: 'Codex',
60
+ surfaceType: 'coding_tool',
61
+ });
62
+ });
63
+ it('returns null resolve input when no bootstrap name is present', () => {
64
+ expect(buildResolveAiProfileInputFromBootstrap('workspace-1', {
65
+ aiProfileToken: 'tfp_automation_codex',
66
+ })).toBeNull();
67
+ });
68
+ });
@@ -1,21 +1,15 @@
1
+ import { type AiProfileBootstrapConfig } from './aiProfileBootstrap.js';
1
2
  export type McpClientId = 'cursor' | 'cline' | 'vscode' | 'windsurf' | 'antigravity' | 'openclaw' | 'codex' | 'claude-code' | 'gemini-cli' | 'claude-desktop' | 'chatgpt-desktop';
2
3
  export interface McpClientOption {
3
4
  id: McpClientId;
4
5
  label: string;
5
6
  disabled?: boolean;
6
7
  }
7
- export interface BuildCloudMcpClientConfigInput {
8
+ export interface BuildCloudMcpClientConfigInput extends AiProfileBootstrapConfig {
8
9
  clientId: McpClientId;
9
10
  endpoint: string;
10
11
  tokenValue: string;
11
12
  serverId: string;
12
- aiProfileName?: string | null;
13
- aiProfileToken?: string | null;
14
- aiProfileDescription?: string | null;
15
- aiProfileRole?: string | null;
16
- aiProfileProvider?: string | null;
17
- aiProfileModel?: string | null;
18
- aiProfileSurfaceType?: string | null;
19
13
  }
20
14
  export interface CloudMcpClientIntegration {
21
15
  clientId: McpClientId;
@@ -1,3 +1,4 @@
1
+ import { buildAiProfileBootstrapHeaders } from './aiProfileBootstrap.js';
1
2
  export const MCP_CLIENT_OPTIONS = [
2
3
  { id: 'cursor', label: 'Cursor' },
3
4
  { id: 'cline', label: 'Cline' },
@@ -29,38 +30,18 @@ function buildConnectorDetails(clientId, endpoint) {
29
30
  return endpoint;
30
31
  }
31
32
  function buildCloudMcpHeaders(input) {
32
- const headers = {
33
- Authorization: `Bearer ${input.tokenValue}`
33
+ return {
34
+ Authorization: `Bearer ${input.tokenValue}`,
35
+ ...buildAiProfileBootstrapHeaders(input),
34
36
  };
35
- const aiProfileName = String(input.aiProfileName || '').trim();
36
- const aiProfileToken = String(input.aiProfileToken || '').trim();
37
- const aiProfileDescription = String(input.aiProfileDescription || '').trim();
38
- const aiProfileRole = String(input.aiProfileRole || '').trim();
39
- const aiProfileProvider = String(input.aiProfileProvider || '').trim();
40
- const aiProfileModel = String(input.aiProfileModel || '').trim();
41
- const aiProfileSurfaceType = String(input.aiProfileSurfaceType || '').trim();
42
- if (aiProfileName) {
43
- headers['X-Taskforce-AI-Name'] = aiProfileName;
44
- }
45
- if (aiProfileToken) {
46
- headers['X-Taskforce-AI-Profile-Token'] = aiProfileToken;
47
- }
48
- if (aiProfileDescription) {
49
- headers['X-Taskforce-AI-Description'] = aiProfileDescription;
50
- }
51
- if (aiProfileRole) {
52
- headers['X-Taskforce-AI-Role'] = aiProfileRole;
53
- }
54
- if (aiProfileProvider) {
55
- headers['X-Taskforce-AI-Provider'] = aiProfileProvider;
56
- }
57
- if (aiProfileModel) {
58
- headers['X-Taskforce-AI-Model'] = aiProfileModel;
59
- }
60
- if (aiProfileSurfaceType) {
61
- headers['X-Taskforce-AI-Surface-Type'] = aiProfileSurfaceType;
62
- }
63
- return headers;
37
+ }
38
+ function buildAutomationBootstrapInstruction(baseInstruction) {
39
+ return `${baseInstruction} Keep the generated AI bootstrap headers intact so Taskforce can bind and restore the same AI profile automatically.`;
40
+ }
41
+ function buildConfigurationInstruction(baseInstruction, options) {
42
+ if (options?.usesGeneratedConfig === false)
43
+ return baseInstruction;
44
+ return buildAutomationBootstrapInstruction(`${baseInstruction} Use the generated config exactly as shown.`);
64
45
  }
65
46
  export function buildCloudMcpClientIntegration(input) {
66
47
  const { clientId, endpoint, tokenValue, serverId } = input;
@@ -81,7 +62,7 @@ export function buildCloudMcpClientIntegration(input) {
81
62
  headers
82
63
  },
83
64
  instructionLabel: 'Cursor Configuration Instructions',
84
- instructionText: 'Open Cursor settings, find MCP servers, and paste this config. For automated AI workers, include X-Taskforce-AI-Name and later X-Taskforce-AI-Profile-Token in the headers to let Taskforce bind the same AI profile automatically.'
65
+ instructionText: buildConfigurationInstruction('Open Cursor settings, find MCP servers, and paste this config.')
85
66
  };
86
67
  case 'cline':
87
68
  return {
@@ -94,7 +75,7 @@ export function buildCloudMcpClientIntegration(input) {
94
75
  headers
95
76
  },
96
77
  instructionLabel: 'Cline Configuration Instructions',
97
- instructionText: 'Open Cline MCP Servers, add a remote server, or paste this into the Cline MCP config with Streamable HTTP selected. For automated AI workers, include X-Taskforce-AI-Name and later X-Taskforce-AI-Profile-Token in the headers to let Taskforce bind the same AI profile automatically.'
78
+ instructionText: buildConfigurationInstruction('Open Cline MCP Servers, add a remote server, or paste this into the Cline MCP config with Streamable HTTP selected.')
98
79
  };
99
80
  case 'vscode':
100
81
  return {
@@ -107,7 +88,7 @@ export function buildCloudMcpClientIntegration(input) {
107
88
  headers
108
89
  },
109
90
  instructionLabel: 'VS Code Configuration Instructions',
110
- instructionText: 'Open .vscode/mcp.json or MCP: Open User Configuration and add this server under the servers object. For automated AI workers, include X-Taskforce-AI-Name and later X-Taskforce-AI-Profile-Token in the headers to let Taskforce bind the same AI profile automatically.'
91
+ instructionText: buildConfigurationInstruction('Open .vscode/mcp.json or MCP: Open User Configuration and add this server under the servers object.')
111
92
  };
112
93
  case 'windsurf':
113
94
  return {
@@ -119,7 +100,7 @@ export function buildCloudMcpClientIntegration(input) {
119
100
  headers
120
101
  },
121
102
  instructionLabel: 'Windsurf Configuration Instructions',
122
- instructionText: 'Open Windsurf MCP settings, add a server, and paste this config. For automated AI workers, include X-Taskforce-AI-Name and later X-Taskforce-AI-Profile-Token in the headers to let Taskforce bind the same AI profile automatically.'
103
+ instructionText: buildConfigurationInstruction('Open Windsurf MCP settings, add a server, and paste this config.')
123
104
  };
124
105
  case 'antigravity':
125
106
  return {
@@ -131,7 +112,7 @@ export function buildCloudMcpClientIntegration(input) {
131
112
  headers
132
113
  },
133
114
  instructionLabel: 'Antigravity Configuration Instructions',
134
- instructionText: 'Open Antigravity MCP settings, view the raw config, and paste this Antigravity-specific configuration. For automated AI workers, include X-Taskforce-AI-Name and later X-Taskforce-AI-Profile-Token in the headers to let Taskforce bind the same AI profile automatically.'
115
+ instructionText: buildConfigurationInstruction('Open Antigravity MCP settings, view the raw config, and paste this Antigravity-specific configuration.')
135
116
  };
136
117
  case 'openclaw':
137
118
  return {
@@ -145,7 +126,7 @@ export function buildCloudMcpClientIntegration(input) {
145
126
  headers
146
127
  },
147
128
  instructionLabel: 'OpenClaw Configuration Instructions',
148
- instructionText: 'Open ~/.openclaw/openclaw.json and add this server under mcp.servers. For automated AI workers, include X-Taskforce-AI-Name and later X-Taskforce-AI-Profile-Token in the headers to let Taskforce bind the same AI profile automatically.'
129
+ instructionText: buildConfigurationInstruction('Open ~/.openclaw/openclaw.json and add this server under mcp.servers.')
149
130
  };
150
131
  case 'codex':
151
132
  return {
@@ -157,7 +138,7 @@ export function buildCloudMcpClientIntegration(input) {
157
138
  `http_headers = { ${Object.entries(headers).map(([key, value]) => `${key} = "${value}"`).join(', ')} }`
158
139
  ].join('\n'),
159
140
  instructionLabel: 'Codex Configuration Instructions',
160
- instructionText: 'Open ~/.codex/config.toml and add this Codex MCP entry. For automated AI workers, set X-Taskforce-AI-Name and later X-Taskforce-AI-Profile-Token here so Taskforce binds the same AI profile automatically.'
141
+ instructionText: buildConfigurationInstruction('Open ~/.codex/config.toml and add this Codex MCP entry.')
161
142
  };
162
143
  case 'claude-code':
163
144
  return {
@@ -165,7 +146,7 @@ export function buildCloudMcpClientIntegration(input) {
165
146
  kind: 'text',
166
147
  renderedText: `claude mcp add --transport http ${serverId} ${endpoint} --header "Authorization: Bearer ${tokenValue}"${extraHeaderFlags}`,
167
148
  instructionLabel: 'Claude Code Configuration Instructions',
168
- instructionText: 'Run this Claude Code command in your terminal to add the remote Taskforce MCP server. For automated AI workers, keep the X-Taskforce-AI-Name header and later add X-Taskforce-AI-Profile-Token so Taskforce binds the same AI profile automatically.'
149
+ instructionText: buildConfigurationInstruction('Run this Claude Code command in your terminal to add the remote Taskforce MCP server.')
169
150
  };
170
151
  case 'gemini-cli':
171
152
  return {
@@ -173,7 +154,7 @@ export function buildCloudMcpClientIntegration(input) {
173
154
  kind: 'text',
174
155
  renderedText: `gemini mcp add ${serverId} ${endpoint} --transport http --header "Authorization: Bearer ${tokenValue}"${extraHeaderFlags}`,
175
156
  instructionLabel: 'Gemini CLI Configuration Instructions',
176
- instructionText: 'Run this Gemini CLI command in your terminal to add the remote Taskforce MCP server. Add --scope user if you want it available across all projects. For automated AI workers, keep the X-Taskforce-AI-Name header and later add X-Taskforce-AI-Profile-Token so Taskforce binds the same AI profile automatically.'
157
+ instructionText: buildConfigurationInstruction('Run this Gemini CLI command in your terminal to add the remote Taskforce MCP server. Add --scope user if you want it available across all projects.')
177
158
  };
178
159
  case 'claude-desktop':
179
160
  return {