@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
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('/');
|
|
@@ -1706,11 +1375,20 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
1706
1375
|
async function flushScheduledResourceInvalidations() {
|
|
1707
1376
|
if (pendingResourceInvalidations.size === 0)
|
|
1708
1377
|
return;
|
|
1378
|
+
const startedAtMs = Date.now();
|
|
1709
1379
|
const uris = Array.from(pendingResourceInvalidations);
|
|
1710
1380
|
pendingResourceInvalidations.clear();
|
|
1711
1381
|
for (const uri of uris) {
|
|
1712
1382
|
await notifyResourceUpdatedIfSubscribed(uri);
|
|
1713
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
|
+
}
|
|
1714
1392
|
}
|
|
1715
1393
|
function scheduleResourceMutation(options) {
|
|
1716
1394
|
const uris = collectResourceMutationUris(options);
|
|
@@ -3059,7 +2737,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
3059
2737
|
{
|
|
3060
2738
|
name: "save_task_attachment",
|
|
3061
2739
|
description: describeTool(runtimeMode === 'cloud'
|
|
3062
|
-
? "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."
|
|
3063
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."),
|
|
3064
2742
|
inputSchema: {
|
|
3065
2743
|
type: "object",
|
|
@@ -3067,7 +2745,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
3067
2745
|
id: { type: "string", description: "Task ID" },
|
|
3068
2746
|
filename: { type: "string", description: "Target filename for the task attachment." },
|
|
3069
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." },
|
|
3070
|
-
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." },
|
|
3071
2749
|
caption: { type: "string", description: "Optional label shown in task context" },
|
|
3072
2750
|
overwrite: { type: "boolean", description: "Overwrite the destination file when it already exists (default: false)." }
|
|
3073
2751
|
},
|
|
@@ -3318,6 +2996,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
3318
2996
|
assertResolvedProfileForMutatingTool(name);
|
|
3319
2997
|
switch (name) {
|
|
3320
2998
|
case "list_tasks": {
|
|
2999
|
+
const startedAtMs = Date.now();
|
|
3321
3000
|
const { format = "markdown", category, status, openOnly = false, assignee, priority, type, sort = "created", order = "desc", limit } = args;
|
|
3322
3001
|
let data = core.listTasks(ACTIVE_WORKSPACE_ID);
|
|
3323
3002
|
let tasks = data.tasks;
|
|
@@ -3360,7 +3039,21 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
3360
3039
|
// Strip verbose detail fields — descriptions, comments, events, and derived
|
|
3361
3040
|
// activity are omitted from list responses to keep context window usage
|
|
3362
3041
|
// manageable. Use get_task to retrieve full details for a specific task.
|
|
3363
|
-
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
|
+
});
|
|
3364
3057
|
if (format === "json") {
|
|
3365
3058
|
return {
|
|
3366
3059
|
content: [{ type: "text", text: JSON.stringify(slimTasks, null, 2) }],
|
|
@@ -3428,6 +3121,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
3428
3121
|
};
|
|
3429
3122
|
}
|
|
3430
3123
|
case "get_current_profile": {
|
|
3124
|
+
const startedAtMs = Date.now();
|
|
3431
3125
|
const { profileToken } = args;
|
|
3432
3126
|
ensureResolvedProfileForSessionBootstrap();
|
|
3433
3127
|
let profile = activeAiProfileId
|
|
@@ -3444,6 +3138,13 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
3444
3138
|
}
|
|
3445
3139
|
bindingSource = 'token_lookup';
|
|
3446
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
|
+
});
|
|
3447
3148
|
return {
|
|
3448
3149
|
content: [{
|
|
3449
3150
|
type: "text",
|
|
@@ -3782,7 +3483,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
3782
3483
|
text: JSON.stringify({
|
|
3783
3484
|
taskId: task.id,
|
|
3784
3485
|
referenceLabel: task.referenceLabel || getTaskReferenceLabel(task),
|
|
3785
|
-
attachments: listTaskAttachmentDescriptors(task)
|
|
3486
|
+
attachments: taskAttachmentHelpers.listTaskAttachmentDescriptors(task)
|
|
3786
3487
|
}, null, 2)
|
|
3787
3488
|
}],
|
|
3788
3489
|
};
|
|
@@ -3793,7 +3494,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
3793
3494
|
if (!task) {
|
|
3794
3495
|
throw new Error(`Task ${taskId} not found`);
|
|
3795
3496
|
}
|
|
3796
|
-
const attachments = listTaskAttachmentDescriptors(task);
|
|
3497
|
+
const attachments = taskAttachmentHelpers.listTaskAttachmentDescriptors(task);
|
|
3797
3498
|
const attachment = attachments.find((entry) => entry.attachmentId === attachmentId);
|
|
3798
3499
|
if (!attachment) {
|
|
3799
3500
|
throw new Error(`Attachment ${attachmentId} not found on task ${taskId}`);
|
|
@@ -3818,7 +3519,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
3818
3519
|
taskId: task.id,
|
|
3819
3520
|
attachment,
|
|
3820
3521
|
format: "download",
|
|
3821
|
-
downloadUrl: buildAttachmentDownloadUrl(attachment),
|
|
3522
|
+
downloadUrl: canonicalAssetHelpers.buildAttachmentDownloadUrl(attachment),
|
|
3822
3523
|
reason: attachment.source === 'external' ? 'external_reference' : 'requested_download'
|
|
3823
3524
|
}, null, 2)
|
|
3824
3525
|
}],
|
|
@@ -3832,14 +3533,14 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
3832
3533
|
taskId: task.id,
|
|
3833
3534
|
attachment,
|
|
3834
3535
|
format: "download",
|
|
3835
|
-
downloadUrl: buildAttachmentDownloadUrl(attachment),
|
|
3536
|
+
downloadUrl: canonicalAssetHelpers.buildAttachmentDownloadUrl(attachment),
|
|
3836
3537
|
reason: attachment.source === 'external' ? 'external_reference' : 'binary_file'
|
|
3837
3538
|
}, null, 2)
|
|
3838
3539
|
}],
|
|
3839
3540
|
};
|
|
3840
3541
|
}
|
|
3841
3542
|
const resolvedMaxChars = Math.max(1, Math.floor(Number(maxChars || 200000)));
|
|
3842
|
-
const buffer = await readCanonicalAttachmentBuffer(task.id, attachment);
|
|
3543
|
+
const buffer = await canonicalAssetHelpers.readCanonicalAttachmentBuffer(task.id, attachment);
|
|
3843
3544
|
const content = buffer.toString('utf8');
|
|
3844
3545
|
if (content.length > resolvedMaxChars) {
|
|
3845
3546
|
throw new Error('Attachment exceeds maxChars limit for inline read');
|
|
@@ -4026,7 +3727,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
4026
3727
|
text: JSON.stringify({
|
|
4027
3728
|
document,
|
|
4028
3729
|
format: "download",
|
|
4029
|
-
downloadUrl: buildDocumentDownloadUrl(asset),
|
|
3730
|
+
downloadUrl: canonicalAssetHelpers.buildDocumentDownloadUrl(asset),
|
|
4030
3731
|
reason: "requested_download"
|
|
4031
3732
|
}, null, 2)
|
|
4032
3733
|
}],
|
|
@@ -4039,14 +3740,14 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
4039
3740
|
text: JSON.stringify({
|
|
4040
3741
|
document,
|
|
4041
3742
|
format: "download",
|
|
4042
|
-
downloadUrl: buildDocumentDownloadUrl(asset),
|
|
3743
|
+
downloadUrl: canonicalAssetHelpers.buildDocumentDownloadUrl(asset),
|
|
4043
3744
|
reason: "binary_file"
|
|
4044
3745
|
}, null, 2)
|
|
4045
3746
|
}],
|
|
4046
3747
|
};
|
|
4047
3748
|
}
|
|
4048
3749
|
const resolvedMaxChars = Math.max(1, Math.floor(Number(maxChars || 200000)));
|
|
4049
|
-
const buffer = await readCanonicalDocumentBuffer(asset);
|
|
3750
|
+
const buffer = await canonicalAssetHelpers.readCanonicalDocumentBuffer(asset);
|
|
4050
3751
|
const content = buffer.toString('utf8');
|
|
4051
3752
|
if (content.length > resolvedMaxChars) {
|
|
4052
3753
|
throw new Error('Document exceeds maxChars limit for inline read');
|
|
@@ -5281,9 +4982,6 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
5281
4982
|
if (hasContent === hasSourcePath) {
|
|
5282
4983
|
throw new Error("Provide exactly one of: content or sourcePath");
|
|
5283
4984
|
}
|
|
5284
|
-
if (runtimeMode === 'cloud' && hasSourcePath) {
|
|
5285
|
-
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.");
|
|
5286
|
-
}
|
|
5287
4985
|
const now = new Date().toISOString();
|
|
5288
4986
|
const safeCaption = typeof caption === 'string' && caption.trim().length > 0
|
|
5289
4987
|
? caption.trim()
|
|
@@ -5291,11 +4989,11 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
5291
4989
|
const safeFilename = sanitizeFilenameForPath(filename);
|
|
5292
4990
|
const { projectRoot, basePath } = core.getPaths();
|
|
5293
4991
|
const overwriteTarget = overwrite === true
|
|
5294
|
-
? resolveCanonicalOverwriteTarget(task, safeFilename)
|
|
4992
|
+
? canonicalAssetHelpers.resolveCanonicalOverwriteTarget(task, safeFilename, taskAttachmentHelpers.describeTaskAttachment)
|
|
5295
4993
|
: null;
|
|
5296
4994
|
const canonicalAsset = overwriteTarget
|
|
5297
4995
|
? null
|
|
5298
|
-
: buildCanonicalWorkspaceAssetRelativePath({
|
|
4996
|
+
: canonicalAssetHelpers.buildCanonicalWorkspaceAssetRelativePath({
|
|
5299
4997
|
workspaceIdRaw: ACTIVE_WORKSPACE_ID,
|
|
5300
4998
|
kind: inferAssetKind(safeFilename, inferMimeTypeFromPath(safeFilename)),
|
|
5301
4999
|
filename: safeFilename
|
|
@@ -5318,7 +5016,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
5318
5016
|
const sourceReal = fs.realpathSync(sourceResolved);
|
|
5319
5017
|
const projectRootReal = fs.realpathSync(projectRoot);
|
|
5320
5018
|
if (!isApprovedAttachmentSourcePath(sourceReal, projectRootReal)) {
|
|
5321
|
-
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");
|
|
5322
5020
|
}
|
|
5323
5021
|
if (!fs.statSync(sourceReal).isFile()) {
|
|
5324
5022
|
throw new Error(`Path is not a file: ${trimmedSource}`);
|
|
@@ -5340,7 +5038,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
5340
5038
|
const persistedCaption = isMarkdownDocument
|
|
5341
5039
|
? canonicalName
|
|
5342
5040
|
: (safeCaption || overwriteTarget?.attachment.caption || safeFilename);
|
|
5343
|
-
const asset = ensureCanonicalTaskAttachment(relativePath, id, persistedCaption, now, 'attachment', {
|
|
5041
|
+
const asset = canonicalAssetHelpers.ensureCanonicalTaskAttachment(relativePath, id, persistedCaption, now, 'attachment', {
|
|
5344
5042
|
originalFilename: safeFilename,
|
|
5345
5043
|
logicalName: isMarkdownDocument ? canonicalName : safeFilename,
|
|
5346
5044
|
...(overwriteTarget
|
|
@@ -5356,7 +5054,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
5356
5054
|
: {})
|
|
5357
5055
|
});
|
|
5358
5056
|
const attachment = {
|
|
5359
|
-
path: overwriteTarget?.attachment.path || (asset?.kind === 'document' &&
|
|
5057
|
+
path: overwriteTarget?.attachment.path || (asset?.kind === 'document' && isValidCanonicalDocumentAsset(asset) && asset.assetId
|
|
5360
5058
|
? `/api/taskforce/documents/${encodeURIComponent(asset.assetId)}/content`
|
|
5361
5059
|
: `/api/taskforce/context/${encodeURIComponent((canonicalAsset || { relativePath: overwriteTarget?.descriptor.fsPath || '' }).relativePath)}`),
|
|
5362
5060
|
fsPath: overwriteTarget?.attachment.fsPath || canonicalAsset?.relativePath || overwriteTarget?.descriptor.fsPath,
|
|
@@ -5426,7 +5124,7 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
5426
5124
|
throw new Error('attachmentId is required');
|
|
5427
5125
|
const { id, task } = resolveTaskByIdentifierOrThrow(inputTaskId);
|
|
5428
5126
|
const existing = Array.isArray(task.attachments) ? task.attachments : [];
|
|
5429
|
-
const descriptors = listTaskAttachmentDescriptors(task);
|
|
5127
|
+
const descriptors = taskAttachmentHelpers.listTaskAttachmentDescriptors(task);
|
|
5430
5128
|
const targetIndex = descriptors.findIndex((entry) => entry.attachmentId === attachmentId);
|
|
5431
5129
|
if (targetIndex < 0) {
|
|
5432
5130
|
throw new Error(`Attachment ${attachmentId} not found on task ${inputTaskId}`);
|