@taskforcehq/taskforce 0.3.302 → 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 +113 -5
- package/dist/TaskforceCore.js +560 -208
- package/dist/TaskforceCore.test.js +1833 -91
- 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/TaskKanban.test.js +17 -0
- 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 +7 -1
- package/dist/components/views/PlansPage.js +139 -53
- package/dist/components/views/PlansPage.test.d.ts +1 -0
- package/dist/components/views/PlansPage.test.js +157 -0
- package/dist/components/views/StandaloneLayout.d.ts +1 -0
- package/dist/components/views/StandaloneLayout.js +216 -186
- package/dist/components/views/WidgetView.js +11 -2
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +8 -3
- package/dist/components/views/standalone/modals/AccountHubModal.js +16 -2
- package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
- package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
- package/dist/core/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +78 -18
- package/dist/core/PlanEntitlementService.d.ts +31 -5
- package/dist/core/PlanEntitlementService.js +350 -222
- package/dist/core/SignupMonetizationSettings.test.js +208 -49
- package/dist/core/SystemAdmin.test.js +186 -96
- 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 +65 -20
- package/dist/core/Taskforce.js +261 -51
- package/dist/core/Taskforce.mcpAuth.test.js +0 -2
- package/dist/core/UserProfileAvatarDrafts.test.js +12 -3
- package/dist/core/WorkspacePlanMode.test.js +65 -0
- package/dist/core/types.d.ts +11 -12
- 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 +315 -860
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +665 -8
- package/dist/hooks/useTaskMutations.js +45 -6
- package/dist/hooks/useTaskMutations.test.js +147 -0
- package/dist/hooks/useTaskforce.d.ts +11 -2
- package/dist/hooks/useTaskforce.js +290 -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 +264 -505
- package/dist/mcp/runtime.test.js +353 -89
- 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/migrations/billingSchemaParity.test.js +3 -0
- package/dist/migrations/taskSchemaMigrations.js +15 -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.cookieProxy.test.js +1 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +48 -3
- package/dist/server/index.test.js +14 -2
- package/dist/server/mockStripe.d.ts +1 -0
- package/dist/server/mockStripe.js +2 -0
- package/dist/server/routes/admin.js +91 -40
- package/dist/server/routes/auth.d.ts +1 -0
- package/dist/server/routes/auth.js +231 -55
- package/dist/server/routes/billing.js +473 -39
- package/dist/server/routes/billing.test.js +972 -37
- package/dist/server/routes/shared.d.ts +30 -0
- package/dist/server/routes/shared.js +31 -3
- package/dist/server/routes/sync.integration.test.js +180 -0
- package/dist/server/routes/sync.js +125 -242
- package/dist/server/routes/tasks.js +49 -7
- package/dist/server/routes.js +11 -22
- package/dist/server/routes.test.js +530 -81
- package/dist/shared/taskActor.d.ts +10 -0
- package/dist/shared/taskActor.js +1 -0
- package/dist/sync/cloudSyncApi.d.ts +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.d.ts +10 -0
- package/dist/sync/contentSyncState.js +277 -14
- 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 +9 -1
- package/dist/sync/syncService.js +57 -10
- package/dist/sync/syncService.test.js +48 -0
- package/dist/sync/workspacePullFeed.d.ts +11 -0
- package/dist/sync/workspacePullFeed.js +123 -68
- package/dist/sync/workspacePullFeed.test.js +180 -0
- 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-Dvi02Y7q.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-iys3zfca.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
- package/dist/ui/assets/{InitiativesModule-DJvLJI3A.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-BguHeQaV.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-C8EED3Oh.js → vendor-icons-I7ZwG1z_.js} +1 -1
- package/dist/ui/index.html +4 -4
- package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
- package/dist/utils/accountProfileSummaryCache.js +97 -0
- package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
- package/dist/utils/accountProfileSummaryCache.test.js +63 -0
- package/dist/utils/taskActivity.js +4 -0
- package/dist/utils/taskActivity.test.js +25 -1
- package/package.json +5 -3
- package/scripts/playwright-auth.sh +4 -0
- package/dist/ui/assets/AgentsModule-JG5jc3he.js +0 -1
- package/dist/ui/assets/PlansPage-BMPlpFpw.js +0 -1
- package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +0 -8
- package/dist/ui/assets/index-MXeWoebC.css +0 -1
- package/dist/ui/assets/index-YJ4k5QNa.js +0 -6
- package/dist/utils/billingStatusCache.d.ts +0 -21
- package/dist/utils/billingStatusCache.js +0 -70
package/dist/mcp/runtime.js
CHANGED
|
@@ -3,10 +3,14 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
3
3
|
import { CallToolRequestSchema, ListResourceTemplatesRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, SubscribeRequestSchema, UnsubscribeRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
4
|
import * as fs from 'fs';
|
|
5
5
|
import * as path from 'path';
|
|
6
|
-
import {
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import * as os from 'node:os';
|
|
7
8
|
import { TaskforceCore, TaskforceRuleError } from '../core/Taskforce.js'; // Note .js extension for ESM
|
|
8
9
|
import { COMPLEXITY_OPTIONS, resolveNumberOption, resolveStringOption, STATUS_OPTIONS } from './optionResolution.js';
|
|
9
10
|
import { buildResolveAiProfileInputFromBootstrap } from './aiProfileBootstrap.js';
|
|
11
|
+
import { createCanonicalAssetHelpers } from './canonicalAssetHelpers.js';
|
|
12
|
+
import { createDocumentHelpers } from './documentHelpers.js';
|
|
13
|
+
import { createTaskAttachmentHelpers } from './taskAttachmentHelpers.js';
|
|
10
14
|
import { validateStructuredDocForAttach } from './structuredDocValidation.js';
|
|
11
15
|
import { validateStructuredCommentForAdd } from './structuredCommentValidation.js';
|
|
12
16
|
import { resolveDatabaseConfigFromEnv } from '../storage/providerConfig.js';
|
|
@@ -282,300 +286,38 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
282
286
|
? new ObjectStorageClient(objectStorageConfig)
|
|
283
287
|
: null;
|
|
284
288
|
const { basePath } = core.getPaths();
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
318
|
-
const storageKey = normalizeAttachmentStorageKey(attachment.path, attachment.fsPath);
|
|
319
|
-
if (!storageKey || /^https?:\/\//i.test(storageKey))
|
|
320
|
-
return null;
|
|
321
|
-
const byStorageKey = workspaceAssetStore.getByStorageKey(storageKey, ACTIVE_WORKSPACE_ID);
|
|
322
|
-
if (byStorageKey && !byStorageKey.deletedAt)
|
|
323
|
-
return byStorageKey;
|
|
324
|
-
return null;
|
|
325
|
-
};
|
|
326
|
-
const isCanonicalDocumentAssetRecord = (asset) => isValidCanonicalDocumentAsset(asset);
|
|
327
|
-
const toTaskAttachmentRecord = (attachment) => {
|
|
328
|
-
if (typeof attachment === 'string') {
|
|
329
|
-
return { path: attachment };
|
|
330
|
-
}
|
|
331
|
-
return attachment;
|
|
332
|
-
};
|
|
333
|
-
const describeTaskAttachment = (taskId, attachmentInput) => {
|
|
334
|
-
const attachment = toTaskAttachmentRecord(attachmentInput);
|
|
335
|
-
const asset = getAttachmentAssetRecord(attachment);
|
|
336
|
-
const storageKey = normalizeAttachmentStorageKey(attachment.path, attachment.fsPath);
|
|
337
|
-
const assetId = String(attachment.assetId || asset?.assetId || '').trim() || undefined;
|
|
338
|
-
const source = (asset?.storageProvider === 'external' || /^https?:\/\//i.test(String(attachment.path || '').trim()))
|
|
339
|
-
? 'external'
|
|
340
|
-
: 'canonical';
|
|
341
|
-
const mimeType = String(asset?.mimeType
|
|
342
|
-
|| (storageKey && !/^https?:\/\//i.test(storageKey) ? inferMimeTypeFromPath(storageKey) : '')
|
|
343
|
-
|| inferMimeTypeFromPath(String(attachment.originalFilename || attachment.displayName || attachment.caption || attachment.path || ''))).trim() || 'application/octet-stream';
|
|
344
|
-
const kind = asset?.kind || inferAssetKind(storageKey || String(attachment.path || ''), mimeType);
|
|
345
|
-
const isCanonicalDocument = Boolean(asset && isCanonicalDocumentAssetRecord(asset)) || (!asset && kind === 'document');
|
|
346
|
-
const referenceNumber = typeof attachment.referenceNumber === 'number'
|
|
347
|
-
? attachment.referenceNumber
|
|
348
|
-
: (typeof asset?.referenceNumber === 'number' ? asset.referenceNumber : null);
|
|
349
|
-
const referenceLabel = String(attachment.referenceLabel
|
|
350
|
-
|| (kind === 'image' ? getImageReferenceLabel(asset) : '')
|
|
351
|
-
|| (isCanonicalDocument ? getDocumentReferenceLabel(asset) : '')).trim() || null;
|
|
352
|
-
const readableAsText = source === 'canonical' && isTextReadableMimeType(mimeType);
|
|
353
|
-
const canonicalDocumentHint = isCanonicalDocument && source === 'canonical' && asset
|
|
354
|
-
? buildCanonicalDocumentAttachmentHint(asset)
|
|
355
|
-
: null;
|
|
356
|
-
return {
|
|
357
|
-
attachmentId: assetId || buildSyntheticAttachmentId(taskId, attachment),
|
|
358
|
-
...(assetId ? { assetId } : {}),
|
|
359
|
-
...(referenceNumber ? { referenceNumber } : {}),
|
|
360
|
-
...(referenceLabel ? { referenceLabel } : {}),
|
|
361
|
-
...(canonicalDocumentHint?.documentId ? { documentId: canonicalDocumentHint.documentId } : {}),
|
|
362
|
-
path: String(attachment.path || '').trim(),
|
|
363
|
-
...(storageKey && !/^https?:\/\//i.test(storageKey) ? { fsPath: storageKey } : (attachment.fsPath ? { fsPath: String(attachment.fsPath).trim() } : {})),
|
|
364
|
-
...(attachment.caption ? { caption: String(attachment.caption).trim() } : {}),
|
|
365
|
-
...(attachment.displayName ? { displayName: String(attachment.displayName).trim() } : {}),
|
|
366
|
-
...(attachment.originalFilename ? { originalFilename: String(attachment.originalFilename).trim() } : {}),
|
|
367
|
-
kind: isCanonicalDocument ? 'document' : kind,
|
|
368
|
-
mimeType,
|
|
369
|
-
sizeBytes: asset ? Number(asset.sizeBytes || 0) : null,
|
|
370
|
-
...(attachment.timestamp ? { timestamp: String(attachment.timestamp).trim() } : {}),
|
|
371
|
-
source,
|
|
372
|
-
readableAsText,
|
|
373
|
-
...(canonicalDocumentHint ? {
|
|
374
|
-
preferredNextTool: canonicalDocumentHint.preferredNextTool,
|
|
375
|
-
preferredWorkflow: canonicalDocumentHint.preferredWorkflow,
|
|
376
|
-
suggestedArgs: canonicalDocumentHint.suggestedArgs,
|
|
377
|
-
guidance: canonicalDocumentHint.guidance,
|
|
378
|
-
} : {}),
|
|
379
|
-
};
|
|
380
|
-
};
|
|
381
|
-
const listTaskAttachmentDescriptors = (task) => {
|
|
382
|
-
if (!task || !Array.isArray(task.attachments))
|
|
383
|
-
return [];
|
|
384
|
-
return task.attachments.map((attachment) => describeTaskAttachment(task.id, attachment));
|
|
385
|
-
};
|
|
386
|
-
const buildAttachmentDownloadUrl = (attachment) => {
|
|
387
|
-
if (attachment.source === 'external')
|
|
388
|
-
return attachment.path;
|
|
389
|
-
const safeName = encodeURIComponent(String(attachment.caption || attachment.displayName || attachment.originalFilename || path.basename(attachment.fsPath || attachment.path) || 'download').trim());
|
|
390
|
-
const joiner = attachment.path.includes('?') ? '&' : '?';
|
|
391
|
-
return `${attachment.path}${joiner}download=1&filename=${safeName}`;
|
|
392
|
-
};
|
|
393
|
-
const describeAttachmentStorageContext = (attachment, storageKey) => {
|
|
394
|
-
const parts = [
|
|
395
|
-
`attachmentId=${attachment.attachmentId}`,
|
|
396
|
-
`assetId=${String(attachment.assetId || 'unknown')}`,
|
|
397
|
-
`storageKey=${storageKey || 'unknown'}`
|
|
398
|
-
];
|
|
399
|
-
return parts.join(', ');
|
|
400
|
-
};
|
|
401
|
-
const readCanonicalAttachmentBuffer = async (taskId, attachment) => {
|
|
402
|
-
const assetId = String(attachment.assetId || '').trim();
|
|
403
|
-
const asset = assetId && workspaceAssetStore
|
|
404
|
-
? workspaceAssetStore.get(assetId, ACTIVE_WORKSPACE_ID)
|
|
405
|
-
: null;
|
|
406
|
-
if (!asset || asset.deletedAt) {
|
|
407
|
-
throw new Error(`Attachment ${attachment.attachmentId} not found on task ${taskId}`);
|
|
408
|
-
}
|
|
409
|
-
if (asset.scanStatus === 'infected') {
|
|
410
|
-
throw new Error('Attachment is quarantined');
|
|
411
|
-
}
|
|
412
|
-
if (asset.scanStatus === 'pending') {
|
|
413
|
-
throw new Error('Attachment scan pending');
|
|
414
|
-
}
|
|
415
|
-
if (asset.storageProvider === 'r2') {
|
|
416
|
-
const attachmentContext = describeAttachmentStorageContext(attachment, asset.storageKey);
|
|
417
|
-
if (!objectStorageClient) {
|
|
418
|
-
throw new Error(`Attachment read failed from R2 because object storage is not configured in this MCP runtime (${attachmentContext})`);
|
|
419
|
-
}
|
|
420
|
-
let object;
|
|
421
|
-
try {
|
|
422
|
-
object = await objectStorageClient.getObject(asset.storageKey);
|
|
423
|
-
}
|
|
424
|
-
catch (error) {
|
|
425
|
-
const message = error instanceof Error ? String(error.message || '').trim() : String(error || '').trim();
|
|
426
|
-
throw new Error(`Attachment read failed from R2 (${attachmentContext}): ${message || 'Unknown object storage error'}`);
|
|
427
|
-
}
|
|
428
|
-
if (!object) {
|
|
429
|
-
throw new Error(`Attachment ${attachment.attachmentId} not found on task ${taskId}`);
|
|
430
|
-
}
|
|
431
|
-
return object.body;
|
|
432
|
-
}
|
|
433
|
-
const resolvedPath = path.resolve(basePath, asset.storageKey);
|
|
434
|
-
if (!resolvedPath.startsWith(path.resolve(basePath))) {
|
|
435
|
-
throw new Error(`Attachment ${attachment.attachmentId} not found on task ${taskId}`);
|
|
436
|
-
}
|
|
437
|
-
if (!fs.existsSync(resolvedPath) || !fs.statSync(resolvedPath).isFile()) {
|
|
438
|
-
throw new Error(`Attachment ${attachment.attachmentId} not found on task ${taskId}`);
|
|
439
|
-
}
|
|
440
|
-
return fs.readFileSync(resolvedPath);
|
|
441
|
-
};
|
|
442
|
-
const readCanonicalDocumentBuffer = async (asset) => {
|
|
443
|
-
if (asset.deletedAt || asset.kind !== 'document') {
|
|
444
|
-
throw new Error(`Document ${asset.assetId} not found in workspace ${ACTIVE_WORKSPACE_ID}.`);
|
|
445
|
-
}
|
|
446
|
-
if (asset.scanStatus === 'infected') {
|
|
447
|
-
throw new Error('Document is quarantined');
|
|
448
|
-
}
|
|
449
|
-
if (asset.scanStatus === 'pending') {
|
|
450
|
-
throw new Error('Document scan pending');
|
|
451
|
-
}
|
|
452
|
-
if (asset.storageProvider === 'r2') {
|
|
453
|
-
if (!objectStorageClient) {
|
|
454
|
-
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'})`);
|
|
455
|
-
}
|
|
456
|
-
let object;
|
|
457
|
-
try {
|
|
458
|
-
object = await objectStorageClient.getObject(asset.storageKey);
|
|
459
|
-
}
|
|
460
|
-
catch (error) {
|
|
461
|
-
const message = error instanceof Error ? String(error.message || '').trim() : String(error || '').trim();
|
|
462
|
-
throw new Error(`Document read failed from R2 (assetId=${asset.assetId}, storageKey=${asset.storageKey || 'unknown'}): ${message || 'Unknown object storage error'}`);
|
|
463
|
-
}
|
|
464
|
-
if (!object) {
|
|
465
|
-
throw new Error(`Document ${asset.assetId} not found in workspace ${ACTIVE_WORKSPACE_ID}.`);
|
|
466
|
-
}
|
|
467
|
-
return object.body;
|
|
468
|
-
}
|
|
469
|
-
const resolvedPath = path.resolve(basePath, asset.storageKey);
|
|
470
|
-
if (!resolvedPath.startsWith(path.resolve(basePath))) {
|
|
471
|
-
throw new Error(`Document ${asset.assetId} not found in workspace ${ACTIVE_WORKSPACE_ID}.`);
|
|
472
|
-
}
|
|
473
|
-
if (!fs.existsSync(resolvedPath) || !fs.statSync(resolvedPath).isFile()) {
|
|
474
|
-
throw new Error(`Document ${asset.assetId} not found in workspace ${ACTIVE_WORKSPACE_ID}.`);
|
|
475
|
-
}
|
|
476
|
-
return fs.readFileSync(resolvedPath);
|
|
477
|
-
};
|
|
478
|
-
const buildDocumentDownloadUrl = (asset) => {
|
|
479
|
-
const safeName = sanitizeFilenameForPath(asset.originalFilename
|
|
480
|
-
|| asset.logicalName
|
|
481
|
-
|| path.basename(String(asset.storageKey || '').trim())
|
|
482
|
-
|| 'document.txt', 'document.txt');
|
|
483
|
-
return `/api/taskforce/documents/${encodeURIComponent(String(asset.assetId || '').trim())}/content?download=1&filename=${encodeURIComponent(safeName)}`;
|
|
484
|
-
};
|
|
485
|
-
const resolveAttachmentLogicalFilename = (attachmentInput, assetOverride) => {
|
|
486
|
-
const attachment = toTaskAttachmentRecord(attachmentInput);
|
|
487
|
-
const asset = assetOverride ?? getAttachmentAssetRecord(attachment);
|
|
488
|
-
const candidate = String(attachment.originalFilename
|
|
489
|
-
|| asset?.originalFilename
|
|
490
|
-
|| attachment.displayName
|
|
491
|
-
|| asset?.logicalName
|
|
492
|
-
|| '').trim();
|
|
493
|
-
if (candidate) {
|
|
494
|
-
return sanitizeFilenameForPath(candidate);
|
|
495
|
-
}
|
|
496
|
-
const storageKey = normalizeAttachmentStorageKey(attachment.path, attachment.fsPath);
|
|
497
|
-
return sanitizeFilenameForPath(storageKey ? path.basename(storageKey) : '', 'attachment.txt');
|
|
498
|
-
};
|
|
499
|
-
const resolveCanonicalOverwriteTarget = (task, filenameRaw) => {
|
|
500
|
-
const requestedFilename = sanitizeFilenameForPath(filenameRaw);
|
|
501
|
-
const existingAttachments = Array.isArray(task.attachments) ? task.attachments : [];
|
|
502
|
-
for (let index = 0; index < existingAttachments.length; index++) {
|
|
503
|
-
const attachment = toTaskAttachmentRecord(existingAttachments[index]);
|
|
504
|
-
const asset = getAttachmentAssetRecord(attachment);
|
|
505
|
-
if (!asset || asset.deletedAt || asset.storageProvider === 'external')
|
|
506
|
-
continue;
|
|
507
|
-
const descriptor = describeTaskAttachment(task.id, attachment);
|
|
508
|
-
if (descriptor.source !== 'canonical')
|
|
509
|
-
continue;
|
|
510
|
-
const logicalFilename = resolveAttachmentLogicalFilename(attachment, asset);
|
|
511
|
-
if (logicalFilename !== requestedFilename)
|
|
512
|
-
continue;
|
|
513
|
-
const relativePath = toPosixPath(path.relative(PROJECT_ROOT, path.resolve(basePath, asset.storageKey)));
|
|
514
|
-
return {
|
|
515
|
-
index,
|
|
516
|
-
attachment,
|
|
517
|
-
asset,
|
|
518
|
-
descriptor,
|
|
519
|
-
relativePath,
|
|
520
|
-
absolutePath: path.resolve(basePath, asset.storageKey),
|
|
521
|
-
logicalFilename,
|
|
522
|
-
};
|
|
523
|
-
}
|
|
524
|
-
return null;
|
|
525
|
-
};
|
|
526
|
-
const ensureCanonicalTaskAttachment = (relativePathRaw, taskIdRaw, captionRaw, timestampRaw, role, options) => {
|
|
527
|
-
if (!workspaceAssetStore)
|
|
528
|
-
return null;
|
|
529
|
-
const relativePath = String(relativePathRaw || '').trim();
|
|
530
|
-
const taskId = String(taskIdRaw || '').trim();
|
|
531
|
-
if (!relativePath || !taskId)
|
|
532
|
-
return null;
|
|
533
|
-
const absolutePath = path.resolve(PROJECT_ROOT, relativePath);
|
|
534
|
-
if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile())
|
|
535
|
-
return null;
|
|
536
|
-
const storageKey = relativePath.replace(/^\.taskforce\//, '').trim() || relativePath;
|
|
537
|
-
const buffer = fs.readFileSync(absolutePath);
|
|
538
|
-
const stats = fs.statSync(absolutePath);
|
|
539
|
-
const mimeType = inferMimeTypeFromPath(relativePath);
|
|
540
|
-
const kind = inferAssetKind(relativePath, mimeType);
|
|
541
|
-
const timestamp = String(timestampRaw || new Date().toISOString()).trim() || new Date().toISOString();
|
|
542
|
-
const existing = workspaceAssetStore.getByStorageKey(storageKey, ACTIVE_WORKSPACE_ID);
|
|
543
|
-
const originalFilename = sanitizeFilenameForPath(options?.originalFilename || existing?.originalFilename || path.basename(relativePath));
|
|
544
|
-
const logicalName = sanitizeDisplayName(options?.logicalName || existing?.logicalName || originalFilename, originalFilename);
|
|
545
|
-
const asset = workspaceAssetStore.upsertAsset({
|
|
546
|
-
assetId: options?.assetId || existing?.assetId || `asset-${randomUUID().replace(/-/g, '')}`,
|
|
547
|
-
workspaceId: ACTIVE_WORKSPACE_ID,
|
|
548
|
-
kind,
|
|
549
|
-
logicalName,
|
|
550
|
-
originalFilename,
|
|
551
|
-
mimeType,
|
|
552
|
-
storageProvider: 'local',
|
|
553
|
-
storageKey,
|
|
554
|
-
contentSha256: createHash('sha256').update(buffer).digest('hex'),
|
|
555
|
-
sizeBytes: stats.size,
|
|
556
|
-
documentId: kind === 'document' ? (options?.documentId || existing?.documentId || `doc-${randomUUID().replace(/-/g, '')}`) : null,
|
|
557
|
-
version: kind === 'document' ? Math.max(1, Number(existing?.version || 0) + 1) : null,
|
|
558
|
-
isLatest: true,
|
|
559
|
-
scanStatus: options?.scanStatus || existing?.scanStatus || 'clean',
|
|
560
|
-
scanEngine: options?.scanEngine !== undefined ? options.scanEngine : (existing?.scanEngine || null),
|
|
561
|
-
scanDetails: options?.scanDetails !== undefined ? options.scanDetails : (existing?.scanDetails || null),
|
|
562
|
-
createdByUserId: options?.createdByUserId !== undefined ? options.createdByUserId : (existing?.createdByUserId || null),
|
|
563
|
-
createdAt: options?.createdAt || existing?.createdAt || timestamp,
|
|
564
|
-
updatedAt: timestamp,
|
|
565
|
-
deletedAt: null,
|
|
566
|
-
purgeAfter: null
|
|
567
|
-
});
|
|
568
|
-
workspaceAssetStore.upsertLink({
|
|
569
|
-
workspaceId: ACTIVE_WORKSPACE_ID,
|
|
570
|
-
assetId: asset.assetId,
|
|
571
|
-
taskId,
|
|
572
|
-
role,
|
|
573
|
-
displayName: kind === 'document' ? null : (captionRaw || null),
|
|
574
|
-
updatedAt: timestamp,
|
|
575
|
-
deletedAt: null
|
|
576
|
-
});
|
|
577
|
-
return asset;
|
|
578
|
-
};
|
|
289
|
+
const documentHelpers = createDocumentHelpers({
|
|
290
|
+
activeWorkspaceId: ACTIVE_WORKSPACE_ID,
|
|
291
|
+
workspaceAssetStore,
|
|
292
|
+
getDocumentReferenceLabel,
|
|
293
|
+
parseDocumentReference,
|
|
294
|
+
isTextReadableMimeType,
|
|
295
|
+
});
|
|
296
|
+
const canonicalAssetHelpers = createCanonicalAssetHelpers({
|
|
297
|
+
activeWorkspaceId: ACTIVE_WORKSPACE_ID,
|
|
298
|
+
projectRoot: PROJECT_ROOT,
|
|
299
|
+
basePath,
|
|
300
|
+
workspaceAssetStore,
|
|
301
|
+
objectStorageClient,
|
|
302
|
+
sanitizeFilenameForPath,
|
|
303
|
+
sanitizeDisplayName,
|
|
304
|
+
toPosixPath,
|
|
305
|
+
inferMimeTypeFromPath,
|
|
306
|
+
inferAssetKind,
|
|
307
|
+
buildCanonicalWorkspaceAssetRelativePath,
|
|
308
|
+
});
|
|
309
|
+
const taskAttachmentHelpers = createTaskAttachmentHelpers({
|
|
310
|
+
activeWorkspaceId: ACTIVE_WORKSPACE_ID,
|
|
311
|
+
workspaceAssetStore,
|
|
312
|
+
getImageReferenceLabel,
|
|
313
|
+
getDocumentReferenceLabel,
|
|
314
|
+
inferMimeTypeFromPath,
|
|
315
|
+
inferAssetKind,
|
|
316
|
+
isTextReadableMimeType,
|
|
317
|
+
isValidCanonicalDocumentAsset,
|
|
318
|
+
canonicalAssetHelpers,
|
|
319
|
+
documentHelpers,
|
|
320
|
+
});
|
|
579
321
|
const server = new Server({
|
|
580
322
|
name: options.serverName || `taskforce-${PROJECT_NAME}`,
|
|
581
323
|
version: MCP_SERVER_VERSION,
|
|
@@ -669,75 +411,9 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
669
411
|
}
|
|
670
412
|
return { id: task.id, task };
|
|
671
413
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
documentId: asset.documentId ?? null,
|
|
676
|
-
referenceNumber: typeof asset.referenceNumber === 'number' ? asset.referenceNumber : null,
|
|
677
|
-
referenceLabel: getDocumentReferenceLabel(asset),
|
|
678
|
-
logicalName: asset.logicalName ?? null,
|
|
679
|
-
originalFilename: asset.originalFilename ?? null,
|
|
680
|
-
version: typeof asset.version === 'number' ? asset.version : null,
|
|
681
|
-
storageKey: asset.storageKey ?? null,
|
|
682
|
-
mimeType: asset.mimeType ?? null,
|
|
683
|
-
sizeBytes: typeof asset.sizeBytes === 'number' ? asset.sizeBytes : null,
|
|
684
|
-
updatedAt: asset.updatedAt ?? null,
|
|
685
|
-
apiUrl: `/api/taskforce/documents/${encodeURIComponent(String(asset.assetId || '').trim())}/content`,
|
|
686
|
-
};
|
|
687
|
-
}
|
|
688
|
-
function buildDocumentReadHint(asset) {
|
|
689
|
-
const preferredIdentifier = getDocumentReferenceLabel(asset)
|
|
690
|
-
|| (typeof asset.documentId === 'string' && asset.documentId.trim().length > 0 ? asset.documentId.trim() : null)
|
|
691
|
-
|| String(asset.assetId || '').trim();
|
|
692
|
-
const readable = isTextReadableMimeType(asset.mimeType || '');
|
|
693
|
-
if (readable) {
|
|
694
|
-
return {
|
|
695
|
-
available: true,
|
|
696
|
-
nextTool: 'read_document_content',
|
|
697
|
-
suggestedArgs: { documentId: preferredIdentifier },
|
|
698
|
-
guidance: 'To read the document body, call read_document_content with this documentId.',
|
|
699
|
-
reviewNext: {
|
|
700
|
-
helperTool: 'review_document',
|
|
701
|
-
listTool: 'get_document_review_sessions',
|
|
702
|
-
createTool: 'create_document_review_session',
|
|
703
|
-
commentTool: 'add_document_review_comment',
|
|
704
|
-
guidance: 'After reading the document body, prefer review_document for a lower-friction review flow, or use get_document_review_sessions/create_document_review_session and add_document_review_comment directly.'
|
|
705
|
-
}
|
|
706
|
-
};
|
|
707
|
-
}
|
|
708
|
-
return {
|
|
709
|
-
available: false,
|
|
710
|
-
nextTool: 'read_document_content',
|
|
711
|
-
suggestedArgs: { documentId: preferredIdentifier },
|
|
712
|
-
guidance: 'This document is not inline-readable as text. Call read_document_content to get the structured download/reference response.',
|
|
713
|
-
reviewNext: {
|
|
714
|
-
helperTool: 'review_document',
|
|
715
|
-
listTool: 'get_document_review_sessions',
|
|
716
|
-
createTool: 'create_document_review_session',
|
|
717
|
-
commentTool: 'add_document_review_comment',
|
|
718
|
-
guidance: 'After inspecting the document access response, prefer review_document for a lower-friction review flow, or use get_document_review_sessions/create_document_review_session and add_document_review_comment directly.'
|
|
719
|
-
},
|
|
720
|
-
reason: 'binary_file',
|
|
721
|
-
};
|
|
722
|
-
}
|
|
723
|
-
function buildCanonicalDocumentAttachmentHint(asset) {
|
|
724
|
-
const preferredIdentifier = getDocumentReferenceLabel(asset)
|
|
725
|
-
|| (typeof asset.documentId === 'string' && asset.documentId.trim().length > 0 ? asset.documentId.trim() : null)
|
|
726
|
-
|| String(asset.assetId || '').trim();
|
|
727
|
-
if (!preferredIdentifier) {
|
|
728
|
-
return null;
|
|
729
|
-
}
|
|
730
|
-
const readable = isTextReadableMimeType(asset.mimeType || '');
|
|
731
|
-
return {
|
|
732
|
-
documentId: typeof asset.documentId === 'string' && asset.documentId.trim().length > 0 ? asset.documentId.trim() : null,
|
|
733
|
-
preferredNextTool: 'get_document',
|
|
734
|
-
preferredWorkflow: 'canonical_document',
|
|
735
|
-
suggestedArgs: { documentId: preferredIdentifier },
|
|
736
|
-
guidance: readable
|
|
737
|
-
? 'This attachment is a canonical document. Prefer get_document, then read_document_content, for ongoing document work and review.'
|
|
738
|
-
: 'This attachment is a canonical document. Prefer get_document for metadata and review context, then read_document_content for the structured content/download response.',
|
|
739
|
-
};
|
|
740
|
-
}
|
|
414
|
+
const buildDocumentDescriptor = documentHelpers.buildDocumentDescriptor;
|
|
415
|
+
const buildDocumentReadHint = documentHelpers.buildDocumentReadHint;
|
|
416
|
+
const buildCanonicalDocumentAttachmentHint = documentHelpers.buildCanonicalDocumentAttachmentHint;
|
|
741
417
|
function searchImageAssets(queryRaw, options) {
|
|
742
418
|
const query = typeof queryRaw === 'string' ? queryRaw.trim().toLowerCase() : '';
|
|
743
419
|
if (!query) {
|
|
@@ -796,65 +472,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
796
472
|
.sort((a, b) => b.score - a.score || String(b.asset.updatedAt || '').localeCompare(String(a.asset.updatedAt || '')))
|
|
797
473
|
.slice(0, limit);
|
|
798
474
|
}
|
|
799
|
-
|
|
800
|
-
const query = typeof queryRaw === 'string' ? queryRaw.trim().toLowerCase() : '';
|
|
801
|
-
if (!query) {
|
|
802
|
-
throw new Error('query is required');
|
|
803
|
-
}
|
|
804
|
-
if (!workspaceAssetStore) {
|
|
805
|
-
throw new Error('Workspace asset store unavailable.');
|
|
806
|
-
}
|
|
807
|
-
const requestedLimit = Number(options?.limit);
|
|
808
|
-
const limit = Number.isFinite(requestedLimit)
|
|
809
|
-
? Math.max(1, Math.min(50, Math.trunc(requestedLimit)))
|
|
810
|
-
: 10;
|
|
811
|
-
const normalize = (value) => typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
812
|
-
const tokenize = (value) => normalize(value).split(/[^a-z0-9]+/i).filter(Boolean);
|
|
813
|
-
const allDocuments = workspaceAssetStore.listAllByWorkspace(ACTIVE_WORKSPACE_ID, { kind: 'document' });
|
|
814
|
-
const scoredMatches = allDocuments.flatMap((asset) => {
|
|
815
|
-
if (asset.deletedAt)
|
|
816
|
-
return [];
|
|
817
|
-
const fields = [
|
|
818
|
-
{ label: 'referenceLabel', value: getDocumentReferenceLabel(asset) || '' },
|
|
819
|
-
{ label: 'documentId', value: asset.documentId || '' },
|
|
820
|
-
{ label: 'logicalName', value: asset.logicalName || '' },
|
|
821
|
-
{ label: 'originalFilename', value: asset.originalFilename || '' },
|
|
822
|
-
{ label: 'assetId', value: asset.assetId || '' },
|
|
823
|
-
];
|
|
824
|
-
let score = 0;
|
|
825
|
-
const matchedOn = new Set();
|
|
826
|
-
for (const field of fields) {
|
|
827
|
-
const normalized = normalize(field.value);
|
|
828
|
-
if (!normalized)
|
|
829
|
-
continue;
|
|
830
|
-
if (normalized === query) {
|
|
831
|
-
score += field.label === 'referenceLabel' || field.label === 'documentId' ? 120 : 90;
|
|
832
|
-
matchedOn.add(field.label);
|
|
833
|
-
continue;
|
|
834
|
-
}
|
|
835
|
-
if (normalized.startsWith(query)) {
|
|
836
|
-
score += field.label === 'logicalName' || field.label === 'originalFilename' ? 60 : 50;
|
|
837
|
-
matchedOn.add(field.label);
|
|
838
|
-
continue;
|
|
839
|
-
}
|
|
840
|
-
if (normalized.includes(query)) {
|
|
841
|
-
score += 35;
|
|
842
|
-
matchedOn.add(field.label);
|
|
843
|
-
continue;
|
|
844
|
-
}
|
|
845
|
-
if (tokenize(field.value).some((token) => token.startsWith(query))) {
|
|
846
|
-
score += 20;
|
|
847
|
-
matchedOn.add(field.label);
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
if (score <= 0)
|
|
851
|
-
return [];
|
|
852
|
-
return [{ asset, matchedOn: Array.from(matchedOn), score }];
|
|
853
|
-
});
|
|
854
|
-
return scoredMatches
|
|
855
|
-
.sort((a, b) => b.score - a.score || String(b.asset.updatedAt || '').localeCompare(String(a.asset.updatedAt || '')))
|
|
856
|
-
.slice(0, limit);
|
|
857
|
-
}
|
|
475
|
+
const searchDocumentAssets = documentHelpers.searchDocumentAssets;
|
|
858
476
|
function buildImageDescriptor(asset) {
|
|
859
477
|
return {
|
|
860
478
|
assetId: asset.assetId,
|
|
@@ -869,32 +487,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
869
487
|
apiUrl: `/api/taskforce/context/${encodeURIComponent(String(asset.storageKey || '').trim())}`,
|
|
870
488
|
};
|
|
871
489
|
}
|
|
872
|
-
|
|
873
|
-
const normalizedValue = typeof documentIdentifier === 'string' ? documentIdentifier.trim() : '';
|
|
874
|
-
if (!normalizedValue) {
|
|
875
|
-
throw new Error('documentId is required');
|
|
876
|
-
}
|
|
877
|
-
if (!workspaceAssetStore) {
|
|
878
|
-
throw new Error('Workspace asset store unavailable.');
|
|
879
|
-
}
|
|
880
|
-
const matchesDocumentAsset = (asset) => Boolean(asset && asset.kind === 'document' && asset.deletedAt == null);
|
|
881
|
-
const directAsset = workspaceAssetStore.get(normalizedValue, ACTIVE_WORKSPACE_ID);
|
|
882
|
-
if (matchesDocumentAsset(directAsset)) {
|
|
883
|
-
return directAsset;
|
|
884
|
-
}
|
|
885
|
-
const documentAssets = workspaceAssetStore.listAllByWorkspace(ACTIVE_WORKSPACE_ID, { kind: 'document' });
|
|
886
|
-
const referenceNumber = parseDocumentReference(normalizedValue);
|
|
887
|
-
const matchedAsset = documentAssets.find((asset) => asset.documentId === normalizedValue)
|
|
888
|
-
|| (referenceNumber
|
|
889
|
-
? documentAssets.find((asset) => asset.referenceNumber === referenceNumber && asset.isLatest)
|
|
890
|
-
|| documentAssets.find((asset) => asset.referenceNumber === referenceNumber)
|
|
891
|
-
: null);
|
|
892
|
-
if (matchesDocumentAsset(matchedAsset)) {
|
|
893
|
-
return matchedAsset;
|
|
894
|
-
}
|
|
895
|
-
throw new Error(options?.notFoundMessage
|
|
896
|
-
|| `Document ${normalizedValue} not found in workspace ${ACTIVE_WORKSPACE_ID}. If you only know a keyword, filename, or rough title, call search_documents first.`);
|
|
897
|
-
}
|
|
490
|
+
const resolveDocumentAssetByIdentifierOrThrow = documentHelpers.resolveDocumentAssetByIdentifierOrThrow;
|
|
898
491
|
function resolveImageAssetByIdentifierOrThrow(imageIdentifier, options) {
|
|
899
492
|
const normalizedValue = typeof imageIdentifier === 'string' ? imageIdentifier.trim() : '';
|
|
900
493
|
if (!normalizedValue) {
|
|
@@ -1419,13 +1012,91 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
1419
1012
|
},
|
|
1420
1013
|
});
|
|
1421
1014
|
}
|
|
1015
|
+
function logMcpStructuredEvent(level, event, details) {
|
|
1016
|
+
const payload = {
|
|
1017
|
+
event,
|
|
1018
|
+
runtimeMode,
|
|
1019
|
+
workspaceId: ACTIVE_WORKSPACE_ID,
|
|
1020
|
+
requestId: auditRequestId,
|
|
1021
|
+
...details,
|
|
1022
|
+
};
|
|
1023
|
+
const logger = level === 'error'
|
|
1024
|
+
? console.error
|
|
1025
|
+
: level === 'warn'
|
|
1026
|
+
? console.warn
|
|
1027
|
+
: console.info;
|
|
1028
|
+
logger(`[Taskforce MCP] ${JSON.stringify(payload)}`);
|
|
1029
|
+
}
|
|
1030
|
+
function logMcpReadObservation(toolName, startedAtMs, details = {}) {
|
|
1031
|
+
const durationMs = Date.now() - startedAtMs;
|
|
1032
|
+
const shouldLog = runtimeMode === 'cloud'
|
|
1033
|
+
? toolName === 'get_current_profile' || toolName === 'list_tasks' || durationMs >= 150
|
|
1034
|
+
: durationMs >= 250;
|
|
1035
|
+
if (!shouldLog)
|
|
1036
|
+
return;
|
|
1037
|
+
logMcpStructuredEvent('info', 'tool_read_complete', {
|
|
1038
|
+
toolName,
|
|
1039
|
+
durationMs,
|
|
1040
|
+
...details,
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1422
1043
|
/**
|
|
1423
1044
|
* Helpers
|
|
1424
1045
|
*/
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1046
|
+
function listExistingAttachmentSourceRoots(candidateRoots) {
|
|
1047
|
+
const seen = new Set();
|
|
1048
|
+
const results = [];
|
|
1049
|
+
for (const candidate of candidateRoots) {
|
|
1050
|
+
const trimmed = String(candidate || '').trim();
|
|
1051
|
+
if (!trimmed)
|
|
1052
|
+
continue;
|
|
1053
|
+
const resolved = path.resolve(trimmed);
|
|
1054
|
+
if (!fs.existsSync(resolved))
|
|
1055
|
+
continue;
|
|
1056
|
+
let realPath;
|
|
1057
|
+
try {
|
|
1058
|
+
realPath = fs.realpathSync(resolved);
|
|
1059
|
+
}
|
|
1060
|
+
catch {
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
if (!fs.statSync(realPath).isDirectory())
|
|
1064
|
+
continue;
|
|
1065
|
+
if (seen.has(realPath))
|
|
1066
|
+
continue;
|
|
1067
|
+
seen.add(realPath);
|
|
1068
|
+
results.push(realPath);
|
|
1069
|
+
}
|
|
1070
|
+
return results;
|
|
1071
|
+
}
|
|
1072
|
+
function listConfiguredAttachmentSourceRoots() {
|
|
1073
|
+
const configured = String(process.env.TASKFORCE_ALLOWED_ATTACHMENT_SOURCE_ROOTS || '').trim();
|
|
1074
|
+
if (!configured)
|
|
1075
|
+
return [];
|
|
1076
|
+
return listExistingAttachmentSourceRoots(configured
|
|
1077
|
+
.split(path.delimiter)
|
|
1078
|
+
.map((entry) => entry.trim())
|
|
1079
|
+
.filter(Boolean));
|
|
1080
|
+
}
|
|
1081
|
+
function listKnownAgentWorkspaceRoots() {
|
|
1082
|
+
const openClawRoot = path.join(os.homedir(), '.openclaw');
|
|
1083
|
+
if (!fs.existsSync(openClawRoot))
|
|
1084
|
+
return [];
|
|
1085
|
+
const entries = fs.readdirSync(openClawRoot, { withFileTypes: true });
|
|
1086
|
+
const workspaceRoots = entries
|
|
1087
|
+
.filter((entry) => entry.isDirectory() && /^workspace-/i.test(entry.name))
|
|
1088
|
+
.map((entry) => path.join(openClawRoot, entry.name));
|
|
1089
|
+
return listExistingAttachmentSourceRoots(workspaceRoots);
|
|
1090
|
+
}
|
|
1091
|
+
function getApprovedAttachmentSourceRoots(projectRootReal) {
|
|
1092
|
+
return listExistingAttachmentSourceRoots([
|
|
1093
|
+
projectRootReal,
|
|
1094
|
+
path.resolve(basePath, 'tmp'),
|
|
1095
|
+
path.resolve(basePath, 'artifacts'),
|
|
1096
|
+
...listConfiguredAttachmentSourceRoots(),
|
|
1097
|
+
...listKnownAgentWorkspaceRoots(),
|
|
1098
|
+
]);
|
|
1099
|
+
}
|
|
1429
1100
|
function renderTaskTable(tasks) {
|
|
1430
1101
|
if (tasks.length === 0)
|
|
1431
1102
|
return "No tasks found.";
|
|
@@ -1458,9 +1129,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
1458
1129
|
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
1459
1130
|
}
|
|
1460
1131
|
function isApprovedAttachmentSourcePath(filePath, projectRootReal) {
|
|
1461
|
-
|
|
1462
|
-
return true;
|
|
1463
|
-
return taskforceManagedAttachmentSourceRoots.some((root) => isWithinRoot(filePath, root));
|
|
1132
|
+
return getApprovedAttachmentSourceRoots(projectRootReal).some((root) => isWithinRoot(filePath, root));
|
|
1464
1133
|
}
|
|
1465
1134
|
function toPosixPath(filePath) {
|
|
1466
1135
|
return filePath.split(path.sep).join('/');
|
|
@@ -1664,7 +1333,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
1664
1333
|
console.error(`[Taskforce MCP] Failed to send resources/updated for ${uri}: ${error.message}`);
|
|
1665
1334
|
}
|
|
1666
1335
|
}
|
|
1667
|
-
|
|
1336
|
+
function collectResourceMutationUris(options) {
|
|
1668
1337
|
const uris = new Set();
|
|
1669
1338
|
if (options.includeConfig)
|
|
1670
1339
|
uris.add(RESOURCE_CONFIG_URI);
|
|
@@ -1691,9 +1360,47 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
1691
1360
|
uris.add(workstreamUriFor(workstreamId));
|
|
1692
1361
|
}
|
|
1693
1362
|
}
|
|
1363
|
+
return uris;
|
|
1364
|
+
}
|
|
1365
|
+
const pendingResourceInvalidations = new Set();
|
|
1366
|
+
let resourceInvalidationFlushHandle = null;
|
|
1367
|
+
function scheduleResourceInvalidationFlush() {
|
|
1368
|
+
if (resourceInvalidationFlushHandle)
|
|
1369
|
+
return;
|
|
1370
|
+
resourceInvalidationFlushHandle = setTimeout(() => {
|
|
1371
|
+
resourceInvalidationFlushHandle = null;
|
|
1372
|
+
void flushScheduledResourceInvalidations();
|
|
1373
|
+
}, 0);
|
|
1374
|
+
}
|
|
1375
|
+
async function flushScheduledResourceInvalidations() {
|
|
1376
|
+
if (pendingResourceInvalidations.size === 0)
|
|
1377
|
+
return;
|
|
1378
|
+
const startedAtMs = Date.now();
|
|
1379
|
+
const uris = Array.from(pendingResourceInvalidations);
|
|
1380
|
+
pendingResourceInvalidations.clear();
|
|
1694
1381
|
for (const uri of uris) {
|
|
1695
1382
|
await notifyResourceUpdatedIfSubscribed(uri);
|
|
1696
1383
|
}
|
|
1384
|
+
const durationMs = Date.now() - startedAtMs;
|
|
1385
|
+
if (runtimeMode === 'cloud' || durationMs >= 100 || uris.length >= 3) {
|
|
1386
|
+
logMcpStructuredEvent('info', 'resource_invalidation_flush', {
|
|
1387
|
+
uriCount: uris.length,
|
|
1388
|
+
durationMs,
|
|
1389
|
+
uris,
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
function scheduleResourceMutation(options) {
|
|
1394
|
+
const uris = collectResourceMutationUris(options);
|
|
1395
|
+
if (uris.size === 0)
|
|
1396
|
+
return;
|
|
1397
|
+
for (const uri of uris) {
|
|
1398
|
+
pendingResourceInvalidations.add(uri);
|
|
1399
|
+
}
|
|
1400
|
+
scheduleResourceInvalidationFlush();
|
|
1401
|
+
}
|
|
1402
|
+
async function notifyResourceMutation(options) {
|
|
1403
|
+
scheduleResourceMutation(options);
|
|
1697
1404
|
}
|
|
1698
1405
|
function buildConfigResourcePayload() {
|
|
1699
1406
|
return {
|
|
@@ -1716,13 +1423,26 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
1716
1423
|
};
|
|
1717
1424
|
}
|
|
1718
1425
|
function buildTasksResourcePayload() {
|
|
1719
|
-
const tasks = core.listTasks(ACTIVE_WORKSPACE_ID).tasks
|
|
1426
|
+
const tasks = core.listTasks(ACTIVE_WORKSPACE_ID).tasks
|
|
1427
|
+
.map((task) => buildPlanningTaskSummary(task));
|
|
1720
1428
|
return {
|
|
1721
1429
|
total: tasks.length,
|
|
1722
1430
|
tasks,
|
|
1723
1431
|
};
|
|
1724
1432
|
}
|
|
1725
|
-
function
|
|
1433
|
+
function buildPlanningTaskSummary(task) {
|
|
1434
|
+
const { description, comments, taskEvents, activity, createdByActor, assigneeActor, attachments, checklistItems, ...rest } = task;
|
|
1435
|
+
return rest;
|
|
1436
|
+
}
|
|
1437
|
+
function buildPlanningWorkstreamSummary(workstream) {
|
|
1438
|
+
const { description, taskIds, ...rest } = workstream;
|
|
1439
|
+
return rest;
|
|
1440
|
+
}
|
|
1441
|
+
function buildPlanningInitiativeSummary(initiative) {
|
|
1442
|
+
const { description, ...rest } = initiative;
|
|
1443
|
+
return rest;
|
|
1444
|
+
}
|
|
1445
|
+
function buildHistoricalWorkspaceTaskSnapshot() {
|
|
1726
1446
|
const activeTasks = core.listTasks(ACTIVE_WORKSPACE_ID).tasks;
|
|
1727
1447
|
const archivedTasks = core.listArchive(ACTIVE_WORKSPACE_ID).archived;
|
|
1728
1448
|
const byId = new Map();
|
|
@@ -1730,40 +1450,70 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
1730
1450
|
byId.set(task.id, task);
|
|
1731
1451
|
for (const task of archivedTasks)
|
|
1732
1452
|
byId.set(task.id, task);
|
|
1733
|
-
|
|
1453
|
+
const tasks = Array.from(byId.values());
|
|
1454
|
+
const tasksByWorkstreamId = new Map();
|
|
1455
|
+
for (const task of tasks) {
|
|
1456
|
+
const normalizedWorkstreamId = String(task.workstreamId || '').trim();
|
|
1457
|
+
if (!normalizedWorkstreamId)
|
|
1458
|
+
continue;
|
|
1459
|
+
const current = tasksByWorkstreamId.get(normalizedWorkstreamId) || [];
|
|
1460
|
+
current.push(buildPlanningTaskSummary(task));
|
|
1461
|
+
tasksByWorkstreamId.set(normalizedWorkstreamId, current);
|
|
1462
|
+
}
|
|
1463
|
+
return {
|
|
1464
|
+
tasks,
|
|
1465
|
+
tasksByWorkstreamId,
|
|
1466
|
+
};
|
|
1734
1467
|
}
|
|
1735
|
-
function buildInitiativeResourcePayload(initiative) {
|
|
1736
|
-
const
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1468
|
+
function buildInitiativeResourcePayload(initiative, options) {
|
|
1469
|
+
const tasksByWorkstreamId = options?.tasksByWorkstreamId || buildHistoricalWorkspaceTaskSnapshot().tasksByWorkstreamId;
|
|
1470
|
+
const workstreams = options?.workstreamsByInitiativeId?.get(initiative.id)
|
|
1471
|
+
|| core.listWorkstreams(ACTIVE_WORKSPACE_ID)
|
|
1472
|
+
.filter((workstream) => workstream.initiativeId === initiative.id);
|
|
1473
|
+
const tasks = workstreams.flatMap((workstream) => tasksByWorkstreamId.get(workstream.id) || []);
|
|
1740
1474
|
return {
|
|
1741
|
-
initiative,
|
|
1475
|
+
initiative: buildPlanningInitiativeSummary(initiative),
|
|
1742
1476
|
workstreamCount: workstreams.length,
|
|
1743
1477
|
taskCount: tasks.length,
|
|
1744
|
-
workstreams,
|
|
1478
|
+
workstreams: workstreams.map((workstream) => buildPlanningWorkstreamSummary(workstream)),
|
|
1745
1479
|
};
|
|
1746
1480
|
}
|
|
1747
|
-
function buildWorkstreamResourcePayload(workstream) {
|
|
1748
|
-
const
|
|
1749
|
-
|
|
1481
|
+
function buildWorkstreamResourcePayload(workstream, options) {
|
|
1482
|
+
const tasksByWorkstreamId = options?.tasksByWorkstreamId || buildHistoricalWorkspaceTaskSnapshot().tasksByWorkstreamId;
|
|
1483
|
+
const tasks = tasksByWorkstreamId.get(workstream.id)
|
|
1484
|
+
|| [];
|
|
1750
1485
|
return {
|
|
1751
|
-
workstream,
|
|
1486
|
+
workstream: buildPlanningWorkstreamSummary(workstream),
|
|
1752
1487
|
taskCount: tasks.length,
|
|
1753
1488
|
tasks,
|
|
1754
1489
|
};
|
|
1755
1490
|
}
|
|
1756
1491
|
function buildInitiativesResourcePayload() {
|
|
1492
|
+
const workstreams = core.listWorkstreams(ACTIVE_WORKSPACE_ID);
|
|
1493
|
+
const workstreamsByInitiativeId = new Map();
|
|
1494
|
+
for (const workstream of workstreams) {
|
|
1495
|
+
const normalizedInitiativeId = String(workstream.initiativeId || '').trim();
|
|
1496
|
+
if (!normalizedInitiativeId)
|
|
1497
|
+
continue;
|
|
1498
|
+
const current = workstreamsByInitiativeId.get(normalizedInitiativeId) || [];
|
|
1499
|
+
current.push(workstream);
|
|
1500
|
+
workstreamsByInitiativeId.set(normalizedInitiativeId, current);
|
|
1501
|
+
}
|
|
1502
|
+
const { tasksByWorkstreamId } = buildHistoricalWorkspaceTaskSnapshot();
|
|
1757
1503
|
const initiatives = core.listInitiatives(ACTIVE_WORKSPACE_ID)
|
|
1758
|
-
.map((initiative) => buildInitiativeResourcePayload(initiative
|
|
1504
|
+
.map((initiative) => buildInitiativeResourcePayload(initiative, {
|
|
1505
|
+
workstreamsByInitiativeId,
|
|
1506
|
+
tasksByWorkstreamId,
|
|
1507
|
+
}));
|
|
1759
1508
|
return {
|
|
1760
1509
|
total: initiatives.length,
|
|
1761
1510
|
initiatives,
|
|
1762
1511
|
};
|
|
1763
1512
|
}
|
|
1764
1513
|
function buildWorkstreamsResourcePayload() {
|
|
1514
|
+
const { tasksByWorkstreamId } = buildHistoricalWorkspaceTaskSnapshot();
|
|
1765
1515
|
const workstreams = core.listWorkstreams(ACTIVE_WORKSPACE_ID)
|
|
1766
|
-
.map((workstream) => buildWorkstreamResourcePayload(workstream));
|
|
1516
|
+
.map((workstream) => buildWorkstreamResourcePayload(workstream, { tasksByWorkstreamId }));
|
|
1767
1517
|
return {
|
|
1768
1518
|
total: workstreams.length,
|
|
1769
1519
|
workstreams,
|
|
@@ -2987,7 +2737,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
2987
2737
|
{
|
|
2988
2738
|
name: "save_task_attachment",
|
|
2989
2739
|
description: describeTool(runtimeMode === 'cloud'
|
|
2990
|
-
? "Save generated content into a task attachment and link it to the task. Use this when an agent creates notes, reports, or artifacts that should live with the task instead of remaining only in chat. For cloud MCP, AI-authored markdown/text
|
|
2740
|
+
? "Save generated content into a task attachment and link it to the task. Use this when an agent creates notes, reports, or artifacts that should live with the task instead of remaining only in chat. For cloud MCP, use inline content for AI-authored markdown/text, or use sourcePath when the file already exists inside the repo, a Taskforce-managed attachment source, or an approved agent workspace root. When overwrite=true, this replaces an existing canonical attachment with the same logical filename in place. External URL attachments are never overwrite targets; if no matching canonical attachment exists, a new attachment is created."
|
|
2991
2741
|
: "Save generated content or copy a file into a task-specific attachment and link it to the task. Use this when an agent creates notes, reports, or artifacts that should live with the task instead of remaining only in chat. When overwrite=true, this replaces an existing canonical attachment with the same logical filename in place. External URL attachments are never overwrite targets; if no matching canonical attachment exists, a new attachment is created."),
|
|
2992
2742
|
inputSchema: {
|
|
2993
2743
|
type: "object",
|
|
@@ -2995,7 +2745,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
2995
2745
|
id: { type: "string", description: "Task ID" },
|
|
2996
2746
|
filename: { type: "string", description: "Target filename for the task attachment." },
|
|
2997
2747
|
content: { type: "string", description: runtimeMode === 'cloud' ? "Text/markdown file content to save. For cloud MCP, use this for AI-authored markdown/text attachments." : "Text/markdown file content to save. Provide either content or sourcePath." },
|
|
2998
|
-
sourcePath: { type: "string", description: runtimeMode === 'cloud' ? "Project-relative or absolute source file path to copy
|
|
2748
|
+
sourcePath: { type: "string", description: runtimeMode === 'cloud' ? "Project-relative or absolute source file path to copy. In cloud MCP this must resolve inside the repo, a Taskforce-managed attachment source, or an approved agent workspace root. Use content instead for AI-authored markdown/text." : "Project-relative or absolute source file path to copy. Provide either sourcePath or content." },
|
|
2999
2749
|
caption: { type: "string", description: "Optional label shown in task context" },
|
|
3000
2750
|
overwrite: { type: "boolean", description: "Overwrite the destination file when it already exists (default: false)." }
|
|
3001
2751
|
},
|
|
@@ -3230,18 +2980,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
3230
2980
|
}
|
|
3231
2981
|
}
|
|
3232
2982
|
};
|
|
3233
|
-
}).filter((tool) =>
|
|
3234
|
-
if (!hasPermission(TOOL_PERMISSIONS[tool.name]))
|
|
3235
|
-
return false;
|
|
3236
|
-
const taskforceToolsEnv = process.env.TASKFORCE_TOOLS;
|
|
3237
|
-
if (taskforceToolsEnv) {
|
|
3238
|
-
const enabledTools = taskforceToolsEnv.split(',').map(t => t.trim()).filter(Boolean);
|
|
3239
|
-
if (enabledTools.length > 0 && !enabledTools.includes(tool.name)) {
|
|
3240
|
-
return false;
|
|
3241
|
-
}
|
|
3242
|
-
}
|
|
3243
|
-
return true;
|
|
3244
|
-
});
|
|
2983
|
+
}).filter((tool) => hasPermission(TOOL_PERMISSIONS[tool.name]));
|
|
3245
2984
|
return {
|
|
3246
2985
|
tools,
|
|
3247
2986
|
};
|
|
@@ -3257,6 +2996,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
3257
2996
|
assertResolvedProfileForMutatingTool(name);
|
|
3258
2997
|
switch (name) {
|
|
3259
2998
|
case "list_tasks": {
|
|
2999
|
+
const startedAtMs = Date.now();
|
|
3260
3000
|
const { format = "markdown", category, status, openOnly = false, assignee, priority, type, sort = "created", order = "desc", limit } = args;
|
|
3261
3001
|
let data = core.listTasks(ACTIVE_WORKSPACE_ID);
|
|
3262
3002
|
let tasks = data.tasks;
|
|
@@ -3299,7 +3039,21 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
3299
3039
|
// Strip verbose detail fields — descriptions, comments, events, and derived
|
|
3300
3040
|
// activity are omitted from list responses to keep context window usage
|
|
3301
3041
|
// manageable. Use get_task to retrieve full details for a specific task.
|
|
3302
|
-
const slimTasks = tasks.map(({ description, comments, taskEvents, activity, createdByActor, assigneeActor, ...rest }) => rest);
|
|
3042
|
+
const slimTasks = tasks.map(({ description, comments, taskEvents, activity, createdByActor, assigneeActor, attachments, checklistItems, ...rest }) => rest);
|
|
3043
|
+
logMcpReadObservation('list_tasks', startedAtMs, {
|
|
3044
|
+
format,
|
|
3045
|
+
totalTasks: data.tasks.length,
|
|
3046
|
+
returnedTasks: slimTasks.length,
|
|
3047
|
+
filtered: slimTasks.length !== data.tasks.length,
|
|
3048
|
+
openOnly,
|
|
3049
|
+
hasAssigneeFilter: Boolean(assignee),
|
|
3050
|
+
hasStatusFilter: Boolean(status),
|
|
3051
|
+
hasCategoryFilter: Boolean(category),
|
|
3052
|
+
hasTypeFilter: Boolean(type),
|
|
3053
|
+
hasPriorityFilter: priority !== undefined,
|
|
3054
|
+
sort,
|
|
3055
|
+
order,
|
|
3056
|
+
});
|
|
3303
3057
|
if (format === "json") {
|
|
3304
3058
|
return {
|
|
3305
3059
|
content: [{ type: "text", text: JSON.stringify(slimTasks, null, 2) }],
|
|
@@ -3367,6 +3121,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
3367
3121
|
};
|
|
3368
3122
|
}
|
|
3369
3123
|
case "get_current_profile": {
|
|
3124
|
+
const startedAtMs = Date.now();
|
|
3370
3125
|
const { profileToken } = args;
|
|
3371
3126
|
ensureResolvedProfileForSessionBootstrap();
|
|
3372
3127
|
let profile = activeAiProfileId
|
|
@@ -3383,6 +3138,13 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
3383
3138
|
}
|
|
3384
3139
|
bindingSource = 'token_lookup';
|
|
3385
3140
|
}
|
|
3141
|
+
logMcpReadObservation('get_current_profile', startedAtMs, {
|
|
3142
|
+
identityBound: Boolean(activeAiProfileId),
|
|
3143
|
+
bindingSource,
|
|
3144
|
+
writeReady: Boolean(profile),
|
|
3145
|
+
profileResolved: Boolean(profile),
|
|
3146
|
+
usedProfileTokenLookup: !activeAiProfileId && typeof profileToken === 'string' && profileToken.trim().length > 0,
|
|
3147
|
+
});
|
|
3386
3148
|
return {
|
|
3387
3149
|
content: [{
|
|
3388
3150
|
type: "text",
|
|
@@ -3721,7 +3483,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
3721
3483
|
text: JSON.stringify({
|
|
3722
3484
|
taskId: task.id,
|
|
3723
3485
|
referenceLabel: task.referenceLabel || getTaskReferenceLabel(task),
|
|
3724
|
-
attachments: listTaskAttachmentDescriptors(task)
|
|
3486
|
+
attachments: taskAttachmentHelpers.listTaskAttachmentDescriptors(task)
|
|
3725
3487
|
}, null, 2)
|
|
3726
3488
|
}],
|
|
3727
3489
|
};
|
|
@@ -3732,7 +3494,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
3732
3494
|
if (!task) {
|
|
3733
3495
|
throw new Error(`Task ${taskId} not found`);
|
|
3734
3496
|
}
|
|
3735
|
-
const attachments = listTaskAttachmentDescriptors(task);
|
|
3497
|
+
const attachments = taskAttachmentHelpers.listTaskAttachmentDescriptors(task);
|
|
3736
3498
|
const attachment = attachments.find((entry) => entry.attachmentId === attachmentId);
|
|
3737
3499
|
if (!attachment) {
|
|
3738
3500
|
throw new Error(`Attachment ${attachmentId} not found on task ${taskId}`);
|
|
@@ -3757,7 +3519,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
3757
3519
|
taskId: task.id,
|
|
3758
3520
|
attachment,
|
|
3759
3521
|
format: "download",
|
|
3760
|
-
downloadUrl: buildAttachmentDownloadUrl(attachment),
|
|
3522
|
+
downloadUrl: canonicalAssetHelpers.buildAttachmentDownloadUrl(attachment),
|
|
3761
3523
|
reason: attachment.source === 'external' ? 'external_reference' : 'requested_download'
|
|
3762
3524
|
}, null, 2)
|
|
3763
3525
|
}],
|
|
@@ -3771,14 +3533,14 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
3771
3533
|
taskId: task.id,
|
|
3772
3534
|
attachment,
|
|
3773
3535
|
format: "download",
|
|
3774
|
-
downloadUrl: buildAttachmentDownloadUrl(attachment),
|
|
3536
|
+
downloadUrl: canonicalAssetHelpers.buildAttachmentDownloadUrl(attachment),
|
|
3775
3537
|
reason: attachment.source === 'external' ? 'external_reference' : 'binary_file'
|
|
3776
3538
|
}, null, 2)
|
|
3777
3539
|
}],
|
|
3778
3540
|
};
|
|
3779
3541
|
}
|
|
3780
3542
|
const resolvedMaxChars = Math.max(1, Math.floor(Number(maxChars || 200000)));
|
|
3781
|
-
const buffer = await readCanonicalAttachmentBuffer(task.id, attachment);
|
|
3543
|
+
const buffer = await canonicalAssetHelpers.readCanonicalAttachmentBuffer(task.id, attachment);
|
|
3782
3544
|
const content = buffer.toString('utf8');
|
|
3783
3545
|
if (content.length > resolvedMaxChars) {
|
|
3784
3546
|
throw new Error('Attachment exceeds maxChars limit for inline read');
|
|
@@ -3965,7 +3727,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
3965
3727
|
text: JSON.stringify({
|
|
3966
3728
|
document,
|
|
3967
3729
|
format: "download",
|
|
3968
|
-
downloadUrl: buildDocumentDownloadUrl(asset),
|
|
3730
|
+
downloadUrl: canonicalAssetHelpers.buildDocumentDownloadUrl(asset),
|
|
3969
3731
|
reason: "requested_download"
|
|
3970
3732
|
}, null, 2)
|
|
3971
3733
|
}],
|
|
@@ -3978,14 +3740,14 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
3978
3740
|
text: JSON.stringify({
|
|
3979
3741
|
document,
|
|
3980
3742
|
format: "download",
|
|
3981
|
-
downloadUrl: buildDocumentDownloadUrl(asset),
|
|
3743
|
+
downloadUrl: canonicalAssetHelpers.buildDocumentDownloadUrl(asset),
|
|
3982
3744
|
reason: "binary_file"
|
|
3983
3745
|
}, null, 2)
|
|
3984
3746
|
}],
|
|
3985
3747
|
};
|
|
3986
3748
|
}
|
|
3987
3749
|
const resolvedMaxChars = Math.max(1, Math.floor(Number(maxChars || 200000)));
|
|
3988
|
-
const buffer = await readCanonicalDocumentBuffer(asset);
|
|
3750
|
+
const buffer = await canonicalAssetHelpers.readCanonicalDocumentBuffer(asset);
|
|
3989
3751
|
const content = buffer.toString('utf8');
|
|
3990
3752
|
if (content.length > resolvedMaxChars) {
|
|
3991
3753
|
throw new Error('Document exceeds maxChars limit for inline read');
|
|
@@ -5220,9 +4982,6 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
5220
4982
|
if (hasContent === hasSourcePath) {
|
|
5221
4983
|
throw new Error("Provide exactly one of: content or sourcePath");
|
|
5222
4984
|
}
|
|
5223
|
-
if (runtimeMode === 'cloud' && hasSourcePath) {
|
|
5224
|
-
throw new Error("Cloud MCP does not support sourcePath for save_task_attachment yet. For AI-authored markdown or text, resend the attachment using inline content instead.");
|
|
5225
|
-
}
|
|
5226
4985
|
const now = new Date().toISOString();
|
|
5227
4986
|
const safeCaption = typeof caption === 'string' && caption.trim().length > 0
|
|
5228
4987
|
? caption.trim()
|
|
@@ -5230,11 +4989,11 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
5230
4989
|
const safeFilename = sanitizeFilenameForPath(filename);
|
|
5231
4990
|
const { projectRoot, basePath } = core.getPaths();
|
|
5232
4991
|
const overwriteTarget = overwrite === true
|
|
5233
|
-
? resolveCanonicalOverwriteTarget(task, safeFilename)
|
|
4992
|
+
? canonicalAssetHelpers.resolveCanonicalOverwriteTarget(task, safeFilename, taskAttachmentHelpers.describeTaskAttachment)
|
|
5234
4993
|
: null;
|
|
5235
4994
|
const canonicalAsset = overwriteTarget
|
|
5236
4995
|
? null
|
|
5237
|
-
: buildCanonicalWorkspaceAssetRelativePath({
|
|
4996
|
+
: canonicalAssetHelpers.buildCanonicalWorkspaceAssetRelativePath({
|
|
5238
4997
|
workspaceIdRaw: ACTIVE_WORKSPACE_ID,
|
|
5239
4998
|
kind: inferAssetKind(safeFilename, inferMimeTypeFromPath(safeFilename)),
|
|
5240
4999
|
filename: safeFilename
|
|
@@ -5257,7 +5016,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
5257
5016
|
const sourceReal = fs.realpathSync(sourceResolved);
|
|
5258
5017
|
const projectRootReal = fs.realpathSync(projectRoot);
|
|
5259
5018
|
if (!isApprovedAttachmentSourcePath(sourceReal, projectRootReal)) {
|
|
5260
|
-
throw new Error("sourcePath must be within project root
|
|
5019
|
+
throw new Error("sourcePath must be within project root, a Taskforce-managed attachment source, or an approved agent workspace root");
|
|
5261
5020
|
}
|
|
5262
5021
|
if (!fs.statSync(sourceReal).isFile()) {
|
|
5263
5022
|
throw new Error(`Path is not a file: ${trimmedSource}`);
|
|
@@ -5279,7 +5038,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
5279
5038
|
const persistedCaption = isMarkdownDocument
|
|
5280
5039
|
? canonicalName
|
|
5281
5040
|
: (safeCaption || overwriteTarget?.attachment.caption || safeFilename);
|
|
5282
|
-
const asset = ensureCanonicalTaskAttachment(relativePath, id, persistedCaption, now, 'attachment', {
|
|
5041
|
+
const asset = canonicalAssetHelpers.ensureCanonicalTaskAttachment(relativePath, id, persistedCaption, now, 'attachment', {
|
|
5283
5042
|
originalFilename: safeFilename,
|
|
5284
5043
|
logicalName: isMarkdownDocument ? canonicalName : safeFilename,
|
|
5285
5044
|
...(overwriteTarget
|
|
@@ -5295,7 +5054,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
5295
5054
|
: {})
|
|
5296
5055
|
});
|
|
5297
5056
|
const attachment = {
|
|
5298
|
-
path: overwriteTarget?.attachment.path || (asset?.kind === 'document' &&
|
|
5057
|
+
path: overwriteTarget?.attachment.path || (asset?.kind === 'document' && isValidCanonicalDocumentAsset(asset) && asset.assetId
|
|
5299
5058
|
? `/api/taskforce/documents/${encodeURIComponent(asset.assetId)}/content`
|
|
5300
5059
|
: `/api/taskforce/context/${encodeURIComponent((canonicalAsset || { relativePath: overwriteTarget?.descriptor.fsPath || '' }).relativePath)}`),
|
|
5301
5060
|
fsPath: overwriteTarget?.attachment.fsPath || canonicalAsset?.relativePath || overwriteTarget?.descriptor.fsPath,
|
|
@@ -5365,7 +5124,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
5365
5124
|
throw new Error('attachmentId is required');
|
|
5366
5125
|
const { id, task } = resolveTaskByIdentifierOrThrow(inputTaskId);
|
|
5367
5126
|
const existing = Array.isArray(task.attachments) ? task.attachments : [];
|
|
5368
|
-
const descriptors = listTaskAttachmentDescriptors(task);
|
|
5127
|
+
const descriptors = taskAttachmentHelpers.listTaskAttachmentDescriptors(task);
|
|
5369
5128
|
const targetIndex = descriptors.findIndex((entry) => entry.attachmentId === attachmentId);
|
|
5370
5129
|
if (targetIndex < 0) {
|
|
5371
5130
|
throw new Error(`Attachment ${attachmentId} not found on task ${inputTaskId}`);
|