@taskforcehq/taskforce 0.3.303 → 0.3.304
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Taskforce.module.css +102 -5
- package/dist/TaskforceCore.js +514 -212
- package/dist/TaskforceCore.test.js +1172 -133
- package/dist/annotatedAttachments/service.d.ts +1 -1
- package/dist/annotatedAttachments/types.d.ts +1 -1
- package/dist/components/features/AgentsModule.js +1 -1
- package/dist/components/features/AgentsModule.test.js +1 -1
- package/dist/components/features/TaskSettings.js +58 -38
- package/dist/components/features/TaskSettings.test.js +139 -40
- package/dist/components/task/TaskActionHeader.d.ts +2 -1
- package/dist/components/task/TaskActionHeader.js +3 -3
- package/dist/components/task/TaskActionHeader.test.js +8 -0
- package/dist/components/task/TaskCard.js +16 -1
- package/dist/components/task/TaskCard.test.js +31 -0
- package/dist/components/task/TaskForm.d.ts +2 -1
- package/dist/components/task/TaskForm.js +22 -4
- package/dist/components/task/TaskForm.test.js +46 -0
- package/dist/components/task/TaskKanban.d.ts +1 -1
- package/dist/components/task/TaskKanban.js +31 -160
- package/dist/components/task/TaskStatusActions.d.ts +6 -1
- package/dist/components/task/TaskStatusActions.js +8 -3
- package/dist/components/task/TaskStatusActions.test.js +9 -0
- package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
- package/dist/components/task/taskKanbanDropRules.js +187 -0
- package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
- package/dist/components/task/taskKanbanDropRules.test.js +114 -0
- package/dist/components/ui/Modal.d.ts +1 -1
- package/dist/components/ui/Modal.js +1 -0
- package/dist/components/views/AppShellHeader.js +5 -1
- package/dist/components/views/AppShellHeader.test.js +4 -0
- package/dist/components/views/PlanComparisonPage.d.ts +3 -2
- package/dist/components/views/PlanComparisonPage.js +4 -2
- package/dist/components/views/PlanComparisonPage.test.js +4 -0
- package/dist/components/views/PlansPage.d.ts +5 -1
- package/dist/components/views/PlansPage.js +39 -12
- package/dist/components/views/PlansPage.test.js +59 -2
- package/dist/components/views/StandaloneLayout.js +113 -38
- package/dist/components/views/WidgetView.js +11 -2
- package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
- package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
- package/dist/core/PlanEntitlementService.d.ts +3 -1
- package/dist/core/PlanEntitlementService.js +61 -11
- package/dist/core/SignupMonetizationSettings.test.js +37 -2
- package/dist/core/TaskActivityService.d.ts +4 -1
- package/dist/core/TaskActivityService.js +45 -0
- package/dist/core/TaskAuditTimeline.test.js +52 -1
- package/dist/core/Taskforce.d.ts +15 -11
- package/dist/core/Taskforce.js +43 -2
- package/dist/core/WorkspacePlanMode.test.js +65 -0
- package/dist/documentReviews/service.d.ts +1 -1
- package/dist/documentReviews/types.d.ts +1 -1
- package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
- package/dist/hooks/sync/auth.d.ts +37 -0
- package/dist/hooks/sync/auth.js +61 -0
- package/dist/hooks/sync/bootstrap.d.ts +34 -0
- package/dist/hooks/sync/bootstrap.js +52 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
- package/dist/hooks/sync/orchestratorShared.js +209 -0
- package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
- package/dist/hooks/sync/orchestratorShared.test.js +49 -0
- package/dist/hooks/sync/realtime.d.ts +25 -0
- package/dist/hooks/sync/realtime.js +60 -0
- package/dist/hooks/sync/recovery.d.ts +69 -0
- package/dist/hooks/sync/recovery.js +118 -0
- package/dist/hooks/sync/transfers.d.ts +122 -0
- package/dist/hooks/sync/transfers.js +431 -0
- package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
- package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
- package/dist/hooks/ui/useResourceExport.d.ts +3 -1
- package/dist/hooks/ui/useResourceExport.js +5 -2
- package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
- package/dist/hooks/useRealtimeSync.js +2 -1
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +5 -4
- package/dist/hooks/useSyncOrchestrator.js +314 -873
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +568 -3
- package/dist/hooks/useTaskMutations.js +45 -6
- package/dist/hooks/useTaskMutations.test.js +147 -0
- package/dist/hooks/useTaskforce.d.ts +8 -2
- package/dist/hooks/useTaskforce.js +285 -812
- package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
- package/dist/hooks/useWorkspaceSyncController.js +22 -10
- package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
- package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
- package/dist/hooks/workspace/workspaceLocalState.js +100 -0
- package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
- package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
- package/dist/localization/locales/en-US.js +2 -2
- package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
- package/dist/mcp/canonicalAssetHelpers.js +259 -0
- package/dist/mcp/clientIntegrations.d.ts +12 -1
- package/dist/mcp/clientIntegrations.js +161 -6
- package/dist/mcp/clientIntegrations.test.js +76 -4
- package/dist/mcp/documentHelpers.d.ts +100 -0
- package/dist/mcp/documentHelpers.js +161 -0
- package/dist/mcp/runtime.js +174 -476
- package/dist/mcp/runtime.test.js +81 -8
- package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
- package/dist/mcp/taskAttachmentHelpers.js +60 -0
- package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
- package/dist/mcp/taskAttachmentTypes.js +1 -0
- package/dist/plugins/vite.js +1 -1
- package/dist/runtime/appGateDefinitions.d.ts +1 -1
- package/dist/runtime/appGateDefinitions.js +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +46 -3
- package/dist/server/index.test.js +12 -1
- package/dist/server/routes/admin.js +13 -19
- package/dist/server/routes/auth.js +37 -2
- package/dist/server/routes/billing.js +18 -67
- package/dist/server/routes/billing.test.js +43 -0
- package/dist/server/routes/shared.d.ts +30 -0
- package/dist/server/routes/shared.js +26 -0
- package/dist/server/routes/sync.integration.test.js +20 -0
- package/dist/server/routes/sync.js +110 -237
- package/dist/server/routes/tasks.js +49 -7
- package/dist/server/routes.js +8 -19
- package/dist/server/routes.test.js +243 -5
- package/dist/shared/taskActor.d.ts +10 -0
- package/dist/shared/taskActor.js +1 -0
- package/dist/sync/collaborationSyncPayload.js +4 -0
- package/dist/sync/collaborationSyncPayload.test.js +8 -0
- package/dist/sync/collaborationSyncState.d.ts +3 -1
- package/dist/sync/collaborationSyncState.js +19 -0
- package/dist/sync/contentSyncState.js +12 -10
- package/dist/sync/syncApplyHandlers.d.ts +87 -1
- package/dist/sync/syncApplyHandlers.js +370 -8
- package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
- package/dist/sync/syncApplyHandlers.test.js +377 -0
- package/dist/sync/syncService.d.ts +8 -1
- package/dist/sync/syncService.js +36 -2
- package/dist/sync/syncService.test.js +48 -0
- package/dist/sync/workspacePullFeed.d.ts +1 -0
- package/dist/sync/workspacePullFeed.js +66 -59
- package/dist/sync/workspacePullFeed.test.js +72 -4
- package/dist/sync/workspaceSyncModel.d.ts +22 -1
- package/dist/sync/workspaceSyncModel.js +65 -0
- package/dist/sync/workspaceSyncModel.test.js +119 -0
- package/dist/types.d.ts +2 -10
- package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-C3GyzrWm.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
- package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-BjR6iBf9.js} +1 -1
- package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
- package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
- package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-DnFqdUME.js} +1 -1
- package/dist/ui/assets/index-DXUdtH1B.js +6 -0
- package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
- package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-I7ZwG1z_.js} +1 -1
- package/dist/ui/index.html +4 -4
- package/dist/utils/taskActivity.js +4 -0
- package/dist/utils/taskActivity.test.js +25 -1
- package/package.json +3 -1
- package/scripts/playwright-auth.sh +4 -0
- package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
- package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
- package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
- package/dist/ui/assets/index-CLIMgR6g.js +0 -6
- package/dist/ui/assets/index-DneETxcu.css +0 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { WorkspaceAssetEntry, WorkspaceAssetKind, WorkspaceAssetLinkRole, WorkspaceAssetScanStatus, WorkspaceAssetStore } from '../storage/workspaceAssetStore.js';
|
|
2
|
+
import type { McpTaskAttachmentDescriptor, TaskAttachmentRecord } from './taskAttachmentTypes.js';
|
|
3
|
+
export interface CanonicalAssetHelpersDeps {
|
|
4
|
+
activeWorkspaceId: string;
|
|
5
|
+
projectRoot: string;
|
|
6
|
+
basePath: string;
|
|
7
|
+
workspaceAssetStore: WorkspaceAssetStore | null;
|
|
8
|
+
objectStorageClient: {
|
|
9
|
+
getObject(storageKey: string): Promise<{
|
|
10
|
+
body: Buffer;
|
|
11
|
+
} | null>;
|
|
12
|
+
} | null;
|
|
13
|
+
sanitizeFilenameForPath: (filenameRaw: unknown, fallback?: string) => string;
|
|
14
|
+
sanitizeDisplayName: (valueRaw: unknown, fallback: string) => string;
|
|
15
|
+
toPosixPath: (filePath: string) => string;
|
|
16
|
+
inferMimeTypeFromPath: (filePath: string) => string;
|
|
17
|
+
inferAssetKind: (filePath: string, mimeType: string) => WorkspaceAssetKind;
|
|
18
|
+
buildCanonicalWorkspaceAssetRelativePath: (params: {
|
|
19
|
+
workspaceIdRaw: unknown;
|
|
20
|
+
kind: WorkspaceAssetKind;
|
|
21
|
+
filename: string;
|
|
22
|
+
now?: Date;
|
|
23
|
+
}) => {
|
|
24
|
+
relativePath: string;
|
|
25
|
+
assetId: string;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export declare function createCanonicalAssetHelpers(deps: CanonicalAssetHelpersDeps): {
|
|
29
|
+
normalizeAttachmentStorageKey: (pathValue: unknown, fsPathValue: unknown) => string | null;
|
|
30
|
+
buildSyntheticAttachmentId: (taskId: string, attachment: TaskAttachmentRecord) => string;
|
|
31
|
+
getAttachmentAssetRecord: (attachment: TaskAttachmentRecord) => WorkspaceAssetEntry | null;
|
|
32
|
+
buildAttachmentDownloadUrl: (attachment: McpTaskAttachmentDescriptor) => string;
|
|
33
|
+
buildDocumentDownloadUrl: (asset: WorkspaceAssetEntry) => string;
|
|
34
|
+
readCanonicalAttachmentBuffer: (taskId: string, attachment: McpTaskAttachmentDescriptor) => Promise<Buffer>;
|
|
35
|
+
readCanonicalDocumentBuffer: (asset: WorkspaceAssetEntry) => Promise<Buffer>;
|
|
36
|
+
toTaskAttachmentRecord: (attachment: string | TaskAttachmentRecord) => TaskAttachmentRecord;
|
|
37
|
+
resolveAttachmentLogicalFilename: (attachmentInput: string | TaskAttachmentRecord, assetOverride?: ReturnType<(attachment: TaskAttachmentRecord) => WorkspaceAssetEntry | null> | null) => string;
|
|
38
|
+
resolveCanonicalOverwriteTarget: (task: {
|
|
39
|
+
id: string;
|
|
40
|
+
attachments?: Array<string | TaskAttachmentRecord> | null;
|
|
41
|
+
}, filenameRaw: unknown, describeTaskAttachment: (taskId: string, attachmentInput: string | TaskAttachmentRecord) => McpTaskAttachmentDescriptor) => null | {
|
|
42
|
+
index: number;
|
|
43
|
+
attachment: TaskAttachmentRecord;
|
|
44
|
+
asset: NonNullable<ReturnType<(attachment: TaskAttachmentRecord) => WorkspaceAssetEntry | null>>;
|
|
45
|
+
descriptor: McpTaskAttachmentDescriptor;
|
|
46
|
+
relativePath: string;
|
|
47
|
+
absolutePath: string;
|
|
48
|
+
logicalFilename: string;
|
|
49
|
+
};
|
|
50
|
+
ensureCanonicalTaskAttachment: (relativePathRaw: string, taskIdRaw: string, captionRaw: string | undefined, timestampRaw: string | undefined, role: WorkspaceAssetLinkRole, options?: {
|
|
51
|
+
originalFilename?: string;
|
|
52
|
+
logicalName?: string;
|
|
53
|
+
assetId?: string;
|
|
54
|
+
documentId?: string | null;
|
|
55
|
+
createdAt?: string;
|
|
56
|
+
createdByUserId?: string | null;
|
|
57
|
+
scanStatus?: WorkspaceAssetScanStatus;
|
|
58
|
+
scanEngine?: string | null;
|
|
59
|
+
scanDetails?: string | null;
|
|
60
|
+
}) => WorkspaceAssetEntry | null;
|
|
61
|
+
buildCanonicalWorkspaceAssetRelativePath: (params: {
|
|
62
|
+
workspaceIdRaw: unknown;
|
|
63
|
+
kind: WorkspaceAssetKind;
|
|
64
|
+
filename: string;
|
|
65
|
+
now?: Date;
|
|
66
|
+
}) => {
|
|
67
|
+
relativePath: string;
|
|
68
|
+
assetId: string;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
export function createCanonicalAssetHelpers(deps) {
|
|
5
|
+
const normalizeAttachmentStorageKey = (pathValue, fsPathValue) => {
|
|
6
|
+
const fsPathRaw = String(fsPathValue || '').trim().replace(/\\/g, '/');
|
|
7
|
+
if (fsPathRaw) {
|
|
8
|
+
return fsPathRaw.replace(/^\.taskforce\//, '').trim() || null;
|
|
9
|
+
}
|
|
10
|
+
const pathRaw = String(pathValue || '').trim();
|
|
11
|
+
if (!pathRaw)
|
|
12
|
+
return null;
|
|
13
|
+
if (/^https?:\/\//i.test(pathRaw))
|
|
14
|
+
return pathRaw;
|
|
15
|
+
if (pathRaw.startsWith('/api/taskforce/context/')) {
|
|
16
|
+
return decodeURIComponent(pathRaw.slice('/api/taskforce/context/'.length)).trim() || null;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
};
|
|
20
|
+
const buildSyntheticAttachmentId = (taskId, attachment) => {
|
|
21
|
+
const stableSource = [
|
|
22
|
+
taskId,
|
|
23
|
+
String(attachment.path || '').trim(),
|
|
24
|
+
String(attachment.fsPath || '').trim(),
|
|
25
|
+
String(attachment.caption || '').trim(),
|
|
26
|
+
].join('::');
|
|
27
|
+
return `attachment-${createHash('sha256').update(stableSource).digest('hex').slice(0, 24)}`;
|
|
28
|
+
};
|
|
29
|
+
const getAttachmentAssetRecord = (attachment) => {
|
|
30
|
+
if (!deps.workspaceAssetStore)
|
|
31
|
+
return null;
|
|
32
|
+
const assetId = String(attachment.assetId || '').trim();
|
|
33
|
+
if (assetId) {
|
|
34
|
+
const byId = deps.workspaceAssetStore.get(assetId, deps.activeWorkspaceId);
|
|
35
|
+
if (byId && !byId.deletedAt)
|
|
36
|
+
return byId;
|
|
37
|
+
}
|
|
38
|
+
const storageKey = normalizeAttachmentStorageKey(attachment.path, attachment.fsPath);
|
|
39
|
+
if (!storageKey || /^https?:\/\//i.test(storageKey))
|
|
40
|
+
return null;
|
|
41
|
+
const byStorageKey = deps.workspaceAssetStore.getByStorageKey(storageKey, deps.activeWorkspaceId);
|
|
42
|
+
if (byStorageKey && !byStorageKey.deletedAt)
|
|
43
|
+
return byStorageKey;
|
|
44
|
+
return null;
|
|
45
|
+
};
|
|
46
|
+
const buildAttachmentDownloadUrl = (attachment) => {
|
|
47
|
+
if (attachment.source === 'external')
|
|
48
|
+
return attachment.path;
|
|
49
|
+
const safeName = encodeURIComponent(String(attachment.caption || attachment.displayName || attachment.originalFilename || path.basename(attachment.fsPath || attachment.path) || 'download').trim());
|
|
50
|
+
const joiner = attachment.path.includes('?') ? '&' : '?';
|
|
51
|
+
return `${attachment.path}${joiner}download=1&filename=${safeName}`;
|
|
52
|
+
};
|
|
53
|
+
const buildDocumentDownloadUrl = (asset) => {
|
|
54
|
+
const safeName = deps.sanitizeFilenameForPath(asset.originalFilename
|
|
55
|
+
|| asset.logicalName
|
|
56
|
+
|| path.basename(String(asset.storageKey || '').trim())
|
|
57
|
+
|| 'document.txt', 'document.txt');
|
|
58
|
+
return `/api/taskforce/documents/${encodeURIComponent(String(asset.assetId || '').trim())}/content?download=1&filename=${encodeURIComponent(safeName)}`;
|
|
59
|
+
};
|
|
60
|
+
const describeAttachmentStorageContext = (attachment, storageKey) => {
|
|
61
|
+
const parts = [
|
|
62
|
+
`attachmentId=${attachment.attachmentId}`,
|
|
63
|
+
`assetId=${String(attachment.assetId || 'unknown')}`,
|
|
64
|
+
`storageKey=${storageKey || 'unknown'}`
|
|
65
|
+
];
|
|
66
|
+
return parts.join(', ');
|
|
67
|
+
};
|
|
68
|
+
const readCanonicalAttachmentBuffer = async (taskId, attachment) => {
|
|
69
|
+
const assetId = String(attachment.assetId || '').trim();
|
|
70
|
+
const asset = assetId && deps.workspaceAssetStore
|
|
71
|
+
? deps.workspaceAssetStore.get(assetId, deps.activeWorkspaceId)
|
|
72
|
+
: null;
|
|
73
|
+
if (!asset || asset.deletedAt) {
|
|
74
|
+
throw new Error(`Attachment ${attachment.attachmentId} not found on task ${taskId}`);
|
|
75
|
+
}
|
|
76
|
+
if (asset.scanStatus === 'infected') {
|
|
77
|
+
throw new Error('Attachment is quarantined');
|
|
78
|
+
}
|
|
79
|
+
if (asset.scanStatus === 'pending') {
|
|
80
|
+
throw new Error('Attachment scan pending');
|
|
81
|
+
}
|
|
82
|
+
if (asset.storageProvider === 'r2') {
|
|
83
|
+
const attachmentContext = describeAttachmentStorageContext(attachment, asset.storageKey);
|
|
84
|
+
if (!deps.objectStorageClient) {
|
|
85
|
+
throw new Error(`Attachment read failed from R2 because object storage is not configured in this MCP runtime (${attachmentContext})`);
|
|
86
|
+
}
|
|
87
|
+
let object;
|
|
88
|
+
try {
|
|
89
|
+
object = await deps.objectStorageClient.getObject(asset.storageKey);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
const message = error instanceof Error ? String(error.message || '').trim() : String(error || '').trim();
|
|
93
|
+
throw new Error(`Attachment read failed from R2 (${attachmentContext}): ${message || 'Unknown object storage error'}`);
|
|
94
|
+
}
|
|
95
|
+
if (!object) {
|
|
96
|
+
throw new Error(`Attachment ${attachment.attachmentId} not found on task ${taskId}`);
|
|
97
|
+
}
|
|
98
|
+
return object.body;
|
|
99
|
+
}
|
|
100
|
+
const resolvedPath = path.resolve(deps.basePath, asset.storageKey);
|
|
101
|
+
if (!resolvedPath.startsWith(path.resolve(deps.basePath))) {
|
|
102
|
+
throw new Error(`Attachment ${attachment.attachmentId} not found on task ${taskId}`);
|
|
103
|
+
}
|
|
104
|
+
if (!fs.existsSync(resolvedPath) || !fs.statSync(resolvedPath).isFile()) {
|
|
105
|
+
throw new Error(`Attachment ${attachment.attachmentId} not found on task ${taskId}`);
|
|
106
|
+
}
|
|
107
|
+
return fs.readFileSync(resolvedPath);
|
|
108
|
+
};
|
|
109
|
+
const readCanonicalDocumentBuffer = async (asset) => {
|
|
110
|
+
if (asset.deletedAt || asset.kind !== 'document') {
|
|
111
|
+
throw new Error(`Document ${asset.assetId} not found in workspace ${deps.activeWorkspaceId}.`);
|
|
112
|
+
}
|
|
113
|
+
if (asset.scanStatus === 'infected') {
|
|
114
|
+
throw new Error('Document is quarantined');
|
|
115
|
+
}
|
|
116
|
+
if (asset.scanStatus === 'pending') {
|
|
117
|
+
throw new Error('Document scan pending');
|
|
118
|
+
}
|
|
119
|
+
if (asset.storageProvider === 'r2') {
|
|
120
|
+
if (!deps.objectStorageClient) {
|
|
121
|
+
throw new Error(`Document read failed from R2 because object storage is not configured in this MCP runtime (assetId=${asset.assetId}, storageKey=${asset.storageKey || 'unknown'})`);
|
|
122
|
+
}
|
|
123
|
+
let object;
|
|
124
|
+
try {
|
|
125
|
+
object = await deps.objectStorageClient.getObject(asset.storageKey);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
const message = error instanceof Error ? String(error.message || '').trim() : String(error || '').trim();
|
|
129
|
+
throw new Error(`Document read failed from R2 (assetId=${asset.assetId}, storageKey=${asset.storageKey || 'unknown'}): ${message || 'Unknown object storage error'}`);
|
|
130
|
+
}
|
|
131
|
+
if (!object) {
|
|
132
|
+
throw new Error(`Document ${asset.assetId} not found in workspace ${deps.activeWorkspaceId}.`);
|
|
133
|
+
}
|
|
134
|
+
return object.body;
|
|
135
|
+
}
|
|
136
|
+
const resolvedPath = path.resolve(deps.basePath, asset.storageKey);
|
|
137
|
+
if (!resolvedPath.startsWith(path.resolve(deps.basePath))) {
|
|
138
|
+
throw new Error(`Document ${asset.assetId} not found in workspace ${deps.activeWorkspaceId}.`);
|
|
139
|
+
}
|
|
140
|
+
if (!fs.existsSync(resolvedPath) || !fs.statSync(resolvedPath).isFile()) {
|
|
141
|
+
throw new Error(`Document ${asset.assetId} not found in workspace ${deps.activeWorkspaceId}.`);
|
|
142
|
+
}
|
|
143
|
+
return fs.readFileSync(resolvedPath);
|
|
144
|
+
};
|
|
145
|
+
const toTaskAttachmentRecord = (attachment) => {
|
|
146
|
+
if (typeof attachment === 'string') {
|
|
147
|
+
return { path: attachment };
|
|
148
|
+
}
|
|
149
|
+
return attachment;
|
|
150
|
+
};
|
|
151
|
+
const resolveAttachmentLogicalFilename = (attachmentInput, assetOverride) => {
|
|
152
|
+
const attachment = toTaskAttachmentRecord(attachmentInput);
|
|
153
|
+
const asset = assetOverride ?? getAttachmentAssetRecord(attachment);
|
|
154
|
+
const candidate = String(attachment.originalFilename
|
|
155
|
+
|| asset?.originalFilename
|
|
156
|
+
|| attachment.displayName
|
|
157
|
+
|| asset?.logicalName
|
|
158
|
+
|| '').trim();
|
|
159
|
+
if (candidate) {
|
|
160
|
+
return deps.sanitizeFilenameForPath(candidate);
|
|
161
|
+
}
|
|
162
|
+
const storageKey = normalizeAttachmentStorageKey(attachment.path, attachment.fsPath);
|
|
163
|
+
return deps.sanitizeFilenameForPath(storageKey ? path.basename(storageKey) : '', 'attachment.txt');
|
|
164
|
+
};
|
|
165
|
+
const resolveCanonicalOverwriteTarget = (task, filenameRaw, describeTaskAttachment) => {
|
|
166
|
+
const requestedFilename = deps.sanitizeFilenameForPath(filenameRaw);
|
|
167
|
+
const existingAttachments = Array.isArray(task.attachments) ? task.attachments : [];
|
|
168
|
+
for (let index = 0; index < existingAttachments.length; index++) {
|
|
169
|
+
const attachment = toTaskAttachmentRecord(existingAttachments[index]);
|
|
170
|
+
const asset = getAttachmentAssetRecord(attachment);
|
|
171
|
+
if (!asset || asset.deletedAt || asset.storageProvider === 'external')
|
|
172
|
+
continue;
|
|
173
|
+
const descriptor = describeTaskAttachment(task.id, attachment);
|
|
174
|
+
if (descriptor.source !== 'canonical')
|
|
175
|
+
continue;
|
|
176
|
+
const logicalFilename = resolveAttachmentLogicalFilename(attachment, asset);
|
|
177
|
+
if (logicalFilename !== requestedFilename)
|
|
178
|
+
continue;
|
|
179
|
+
const relativePath = deps.toPosixPath(path.relative(deps.projectRoot, path.resolve(deps.basePath, asset.storageKey)));
|
|
180
|
+
return {
|
|
181
|
+
index,
|
|
182
|
+
attachment,
|
|
183
|
+
asset,
|
|
184
|
+
descriptor,
|
|
185
|
+
relativePath,
|
|
186
|
+
absolutePath: path.resolve(deps.basePath, asset.storageKey),
|
|
187
|
+
logicalFilename,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
};
|
|
192
|
+
const ensureCanonicalTaskAttachment = (relativePathRaw, taskIdRaw, captionRaw, timestampRaw, role, options) => {
|
|
193
|
+
if (!deps.workspaceAssetStore)
|
|
194
|
+
return null;
|
|
195
|
+
const relativePath = String(relativePathRaw || '').trim();
|
|
196
|
+
const taskId = String(taskIdRaw || '').trim();
|
|
197
|
+
if (!relativePath || !taskId)
|
|
198
|
+
return null;
|
|
199
|
+
const absolutePath = path.resolve(deps.projectRoot, relativePath);
|
|
200
|
+
if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile())
|
|
201
|
+
return null;
|
|
202
|
+
const storageKey = relativePath.replace(/^\.taskforce\//, '').trim() || relativePath;
|
|
203
|
+
const buffer = fs.readFileSync(absolutePath);
|
|
204
|
+
const stats = fs.statSync(absolutePath);
|
|
205
|
+
const mimeType = deps.inferMimeTypeFromPath(relativePath);
|
|
206
|
+
const kind = deps.inferAssetKind(relativePath, mimeType);
|
|
207
|
+
const timestamp = String(timestampRaw || new Date().toISOString()).trim() || new Date().toISOString();
|
|
208
|
+
const existing = deps.workspaceAssetStore.getByStorageKey(storageKey, deps.activeWorkspaceId);
|
|
209
|
+
const originalFilename = deps.sanitizeFilenameForPath(options?.originalFilename || existing?.originalFilename || path.basename(relativePath));
|
|
210
|
+
const logicalName = deps.sanitizeDisplayName(options?.logicalName || existing?.logicalName || originalFilename, originalFilename);
|
|
211
|
+
const asset = deps.workspaceAssetStore.upsertAsset({
|
|
212
|
+
assetId: options?.assetId || existing?.assetId || `asset-${randomUUID().replace(/-/g, '')}`,
|
|
213
|
+
workspaceId: deps.activeWorkspaceId,
|
|
214
|
+
kind,
|
|
215
|
+
logicalName,
|
|
216
|
+
originalFilename,
|
|
217
|
+
mimeType,
|
|
218
|
+
storageProvider: 'local',
|
|
219
|
+
storageKey,
|
|
220
|
+
contentSha256: createHash('sha256').update(buffer).digest('hex'),
|
|
221
|
+
sizeBytes: stats.size,
|
|
222
|
+
documentId: kind === 'document' ? (options?.documentId || existing?.documentId || `doc-${randomUUID().replace(/-/g, '')}`) : null,
|
|
223
|
+
version: kind === 'document' ? Math.max(1, Number(existing?.version || 0) + 1) : null,
|
|
224
|
+
isLatest: true,
|
|
225
|
+
scanStatus: options?.scanStatus || existing?.scanStatus || 'clean',
|
|
226
|
+
scanEngine: options?.scanEngine !== undefined ? options.scanEngine : (existing?.scanEngine || null),
|
|
227
|
+
scanDetails: options?.scanDetails !== undefined ? options.scanDetails : (existing?.scanDetails || null),
|
|
228
|
+
createdByUserId: options?.createdByUserId !== undefined ? options.createdByUserId : (existing?.createdByUserId || null),
|
|
229
|
+
createdAt: options?.createdAt || existing?.createdAt || timestamp,
|
|
230
|
+
updatedAt: timestamp,
|
|
231
|
+
deletedAt: null,
|
|
232
|
+
purgeAfter: null
|
|
233
|
+
});
|
|
234
|
+
deps.workspaceAssetStore.upsertLink({
|
|
235
|
+
workspaceId: deps.activeWorkspaceId,
|
|
236
|
+
assetId: asset.assetId,
|
|
237
|
+
taskId,
|
|
238
|
+
role,
|
|
239
|
+
displayName: kind === 'document' ? null : (captionRaw || null),
|
|
240
|
+
updatedAt: timestamp,
|
|
241
|
+
deletedAt: null
|
|
242
|
+
});
|
|
243
|
+
return asset;
|
|
244
|
+
};
|
|
245
|
+
return {
|
|
246
|
+
normalizeAttachmentStorageKey,
|
|
247
|
+
buildSyntheticAttachmentId,
|
|
248
|
+
getAttachmentAssetRecord,
|
|
249
|
+
buildAttachmentDownloadUrl,
|
|
250
|
+
buildDocumentDownloadUrl,
|
|
251
|
+
readCanonicalAttachmentBuffer,
|
|
252
|
+
readCanonicalDocumentBuffer,
|
|
253
|
+
toTaskAttachmentRecord,
|
|
254
|
+
resolveAttachmentLogicalFilename,
|
|
255
|
+
resolveCanonicalOverwriteTarget,
|
|
256
|
+
ensureCanonicalTaskAttachment,
|
|
257
|
+
buildCanonicalWorkspaceAssetRelativePath: deps.buildCanonicalWorkspaceAssetRelativePath,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type AiProfileBootstrapConfig } from './aiProfileBootstrap.js';
|
|
2
|
-
export type McpClientId = 'cursor' | 'cline' | 'vscode' | 'windsurf' | 'antigravity' | 'openclaw' | 'codex' | 'claude-code' | 'gemini-cli' | 'claude-desktop' | 'chatgpt-desktop';
|
|
2
|
+
export type McpClientId = 'general' | 'cursor' | 'cline' | 'vscode' | 'windsurf' | 'antigravity' | 'openclaw' | 'codex' | 'claude-code' | 'gemini-cli' | 'claude-desktop' | 'chatgpt-desktop';
|
|
3
3
|
export interface McpClientOption {
|
|
4
4
|
id: McpClientId;
|
|
5
5
|
label: string;
|
|
@@ -11,6 +11,16 @@ export interface BuildCloudMcpClientConfigInput extends AiProfileBootstrapConfig
|
|
|
11
11
|
tokenValue: string;
|
|
12
12
|
serverId: string;
|
|
13
13
|
}
|
|
14
|
+
export interface LocalMcpServerConfig {
|
|
15
|
+
command: string;
|
|
16
|
+
args: string[];
|
|
17
|
+
env?: Record<string, string>;
|
|
18
|
+
}
|
|
19
|
+
export interface BuildLocalMcpClientConfigInput {
|
|
20
|
+
clientId: McpClientId;
|
|
21
|
+
serverId: string;
|
|
22
|
+
serverConfig: LocalMcpServerConfig;
|
|
23
|
+
}
|
|
14
24
|
export interface CloudMcpClientIntegration {
|
|
15
25
|
clientId: McpClientId;
|
|
16
26
|
kind: 'json' | 'text';
|
|
@@ -25,3 +35,4 @@ export declare const MCP_CLIENT_OPTIONS: McpClientOption[];
|
|
|
25
35
|
export declare function sanitizeMcpId(value: string, fallback: string): string;
|
|
26
36
|
export declare function buildCloudMcpServerId(workspaceName?: string | null, projectName?: string | null): string;
|
|
27
37
|
export declare function buildCloudMcpClientIntegration(input: BuildCloudMcpClientConfigInput): CloudMcpClientIntegration;
|
|
38
|
+
export declare function buildLocalMcpClientIntegration(input: BuildLocalMcpClientConfigInput): CloudMcpClientIntegration;
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { buildAiProfileBootstrapHeaders } from './aiProfileBootstrap.js';
|
|
2
2
|
export const MCP_CLIENT_OPTIONS = [
|
|
3
|
-
{ id: '
|
|
4
|
-
{ id: 'cline', label: 'Cline' },
|
|
5
|
-
{ id: 'vscode', label: 'VS Code / Copilot' },
|
|
6
|
-
{ id: 'windsurf', label: 'Windsurf' },
|
|
3
|
+
{ id: 'general', label: 'General' },
|
|
7
4
|
{ id: 'antigravity', label: 'Antigravity' },
|
|
8
|
-
{ id: 'openclaw', label: 'OpenClaw' },
|
|
9
|
-
{ id: 'codex', label: 'Codex' },
|
|
10
5
|
{ id: 'claude-code', label: 'Claude Code CLI' },
|
|
6
|
+
{ id: 'cline', label: 'Cline' },
|
|
7
|
+
{ id: 'codex', label: 'Codex' },
|
|
8
|
+
{ id: 'cursor', label: 'Cursor' },
|
|
11
9
|
{ id: 'gemini-cli', label: 'Gemini CLI' },
|
|
10
|
+
{ id: 'openclaw', label: 'OpenClaw' },
|
|
11
|
+
{ id: 'vscode', label: 'VS Code / Copilot' },
|
|
12
|
+
{ id: 'windsurf', label: 'Windsurf' },
|
|
12
13
|
];
|
|
13
14
|
export function sanitizeMcpId(value, fallback) {
|
|
14
15
|
const normalized = String(value || '')
|
|
@@ -43,6 +44,27 @@ function buildConfigurationInstruction(baseInstruction, options) {
|
|
|
43
44
|
return baseInstruction;
|
|
44
45
|
return buildAutomationBootstrapInstruction(`${baseInstruction} Use the generated config exactly as shown.`);
|
|
45
46
|
}
|
|
47
|
+
function buildLocalConfigurationInstruction(baseInstruction) {
|
|
48
|
+
return `${baseInstruction} Use the generated config exactly as shown. No token is required for local stdio MCP.`;
|
|
49
|
+
}
|
|
50
|
+
function formatTomlString(value) {
|
|
51
|
+
return JSON.stringify(value);
|
|
52
|
+
}
|
|
53
|
+
function formatTomlArray(values) {
|
|
54
|
+
return `[${values.map((value) => formatTomlString(value)).join(', ')}]`;
|
|
55
|
+
}
|
|
56
|
+
function buildTomlEnvAssignment(env) {
|
|
57
|
+
if (!env || Object.keys(env).length === 0)
|
|
58
|
+
return [];
|
|
59
|
+
return [
|
|
60
|
+
`env = { ${Object.entries(env).map(([key, value]) => `${key} = ${formatTomlString(value)}`).join(', ')} }`
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
function shellEscapeArg(value) {
|
|
64
|
+
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value))
|
|
65
|
+
return value;
|
|
66
|
+
return `'${value.replace(/'/g, `'\"'\"'`)}'`;
|
|
67
|
+
}
|
|
46
68
|
export function buildCloudMcpClientIntegration(input) {
|
|
47
69
|
const { clientId, endpoint, tokenValue, serverId } = input;
|
|
48
70
|
const headers = buildCloudMcpHeaders(input);
|
|
@@ -52,6 +74,18 @@ export function buildCloudMcpClientIntegration(input) {
|
|
|
52
74
|
.map(([key, value]) => ` --header "${key}: ${value}"`)
|
|
53
75
|
.join('');
|
|
54
76
|
switch (clientId) {
|
|
77
|
+
case 'general':
|
|
78
|
+
return {
|
|
79
|
+
clientId,
|
|
80
|
+
kind: 'json',
|
|
81
|
+
rootKey: 'mcpServers',
|
|
82
|
+
serverConfig: {
|
|
83
|
+
url: endpoint,
|
|
84
|
+
headers
|
|
85
|
+
},
|
|
86
|
+
instructionLabel: 'General MCP Configuration Instructions',
|
|
87
|
+
instructionText: buildConfigurationInstruction('Add this server to your MCP client using its standard remote HTTP configuration format.')
|
|
88
|
+
};
|
|
55
89
|
case 'cursor':
|
|
56
90
|
return {
|
|
57
91
|
clientId,
|
|
@@ -178,3 +212,124 @@ export function buildCloudMcpClientIntegration(input) {
|
|
|
178
212
|
}
|
|
179
213
|
}
|
|
180
214
|
}
|
|
215
|
+
export function buildLocalMcpClientIntegration(input) {
|
|
216
|
+
const { clientId, serverId, serverConfig } = input;
|
|
217
|
+
const baseStdioConfig = {
|
|
218
|
+
command: serverConfig.command,
|
|
219
|
+
args: serverConfig.args,
|
|
220
|
+
...(serverConfig.env && Object.keys(serverConfig.env).length > 0 ? { env: serverConfig.env } : {})
|
|
221
|
+
};
|
|
222
|
+
const claudeEnvFlags = Object.entries(serverConfig.env || {})
|
|
223
|
+
.map(([key, value]) => ` --env ${shellEscapeArg(`${key}=${value}`)}`)
|
|
224
|
+
.join('');
|
|
225
|
+
const claudeArgs = [serverConfig.command, ...serverConfig.args].map((value) => shellEscapeArg(value)).join(' ');
|
|
226
|
+
switch (clientId) {
|
|
227
|
+
case 'general':
|
|
228
|
+
return {
|
|
229
|
+
clientId,
|
|
230
|
+
kind: 'json',
|
|
231
|
+
rootKey: 'mcpServers',
|
|
232
|
+
serverConfig: baseStdioConfig,
|
|
233
|
+
instructionLabel: 'General MCP Configuration Instructions',
|
|
234
|
+
instructionText: buildLocalConfigurationInstruction('Add this server to your MCP client using its standard local stdio configuration format.')
|
|
235
|
+
};
|
|
236
|
+
case 'cursor':
|
|
237
|
+
return {
|
|
238
|
+
clientId,
|
|
239
|
+
kind: 'json',
|
|
240
|
+
rootKey: 'mcpServers',
|
|
241
|
+
serverConfig: baseStdioConfig,
|
|
242
|
+
instructionLabel: 'Cursor Configuration Instructions',
|
|
243
|
+
instructionText: buildLocalConfigurationInstruction('Open Cursor settings, find MCP servers, and paste this local stdio config.')
|
|
244
|
+
};
|
|
245
|
+
case 'cline':
|
|
246
|
+
return {
|
|
247
|
+
clientId,
|
|
248
|
+
kind: 'json',
|
|
249
|
+
rootKey: 'mcpServers',
|
|
250
|
+
serverConfig: baseStdioConfig,
|
|
251
|
+
instructionLabel: 'Cline Configuration Instructions',
|
|
252
|
+
instructionText: buildLocalConfigurationInstruction('Open Cline MCP Servers and paste this local stdio server config.')
|
|
253
|
+
};
|
|
254
|
+
case 'vscode':
|
|
255
|
+
return {
|
|
256
|
+
clientId,
|
|
257
|
+
kind: 'json',
|
|
258
|
+
rootKey: 'servers',
|
|
259
|
+
serverConfig: {
|
|
260
|
+
type: 'stdio',
|
|
261
|
+
...baseStdioConfig
|
|
262
|
+
},
|
|
263
|
+
instructionLabel: 'VS Code Configuration Instructions',
|
|
264
|
+
instructionText: buildLocalConfigurationInstruction('Open .vscode/mcp.json or MCP: Open User Configuration and add this server under the servers object.')
|
|
265
|
+
};
|
|
266
|
+
case 'windsurf':
|
|
267
|
+
return {
|
|
268
|
+
clientId,
|
|
269
|
+
kind: 'json',
|
|
270
|
+
rootKey: 'mcpServers',
|
|
271
|
+
serverConfig: baseStdioConfig,
|
|
272
|
+
instructionLabel: 'Windsurf Configuration Instructions',
|
|
273
|
+
instructionText: buildLocalConfigurationInstruction('Open Windsurf MCP settings and paste this local stdio config.')
|
|
274
|
+
};
|
|
275
|
+
case 'antigravity':
|
|
276
|
+
return {
|
|
277
|
+
clientId,
|
|
278
|
+
kind: 'json',
|
|
279
|
+
rootKey: 'mcpServers',
|
|
280
|
+
serverConfig: baseStdioConfig,
|
|
281
|
+
instructionLabel: 'Antigravity Configuration Instructions',
|
|
282
|
+
instructionText: buildLocalConfigurationInstruction('Open Antigravity MCP settings, view the raw config, and paste this local stdio configuration.')
|
|
283
|
+
};
|
|
284
|
+
case 'openclaw':
|
|
285
|
+
return {
|
|
286
|
+
clientId,
|
|
287
|
+
kind: 'json',
|
|
288
|
+
rootKey: 'servers',
|
|
289
|
+
wrapperKeys: ['mcp'],
|
|
290
|
+
serverConfig: {
|
|
291
|
+
transport: 'stdio',
|
|
292
|
+
...baseStdioConfig
|
|
293
|
+
},
|
|
294
|
+
instructionLabel: 'OpenClaw Configuration Instructions',
|
|
295
|
+
instructionText: buildLocalConfigurationInstruction('Open ~/.openclaw/openclaw.json and add this local stdio server under mcp.servers.')
|
|
296
|
+
};
|
|
297
|
+
case 'codex':
|
|
298
|
+
return {
|
|
299
|
+
clientId,
|
|
300
|
+
kind: 'text',
|
|
301
|
+
renderedText: [
|
|
302
|
+
`[mcp_servers.${serverId}]`,
|
|
303
|
+
`command = ${formatTomlString(serverConfig.command)}`,
|
|
304
|
+
`args = ${formatTomlArray(serverConfig.args)}`,
|
|
305
|
+
...buildTomlEnvAssignment(serverConfig.env)
|
|
306
|
+
].join('\n'),
|
|
307
|
+
instructionLabel: 'Codex Configuration Instructions',
|
|
308
|
+
instructionText: buildLocalConfigurationInstruction('Open ~/.codex/config.toml and add this local MCP entry.')
|
|
309
|
+
};
|
|
310
|
+
case 'claude-code':
|
|
311
|
+
return {
|
|
312
|
+
clientId,
|
|
313
|
+
kind: 'text',
|
|
314
|
+
renderedText: `claude mcp add ${serverId}${claudeEnvFlags} -- ${claudeArgs}`,
|
|
315
|
+
instructionLabel: 'Claude Code Configuration Instructions',
|
|
316
|
+
instructionText: buildLocalConfigurationInstruction('Run this Claude Code command in your terminal to add the local Taskforce MCP server.')
|
|
317
|
+
};
|
|
318
|
+
case 'gemini-cli':
|
|
319
|
+
return {
|
|
320
|
+
clientId,
|
|
321
|
+
kind: 'json',
|
|
322
|
+
rootKey: 'mcpServers',
|
|
323
|
+
serverConfig: baseStdioConfig,
|
|
324
|
+
instructionLabel: 'Gemini CLI Configuration Instructions',
|
|
325
|
+
instructionText: buildLocalConfigurationInstruction('Open ~/.gemini/settings.json and add this local MCP server under mcpServers.')
|
|
326
|
+
};
|
|
327
|
+
case 'claude-desktop':
|
|
328
|
+
case 'chatgpt-desktop':
|
|
329
|
+
throw new Error(`Unsupported local MCP client: ${clientId}`);
|
|
330
|
+
default: {
|
|
331
|
+
const exhaustiveCheck = clientId;
|
|
332
|
+
throw new Error(`Unsupported local MCP client: ${exhaustiveCheck}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
@@ -1,13 +1,40 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { MCP_CLIENT_OPTIONS, buildCloudMcpClientIntegration, buildCloudMcpServerId, } from './clientIntegrations';
|
|
2
|
+
import { MCP_CLIENT_OPTIONS, buildCloudMcpClientIntegration, buildLocalMcpClientIntegration, buildCloudMcpServerId, } from './clientIntegrations';
|
|
3
3
|
describe('clientIntegrations', () => {
|
|
4
4
|
it('builds a readable cloud MCP server id from workspace name', () => {
|
|
5
5
|
expect(buildCloudMcpServerId('Workspace Alpha', null)).toBe('taskforce-workspace-alpha');
|
|
6
6
|
expect(buildCloudMcpServerId('A Workspace Name That Is Way Too Long For Comfort', null)).toBe('taskforce-a-workspace-name-that-is');
|
|
7
7
|
});
|
|
8
|
-
it('keeps enabled connector clients available in the selector', () => {
|
|
9
|
-
expect(MCP_CLIENT_OPTIONS.map((option) => option.id)).toEqual(
|
|
10
|
-
|
|
8
|
+
it('keeps enabled connector clients available in the selector in the intended order', () => {
|
|
9
|
+
expect(MCP_CLIENT_OPTIONS.map((option) => option.id)).toEqual([
|
|
10
|
+
'general',
|
|
11
|
+
'antigravity',
|
|
12
|
+
'claude-code',
|
|
13
|
+
'cline',
|
|
14
|
+
'codex',
|
|
15
|
+
'cursor',
|
|
16
|
+
'gemini-cli',
|
|
17
|
+
'openclaw',
|
|
18
|
+
'vscode',
|
|
19
|
+
'windsurf'
|
|
20
|
+
]);
|
|
21
|
+
});
|
|
22
|
+
it('builds generic cloud MCP config for the General option', () => {
|
|
23
|
+
const integration = buildCloudMcpClientIntegration({
|
|
24
|
+
clientId: 'general',
|
|
25
|
+
endpoint: 'https://performance-app.taskforcehq.ai/mcp',
|
|
26
|
+
tokenValue: '<<GENERATE_TOKEN>>',
|
|
27
|
+
serverId: 'taskforce-workspace-alpha',
|
|
28
|
+
});
|
|
29
|
+
expect(integration.kind).toBe('json');
|
|
30
|
+
expect(integration.rootKey).toBe('mcpServers');
|
|
31
|
+
expect(integration.instructionLabel).toBe('General MCP Configuration Instructions');
|
|
32
|
+
expect(integration.serverConfig).toEqual({
|
|
33
|
+
url: 'https://performance-app.taskforcehq.ai/mcp',
|
|
34
|
+
headers: {
|
|
35
|
+
Authorization: 'Bearer <<GENERATE_TOKEN>>'
|
|
36
|
+
}
|
|
37
|
+
});
|
|
11
38
|
});
|
|
12
39
|
it('builds JSON config for VS Code using the servers root', () => {
|
|
13
40
|
const integration = buildCloudMcpClientIntegration({
|
|
@@ -133,4 +160,49 @@ describe('clientIntegrations', () => {
|
|
|
133
160
|
expect(integration.kind).toBe('text');
|
|
134
161
|
expect(integration.renderedText).toBe('gemini mcp add taskforce-workspace-alpha https://performance-app.taskforcehq.ai/mcp --transport http --header "Authorization: Bearer <<GENERATE_TOKEN>>"');
|
|
135
162
|
});
|
|
163
|
+
it('builds local Codex config without token headers', () => {
|
|
164
|
+
const integration = buildLocalMcpClientIntegration({
|
|
165
|
+
clientId: 'codex',
|
|
166
|
+
serverId: 'taskforce-workspace-alpha',
|
|
167
|
+
serverConfig: {
|
|
168
|
+
command: 'taskforce',
|
|
169
|
+
args: ['mcp', '/Users/rossburke/projects/taskforce'],
|
|
170
|
+
env: {
|
|
171
|
+
TASKFORCE_TENANT_ID: 'tenant-alpha',
|
|
172
|
+
TASKFORCE_WORKSPACE_ID: 'workspace-alpha'
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
expect(integration.kind).toBe('text');
|
|
177
|
+
expect(integration.renderedText).toContain('[mcp_servers.taskforce-workspace-alpha]');
|
|
178
|
+
expect(integration.renderedText).toContain('command = "taskforce"');
|
|
179
|
+
expect(integration.renderedText).toContain('args = ["mcp", "/Users/rossburke/projects/taskforce"]');
|
|
180
|
+
expect(integration.renderedText).toContain('TASKFORCE_TENANT_ID = "tenant-alpha"');
|
|
181
|
+
expect(integration.renderedText).not.toContain('Authorization');
|
|
182
|
+
});
|
|
183
|
+
it('builds local VS Code config with stdio transport', () => {
|
|
184
|
+
const integration = buildLocalMcpClientIntegration({
|
|
185
|
+
clientId: 'vscode',
|
|
186
|
+
serverId: 'taskforce-workspace-alpha',
|
|
187
|
+
serverConfig: {
|
|
188
|
+
command: 'taskforce',
|
|
189
|
+
args: ['mcp', '/Users/rossburke/projects/taskforce'],
|
|
190
|
+
env: {
|
|
191
|
+
TASKFORCE_TENANT_ID: 'tenant-alpha',
|
|
192
|
+
TASKFORCE_WORKSPACE_ID: 'workspace-alpha'
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
expect(integration.kind).toBe('json');
|
|
197
|
+
expect(integration.rootKey).toBe('servers');
|
|
198
|
+
expect(integration.serverConfig).toEqual({
|
|
199
|
+
type: 'stdio',
|
|
200
|
+
command: 'taskforce',
|
|
201
|
+
args: ['mcp', '/Users/rossburke/projects/taskforce'],
|
|
202
|
+
env: {
|
|
203
|
+
TASKFORCE_TENANT_ID: 'tenant-alpha',
|
|
204
|
+
TASKFORCE_WORKSPACE_ID: 'workspace-alpha'
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
});
|
|
136
208
|
});
|