@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.test.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import path from 'path';
|
|
@@ -7,61 +7,8 @@ import { MCP_TOOL_REGISTRY } from './toolRegistry';
|
|
|
7
7
|
import { TaskforceCore, TaskforceRuleError } from '../core/Taskforce';
|
|
8
8
|
import { WorkspaceAssetStore } from '../storage/workspaceAssetStore';
|
|
9
9
|
import { ANNOTATED_ATTACHMENTS_WORKSPACE_FEATURE_KEY, APP_GATE_DEFINITIONS, } from '../runtime/appGates';
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
const core = new TaskforceCore({
|
|
13
|
-
projectRoot,
|
|
14
|
-
storagePath: path.join(projectRoot, '.taskforce')
|
|
15
|
-
});
|
|
16
|
-
try {
|
|
17
|
-
Object.defineProperty(core, 'getAccountAccessStatus', {
|
|
18
|
-
configurable: true,
|
|
19
|
-
value: () => ({
|
|
20
|
-
gate: 'ok',
|
|
21
|
-
hasEntitlement: true,
|
|
22
|
-
canAccessApp: true,
|
|
23
|
-
canAccessPlans: true,
|
|
24
|
-
signupMonetizationStrategy: 'none',
|
|
25
|
-
planSelectionRequired: false,
|
|
26
|
-
message: null,
|
|
27
|
-
entitlement: null
|
|
28
|
-
})
|
|
29
|
-
});
|
|
30
|
-
core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
|
|
31
|
-
core.ensureBootstrapUserAccess({
|
|
32
|
-
userId: 'owner-1',
|
|
33
|
-
workspaceId: 'workspace-1',
|
|
34
|
-
role: 'owner',
|
|
35
|
-
email: 'owner-1@example.com'
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
finally {
|
|
39
|
-
core.close();
|
|
40
|
-
}
|
|
41
|
-
return projectRoot;
|
|
42
|
-
}
|
|
43
|
-
async function listTools(projectRoot, runtimeMode) {
|
|
44
|
-
const runtime = createTaskforceMcpServer({
|
|
45
|
-
projectRoot,
|
|
46
|
-
runtimeMode,
|
|
47
|
-
logStartup: false,
|
|
48
|
-
auth: runtimeMode === 'cloud'
|
|
49
|
-
? { scopes: ['tasks:read', 'tasks:write', 'workspace:read'] }
|
|
50
|
-
: undefined
|
|
51
|
-
});
|
|
52
|
-
const handler = runtime.server._requestHandlers.get('tools/list');
|
|
53
|
-
if (!handler) {
|
|
54
|
-
throw new Error('tools/list handler not registered');
|
|
55
|
-
}
|
|
56
|
-
return await handler({
|
|
57
|
-
jsonrpc: '2.0',
|
|
58
|
-
id: 'test-tools-list',
|
|
59
|
-
method: 'tools/list',
|
|
60
|
-
params: {}
|
|
61
|
-
}, {});
|
|
62
|
-
}
|
|
63
|
-
async function callTool(projectRoot, runtimeMode, name, args, options) {
|
|
64
|
-
const runtime = createTaskforceMcpServer({
|
|
10
|
+
function createRuntime(projectRoot, runtimeMode, options) {
|
|
11
|
+
return createTaskforceMcpServer({
|
|
65
12
|
projectRoot,
|
|
66
13
|
runtimeMode,
|
|
67
14
|
workspaceId: options?.workspaceId,
|
|
@@ -83,6 +30,8 @@ async function callTool(projectRoot, runtimeMode, name, args, options) {
|
|
|
83
30
|
}
|
|
84
31
|
: undefined
|
|
85
32
|
});
|
|
33
|
+
}
|
|
34
|
+
async function callToolWithRuntime(runtime, name, args, options) {
|
|
86
35
|
const handler = runtime.server._requestHandlers.get('tools/call');
|
|
87
36
|
if (!handler) {
|
|
88
37
|
throw new Error('tools/call handler not registered');
|
|
@@ -112,20 +61,71 @@ async function callTool(projectRoot, runtimeMode, name, args, options) {
|
|
|
112
61
|
}
|
|
113
62
|
}, {});
|
|
114
63
|
}
|
|
115
|
-
async function
|
|
116
|
-
const
|
|
64
|
+
async function subscribeResource(runtime, uri) {
|
|
65
|
+
const handler = runtime.server._requestHandlers.get('resources/subscribe');
|
|
66
|
+
if (!handler) {
|
|
67
|
+
throw new Error('resources/subscribe handler not registered');
|
|
68
|
+
}
|
|
69
|
+
await handler({
|
|
70
|
+
jsonrpc: '2.0',
|
|
71
|
+
id: `test-resources-subscribe-${uri}`,
|
|
72
|
+
method: 'resources/subscribe',
|
|
73
|
+
params: { uri }
|
|
74
|
+
}, {});
|
|
75
|
+
}
|
|
76
|
+
function createProjectRoot() {
|
|
77
|
+
const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-mcp-runtime-'));
|
|
78
|
+
const core = new TaskforceCore({
|
|
117
79
|
projectRoot,
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
80
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
81
|
+
});
|
|
82
|
+
try {
|
|
83
|
+
Object.defineProperty(core, 'getAccountAccessStatus', {
|
|
84
|
+
configurable: true,
|
|
85
|
+
value: () => ({
|
|
86
|
+
gate: 'ok',
|
|
87
|
+
hasEntitlement: true,
|
|
88
|
+
canAccessApp: true,
|
|
89
|
+
canAccessPlans: true,
|
|
90
|
+
planSelectionRequired: false,
|
|
91
|
+
message: null,
|
|
92
|
+
entitlement: null
|
|
93
|
+
})
|
|
94
|
+
});
|
|
95
|
+
core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
|
|
96
|
+
core.ensureBootstrapUserAccess({
|
|
97
|
+
userId: 'owner-1',
|
|
98
|
+
workspaceId: 'workspace-1',
|
|
99
|
+
role: 'owner',
|
|
100
|
+
email: 'owner-1@example.com'
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
core.close();
|
|
105
|
+
}
|
|
106
|
+
return projectRoot;
|
|
107
|
+
}
|
|
108
|
+
async function listTools(projectRoot, runtimeMode) {
|
|
109
|
+
const runtime = createRuntime(projectRoot, runtimeMode);
|
|
110
|
+
const handler = runtime.server._requestHandlers.get('tools/list');
|
|
111
|
+
if (!handler) {
|
|
112
|
+
throw new Error('tools/list handler not registered');
|
|
113
|
+
}
|
|
114
|
+
return await handler({
|
|
115
|
+
jsonrpc: '2.0',
|
|
116
|
+
id: 'test-tools-list',
|
|
117
|
+
method: 'tools/list',
|
|
118
|
+
params: {}
|
|
119
|
+
}, {});
|
|
120
|
+
}
|
|
121
|
+
async function callTool(projectRoot, runtimeMode, name, args, options) {
|
|
122
|
+
const runtime = createRuntime(projectRoot, runtimeMode, options);
|
|
123
|
+
return await callToolWithRuntime(runtime, name, args, {
|
|
124
|
+
resolveProfile: options?.resolveProfile,
|
|
128
125
|
});
|
|
126
|
+
}
|
|
127
|
+
async function readResource(projectRoot, runtimeMode, uri, options) {
|
|
128
|
+
const runtime = createRuntime(projectRoot, runtimeMode, options);
|
|
129
129
|
const handler = runtime.server._requestHandlers.get('resources/read');
|
|
130
130
|
if (!handler) {
|
|
131
131
|
throw new Error('resources/read handler not registered');
|
|
@@ -138,19 +138,7 @@ async function readResource(projectRoot, runtimeMode, uri, options) {
|
|
|
138
138
|
}, {});
|
|
139
139
|
}
|
|
140
140
|
async function listResources(projectRoot, runtimeMode, options) {
|
|
141
|
-
const runtime =
|
|
142
|
-
projectRoot,
|
|
143
|
-
runtimeMode,
|
|
144
|
-
workspaceId: options?.workspaceId,
|
|
145
|
-
logStartup: false,
|
|
146
|
-
auth: runtimeMode === 'cloud'
|
|
147
|
-
? {
|
|
148
|
-
scopes: ['tasks:read', 'tasks:write', 'workspace:read'],
|
|
149
|
-
userId: options?.authUserId || 'owner-1',
|
|
150
|
-
actorType: 'oauth'
|
|
151
|
-
}
|
|
152
|
-
: undefined
|
|
153
|
-
});
|
|
141
|
+
const runtime = createRuntime(projectRoot, runtimeMode, options);
|
|
154
142
|
const handler = runtime.server._requestHandlers.get('resources/list');
|
|
155
143
|
if (!handler) {
|
|
156
144
|
throw new Error('resources/list handler not registered');
|
|
@@ -304,6 +292,29 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
304
292
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
305
293
|
}
|
|
306
294
|
});
|
|
295
|
+
it('does not let TASKFORCE_TOOLS prune the runtime tool catalog', async () => {
|
|
296
|
+
const projectRoot = createProjectRoot();
|
|
297
|
+
const previousValue = process.env.TASKFORCE_TOOLS;
|
|
298
|
+
process.env.TASKFORCE_TOOLS = 'get_task';
|
|
299
|
+
try {
|
|
300
|
+
const result = await listTools(projectRoot, 'cloud');
|
|
301
|
+
const runtimeToolNames = result.tools.map((tool) => tool.name).sort();
|
|
302
|
+
const registryToolNames = Object.entries(MCP_TOOL_REGISTRY)
|
|
303
|
+
.filter(([, entry]) => entry.permission.kind !== 'local-only' && entry.permission.kind !== 'session-only')
|
|
304
|
+
.map(([name]) => name)
|
|
305
|
+
.sort();
|
|
306
|
+
expect(runtimeToolNames).toEqual(registryToolNames);
|
|
307
|
+
}
|
|
308
|
+
finally {
|
|
309
|
+
if (previousValue === undefined) {
|
|
310
|
+
delete process.env.TASKFORCE_TOOLS;
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
process.env.TASKFORCE_TOOLS = previousValue;
|
|
314
|
+
}
|
|
315
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
316
|
+
}
|
|
317
|
+
});
|
|
307
318
|
it('includes local-only guidance in local runtime tool descriptions', async () => {
|
|
308
319
|
const projectRoot = createProjectRoot();
|
|
309
320
|
try {
|
|
@@ -425,11 +436,11 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
425
436
|
expect(linkTaskContext.description).toContain('external http(s) URL');
|
|
426
437
|
expect(saveTaskAttachment.description).toContain('generated content');
|
|
427
438
|
expect(saveTaskAttachment.description).toContain('overwrite=true');
|
|
428
|
-
expect(saveTaskAttachment.description).toContain('AI-authored markdown/text
|
|
439
|
+
expect(saveTaskAttachment.description).toContain('use inline content for AI-authored markdown/text');
|
|
429
440
|
expect(saveTaskAttachment.inputSchema).toBeDefined();
|
|
430
441
|
const saveTaskAttachmentSchema = saveTaskAttachment.inputSchema;
|
|
431
442
|
expect(saveTaskAttachmentSchema.properties.content.description).toContain('use this for AI-authored markdown/text attachments');
|
|
432
|
-
expect(saveTaskAttachmentSchema.properties.sourcePath.description).toContain('
|
|
443
|
+
expect(saveTaskAttachmentSchema.properties.sourcePath.description).toContain('approved agent workspace root');
|
|
433
444
|
expect(resolveProfile.description).toContain('AI collaborator profile');
|
|
434
445
|
expect(resolveProfile.description).toContain('Update semantics');
|
|
435
446
|
expect(resolveProfile.description).toContain('empty strings are ignored rather than clearing');
|
|
@@ -1500,6 +1511,150 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
1500
1511
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1501
1512
|
}
|
|
1502
1513
|
});
|
|
1514
|
+
it('builds workstream listings from one historical task snapshot and returns task summaries', async () => {
|
|
1515
|
+
const projectRoot = createProjectRoot();
|
|
1516
|
+
const core = new TaskforceCore({
|
|
1517
|
+
projectRoot,
|
|
1518
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1519
|
+
});
|
|
1520
|
+
try {
|
|
1521
|
+
const category = core.getCategories()[0]?.value;
|
|
1522
|
+
const type = core.getTypes()[0]?.value;
|
|
1523
|
+
const firstWorkstream = core.createWorkstream({ title: 'Workstream one' }, 'workspace-1');
|
|
1524
|
+
const secondWorkstream = core.createWorkstream({ title: 'Workstream two' }, 'workspace-1');
|
|
1525
|
+
core.createTask({
|
|
1526
|
+
title: 'Linked task one',
|
|
1527
|
+
category,
|
|
1528
|
+
type,
|
|
1529
|
+
workstreamId: firstWorkstream.id,
|
|
1530
|
+
}, { workspaceId: 'workspace-1', actorRef: 'owner-1' });
|
|
1531
|
+
core.createTask({
|
|
1532
|
+
title: 'Linked task two',
|
|
1533
|
+
category,
|
|
1534
|
+
type,
|
|
1535
|
+
workstreamId: secondWorkstream.id,
|
|
1536
|
+
}, { workspaceId: 'workspace-1', actorRef: 'owner-1' });
|
|
1537
|
+
const archivedTask = core.createTask({
|
|
1538
|
+
title: 'Archived linked task',
|
|
1539
|
+
category,
|
|
1540
|
+
type,
|
|
1541
|
+
workstreamId: firstWorkstream.id,
|
|
1542
|
+
status: 'done',
|
|
1543
|
+
}, { workspaceId: 'workspace-1', actorRef: 'owner-1' });
|
|
1544
|
+
core.archiveTask(archivedTask.id, false, undefined, 'workspace-1', { actorRef: 'owner-1' });
|
|
1545
|
+
}
|
|
1546
|
+
finally {
|
|
1547
|
+
core.close();
|
|
1548
|
+
}
|
|
1549
|
+
const listTasksSpy = vi.spyOn(TaskforceCore.prototype, 'listTasks');
|
|
1550
|
+
const listArchiveSpy = vi.spyOn(TaskforceCore.prototype, 'listArchive');
|
|
1551
|
+
try {
|
|
1552
|
+
const beforeListTasksCalls = listTasksSpy.mock.calls.length;
|
|
1553
|
+
const beforeListArchiveCalls = listArchiveSpy.mock.calls.length;
|
|
1554
|
+
const payload = parseToolJson(await callTool(projectRoot, 'local', 'list_workstreams', {}));
|
|
1555
|
+
const firstWorkstreamPayload = payload.workstreams.find((entry) => entry.workstream.title === 'Workstream one');
|
|
1556
|
+
const linkedTask = firstWorkstreamPayload?.tasks.find((task) => task.title === 'Archived linked task');
|
|
1557
|
+
expect(listTasksSpy.mock.calls.length - beforeListTasksCalls).toBe(1);
|
|
1558
|
+
expect(listArchiveSpy.mock.calls.length - beforeListArchiveCalls).toBe(1);
|
|
1559
|
+
expect(linkedTask).toMatchObject({
|
|
1560
|
+
title: 'Archived linked task',
|
|
1561
|
+
workstreamId: firstWorkstreamPayload?.workstream.id,
|
|
1562
|
+
status: 'done',
|
|
1563
|
+
});
|
|
1564
|
+
expect(linkedTask.description).toBeUndefined();
|
|
1565
|
+
expect(linkedTask.comments).toBeUndefined();
|
|
1566
|
+
expect(linkedTask.activity).toBeUndefined();
|
|
1567
|
+
expect(linkedTask.attachments).toBeUndefined();
|
|
1568
|
+
expect(linkedTask.checklistItems).toBeUndefined();
|
|
1569
|
+
}
|
|
1570
|
+
finally {
|
|
1571
|
+
listTasksSpy.mockRestore();
|
|
1572
|
+
listArchiveSpy.mockRestore();
|
|
1573
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1574
|
+
}
|
|
1575
|
+
});
|
|
1576
|
+
it('queues bulk resource invalidations instead of awaiting them in the write path', async () => {
|
|
1577
|
+
const projectRoot = createProjectRoot();
|
|
1578
|
+
const runtime = createRuntime(projectRoot, 'local');
|
|
1579
|
+
const resourceUri = `taskforce://${path.basename(projectRoot)}/tasks`;
|
|
1580
|
+
const sendResourceUpdatedSpy = vi
|
|
1581
|
+
.spyOn(runtime.server, 'sendResourceUpdated')
|
|
1582
|
+
.mockImplementation(async () => {
|
|
1583
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
1584
|
+
});
|
|
1585
|
+
try {
|
|
1586
|
+
await subscribeResource(runtime, resourceUri);
|
|
1587
|
+
const created = parseToolJson(await callToolWithRuntime(runtime, 'create_task', {
|
|
1588
|
+
title: 'Queued invalidation task'
|
|
1589
|
+
}));
|
|
1590
|
+
expect(created.title).toBe('Queued invalidation task');
|
|
1591
|
+
expect(sendResourceUpdatedSpy).not.toHaveBeenCalled();
|
|
1592
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
1593
|
+
expect(sendResourceUpdatedSpy).toHaveBeenCalledTimes(1);
|
|
1594
|
+
expect(sendResourceUpdatedSpy).toHaveBeenCalledWith({ uri: resourceUri });
|
|
1595
|
+
}
|
|
1596
|
+
finally {
|
|
1597
|
+
sendResourceUpdatedSpy.mockRestore();
|
|
1598
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1599
|
+
}
|
|
1600
|
+
});
|
|
1601
|
+
it('reads the bulk tasks resource as summary-only rows', async () => {
|
|
1602
|
+
const projectRoot = createProjectRoot();
|
|
1603
|
+
try {
|
|
1604
|
+
const created = parseToolJson(await callTool(projectRoot, 'local', 'create_task', {
|
|
1605
|
+
title: 'Verbose task',
|
|
1606
|
+
description: 'This should stay off the bulk tasks resource'
|
|
1607
|
+
}));
|
|
1608
|
+
await callTool(projectRoot, 'local', 'add_comment', {
|
|
1609
|
+
id: created.referenceLabel,
|
|
1610
|
+
text: 'This comment should stay off the bulk tasks resource',
|
|
1611
|
+
author: 'ai-profile-019d15c5-6ad3-7bf0-9b9e-fa9c366c3ff4'
|
|
1612
|
+
});
|
|
1613
|
+
const taskResource = await readResource(projectRoot, 'cloud', `taskforce://${path.basename(projectRoot)}/tasks`, {
|
|
1614
|
+
workspaceId: 'workspace-1',
|
|
1615
|
+
authUserId: 'owner-1'
|
|
1616
|
+
});
|
|
1617
|
+
const payload = JSON.parse(String(taskResource.contents[0]?.text || '{}'));
|
|
1618
|
+
const resourceTask = payload.tasks.find((task) => task.referenceLabel === created.referenceLabel);
|
|
1619
|
+
expect(resourceTask).toMatchObject({
|
|
1620
|
+
referenceLabel: created.referenceLabel,
|
|
1621
|
+
title: 'Verbose task',
|
|
1622
|
+
});
|
|
1623
|
+
expect(resourceTask.description).toBeUndefined();
|
|
1624
|
+
expect(resourceTask.comments).toBeUndefined();
|
|
1625
|
+
expect(resourceTask.activity).toBeUndefined();
|
|
1626
|
+
expect(resourceTask.attachments).toBeUndefined();
|
|
1627
|
+
expect(resourceTask.checklistItems).toBeUndefined();
|
|
1628
|
+
}
|
|
1629
|
+
finally {
|
|
1630
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1631
|
+
}
|
|
1632
|
+
});
|
|
1633
|
+
it('reads initiative listings with summary initiative and workstream records only', async () => {
|
|
1634
|
+
const projectRoot = createProjectRoot();
|
|
1635
|
+
try {
|
|
1636
|
+
const initiative = parseToolJson(await callTool(projectRoot, 'local', 'create_initiative', {
|
|
1637
|
+
title: 'Summary initiative',
|
|
1638
|
+
description: 'Verbose initiative details should stay off the bulk listing'
|
|
1639
|
+
}));
|
|
1640
|
+
await callTool(projectRoot, 'local', 'create_workstream', {
|
|
1641
|
+
title: 'Summary workstream',
|
|
1642
|
+
description: 'Verbose workstream details should stay off the bulk listing',
|
|
1643
|
+
initiativeId: initiative.initiative.referenceLabel
|
|
1644
|
+
});
|
|
1645
|
+
const initiatives = parseToolJson(await callTool(projectRoot, 'local', 'list_initiatives', {}));
|
|
1646
|
+
const listedInitiative = initiatives.initiatives.find((entry) => entry.initiative.referenceLabel === initiative.initiative.referenceLabel);
|
|
1647
|
+
const listedWorkstream = listedInitiative?.workstreams.find((entry) => entry.title === 'Summary workstream');
|
|
1648
|
+
expect(listedInitiative).toBeTruthy();
|
|
1649
|
+
expect(listedInitiative.initiative.description).toBeUndefined();
|
|
1650
|
+
expect(listedWorkstream).toBeTruthy();
|
|
1651
|
+
expect(listedWorkstream.description).toBeUndefined();
|
|
1652
|
+
expect(listedWorkstream.taskIds).toBeUndefined();
|
|
1653
|
+
}
|
|
1654
|
+
finally {
|
|
1655
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1656
|
+
}
|
|
1657
|
+
});
|
|
1503
1658
|
it('creates MCP tasks as unassigned when no explicit assignee is provided', async () => {
|
|
1504
1659
|
const projectRoot = createProjectRoot();
|
|
1505
1660
|
try {
|
|
@@ -1512,6 +1667,40 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
1512
1667
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1513
1668
|
}
|
|
1514
1669
|
});
|
|
1670
|
+
it('returns summary-only rows from list_tasks json output', async () => {
|
|
1671
|
+
const projectRoot = createProjectRoot();
|
|
1672
|
+
try {
|
|
1673
|
+
const created = parseToolJson(await callTool(projectRoot, 'local', 'create_task', {
|
|
1674
|
+
title: 'List summary task',
|
|
1675
|
+
description: 'Verbose description that should not appear in list rows'
|
|
1676
|
+
}));
|
|
1677
|
+
await callTool(projectRoot, 'local', 'replace_task_checklist', {
|
|
1678
|
+
taskId: created.referenceLabel,
|
|
1679
|
+
items: [
|
|
1680
|
+
{ title: 'Checklist item one', isCompleted: false }
|
|
1681
|
+
]
|
|
1682
|
+
});
|
|
1683
|
+
await callTool(projectRoot, 'local', 'add_comment', {
|
|
1684
|
+
id: created.referenceLabel,
|
|
1685
|
+
text: 'Verbose comment that should not appear in list rows',
|
|
1686
|
+
author: 'ai-profile-019d15c5-6ad3-7bf0-9b9e-fa9c366c3ff4'
|
|
1687
|
+
});
|
|
1688
|
+
const listed = parseToolJson(await callTool(projectRoot, 'local', 'list_tasks', {
|
|
1689
|
+
format: 'json'
|
|
1690
|
+
}));
|
|
1691
|
+
const row = listed.find((task) => task.referenceLabel === created.referenceLabel);
|
|
1692
|
+
expect(row).toBeTruthy();
|
|
1693
|
+
expect(row.description).toBeUndefined();
|
|
1694
|
+
expect(row.comments).toBeUndefined();
|
|
1695
|
+
expect(row.taskEvents).toBeUndefined();
|
|
1696
|
+
expect(row.activity).toBeUndefined();
|
|
1697
|
+
expect(row.attachments).toBeUndefined();
|
|
1698
|
+
expect(row.checklistItems).toBeUndefined();
|
|
1699
|
+
}
|
|
1700
|
+
finally {
|
|
1701
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1702
|
+
}
|
|
1703
|
+
});
|
|
1515
1704
|
it('reads task resources by T-XXX reference and exposes version/scope in config resource', async () => {
|
|
1516
1705
|
const projectRoot = createProjectRoot();
|
|
1517
1706
|
try {
|
|
@@ -1541,6 +1730,42 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
1541
1730
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1542
1731
|
}
|
|
1543
1732
|
});
|
|
1733
|
+
it('returns summary-shaped task entries from the shared tasks resource', async () => {
|
|
1734
|
+
const projectRoot = createProjectRoot();
|
|
1735
|
+
try {
|
|
1736
|
+
const core = new TaskforceCore({
|
|
1737
|
+
projectRoot,
|
|
1738
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1739
|
+
});
|
|
1740
|
+
core.createTask({
|
|
1741
|
+
title: 'Resource summary task',
|
|
1742
|
+
description: 'Verbose task description',
|
|
1743
|
+
comments: [{ id: 'comment-1', author: 'user', text: 'Hello', timestamp: '2026-04-13T01:00:00.000Z' }],
|
|
1744
|
+
attachments: [{ name: 'note.md', path: 'notes/note.md' }],
|
|
1745
|
+
checklistItems: [{ id: 'item-1', taskId: 'temp', title: 'Check me', isCompleted: false, order: 0, createdAt: '2026-04-13T01:00:00.000Z' }],
|
|
1746
|
+
}, {
|
|
1747
|
+
workspaceId: 'workspace-1',
|
|
1748
|
+
actorRef: 'owner-1'
|
|
1749
|
+
});
|
|
1750
|
+
core.close();
|
|
1751
|
+
const tasksResource = await readResource(projectRoot, 'cloud', `taskforce://${path.basename(projectRoot)}/tasks`, {
|
|
1752
|
+
workspaceId: 'workspace-1',
|
|
1753
|
+
authUserId: 'owner-1'
|
|
1754
|
+
});
|
|
1755
|
+
const payload = JSON.parse(String(tasksResource.contents[0]?.text || '{}'));
|
|
1756
|
+
const task = payload.tasks.find((entry) => entry.title === 'Resource summary task');
|
|
1757
|
+
expect(task).toBeDefined();
|
|
1758
|
+
expect(task.description).toBeUndefined();
|
|
1759
|
+
expect(task.comments).toBeUndefined();
|
|
1760
|
+
expect(task.taskEvents).toBeUndefined();
|
|
1761
|
+
expect(task.activity).toBeUndefined();
|
|
1762
|
+
expect(task.attachments).toBeUndefined();
|
|
1763
|
+
expect(task.checklistItems).toBeUndefined();
|
|
1764
|
+
}
|
|
1765
|
+
finally {
|
|
1766
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1767
|
+
}
|
|
1768
|
+
});
|
|
1544
1769
|
it('persists save_task_attachment through canonical task attachments', async () => {
|
|
1545
1770
|
const projectRoot = createProjectRoot();
|
|
1546
1771
|
try {
|
|
@@ -1770,8 +1995,45 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
1770
1995
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1771
1996
|
}
|
|
1772
1997
|
});
|
|
1773
|
-
it('
|
|
1998
|
+
it('allows save_task_attachment sourcePath in cloud runtime for approved agent workspace roots', async () => {
|
|
1774
1999
|
const projectRoot = createProjectRoot();
|
|
2000
|
+
const agentWorkspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-mcp-agent-workspace-'));
|
|
2001
|
+
const previousRoots = process.env.TASKFORCE_ALLOWED_ATTACHMENT_SOURCE_ROOTS;
|
|
2002
|
+
try {
|
|
2003
|
+
const core = new TaskforceCore({
|
|
2004
|
+
projectRoot,
|
|
2005
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
2006
|
+
});
|
|
2007
|
+
const task = core.createTask({ title: 'Cloud sourcePath allow test' }, { workspaceId: 'workspace-1' });
|
|
2008
|
+
core.close();
|
|
2009
|
+
const sourcePath = path.join(agentWorkspaceRoot, 'implementation-plan.md');
|
|
2010
|
+
fs.writeFileSync(sourcePath, '# Recovered implementation plan', 'utf8');
|
|
2011
|
+
process.env.TASKFORCE_ALLOWED_ATTACHMENT_SOURCE_ROOTS = agentWorkspaceRoot;
|
|
2012
|
+
await callTool(projectRoot, 'cloud', 'save_task_attachment', {
|
|
2013
|
+
id: task.id,
|
|
2014
|
+
filename: 'implementation-plan.md',
|
|
2015
|
+
sourcePath
|
|
2016
|
+
});
|
|
2017
|
+
const readBack = parseToolJson(await callTool(projectRoot, 'cloud', 'read_task_attachment', {
|
|
2018
|
+
taskId: task.id,
|
|
2019
|
+
attachmentId: parseToolJson(await callTool(projectRoot, 'cloud', 'get_task_attachments', { id: task.id })).attachments[0]?.attachmentId
|
|
2020
|
+
}));
|
|
2021
|
+
expect(readBack.content).toContain('# Recovered implementation plan');
|
|
2022
|
+
}
|
|
2023
|
+
finally {
|
|
2024
|
+
if (previousRoots === undefined) {
|
|
2025
|
+
delete process.env.TASKFORCE_ALLOWED_ATTACHMENT_SOURCE_ROOTS;
|
|
2026
|
+
}
|
|
2027
|
+
else {
|
|
2028
|
+
process.env.TASKFORCE_ALLOWED_ATTACHMENT_SOURCE_ROOTS = previousRoots;
|
|
2029
|
+
}
|
|
2030
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2031
|
+
fs.rmSync(agentWorkspaceRoot, { recursive: true, force: true });
|
|
2032
|
+
}
|
|
2033
|
+
});
|
|
2034
|
+
it('rejects save_task_attachment sourcePath in cloud runtime for unapproved roots', async () => {
|
|
2035
|
+
const projectRoot = createProjectRoot();
|
|
2036
|
+
const externalRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-mcp-cloud-unapproved-'));
|
|
1775
2037
|
try {
|
|
1776
2038
|
const core = new TaskforceCore({
|
|
1777
2039
|
projectRoot,
|
|
@@ -1779,16 +2041,18 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
1779
2041
|
});
|
|
1780
2042
|
const task = core.createTask({ title: 'Cloud sourcePath reject test' }, { workspaceId: 'workspace-1' });
|
|
1781
2043
|
core.close();
|
|
2044
|
+
const sourcePath = path.join(externalRoot, 'outside.md');
|
|
2045
|
+
fs.writeFileSync(sourcePath, '# Outside approved roots', 'utf8');
|
|
1782
2046
|
const result = await callTool(projectRoot, 'cloud', 'save_task_attachment', {
|
|
1783
2047
|
id: task.id,
|
|
1784
|
-
filename: '
|
|
1785
|
-
sourcePath
|
|
2048
|
+
filename: 'outside.md',
|
|
2049
|
+
sourcePath
|
|
1786
2050
|
});
|
|
1787
|
-
expect(result.content?.[0]?.text).toContain('
|
|
1788
|
-
expect(result.content?.[0]?.text).toContain('resend the attachment using inline content instead');
|
|
2051
|
+
expect(result.content?.[0]?.text).toContain('sourcePath must be within project root, a Taskforce-managed attachment source, or an approved agent workspace root');
|
|
1789
2052
|
}
|
|
1790
2053
|
finally {
|
|
1791
2054
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2055
|
+
fs.rmSync(externalRoot, { recursive: true, force: true });
|
|
1792
2056
|
}
|
|
1793
2057
|
});
|
|
1794
2058
|
it('accepts task references for save_task_attachment', async () => {
|
|
@@ -2140,7 +2404,7 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
2140
2404
|
filename: 'outside.md',
|
|
2141
2405
|
sourcePath: externalSourcePath
|
|
2142
2406
|
});
|
|
2143
|
-
expect(result.content?.[0]?.text).toContain('sourcePath must be within project root
|
|
2407
|
+
expect(result.content?.[0]?.text).toContain('sourcePath must be within project root, a Taskforce-managed attachment source, or an approved agent workspace root');
|
|
2144
2408
|
}
|
|
2145
2409
|
finally {
|
|
2146
2410
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { WorkspaceAssetEntry, WorkspaceAssetKind, WorkspaceAssetStore } from '../storage/workspaceAssetStore.js';
|
|
2
|
+
import type { McpTaskAttachmentDescriptor, TaskAttachmentRecord } from './taskAttachmentTypes.js';
|
|
3
|
+
export interface TaskAttachmentHelpersDeps {
|
|
4
|
+
activeWorkspaceId: string;
|
|
5
|
+
workspaceAssetStore: WorkspaceAssetStore | null;
|
|
6
|
+
getImageReferenceLabel: (asset: WorkspaceAssetEntry | null | undefined) => string | null;
|
|
7
|
+
getDocumentReferenceLabel: (asset: WorkspaceAssetEntry | null | undefined) => string | null;
|
|
8
|
+
inferMimeTypeFromPath: (filePath: string) => string;
|
|
9
|
+
inferAssetKind: (filePath: string, mimeType: string) => WorkspaceAssetKind;
|
|
10
|
+
isTextReadableMimeType: (mimeTypeRaw: string) => boolean;
|
|
11
|
+
isValidCanonicalDocumentAsset: (params: Pick<WorkspaceAssetEntry, 'kind' | 'storageKey' | 'originalFilename' | 'mimeType'>) => boolean;
|
|
12
|
+
canonicalAssetHelpers: {
|
|
13
|
+
getAttachmentAssetRecord: (attachment: TaskAttachmentRecord) => WorkspaceAssetEntry | null;
|
|
14
|
+
normalizeAttachmentStorageKey: (pathValue: unknown, fsPathValue: unknown) => string | null;
|
|
15
|
+
buildSyntheticAttachmentId: (taskId: string, attachment: TaskAttachmentRecord) => string;
|
|
16
|
+
toTaskAttachmentRecord: (attachment: string | TaskAttachmentRecord) => TaskAttachmentRecord;
|
|
17
|
+
};
|
|
18
|
+
documentHelpers: {
|
|
19
|
+
buildCanonicalDocumentAttachmentHint: (asset: {
|
|
20
|
+
assetId: string;
|
|
21
|
+
documentId: string | null;
|
|
22
|
+
referenceNumber?: number | null;
|
|
23
|
+
mimeType?: string | null;
|
|
24
|
+
}) => {
|
|
25
|
+
documentId: string | null;
|
|
26
|
+
preferredNextTool: string;
|
|
27
|
+
preferredWorkflow: string;
|
|
28
|
+
suggestedArgs: {
|
|
29
|
+
documentId: string;
|
|
30
|
+
};
|
|
31
|
+
guidance: string;
|
|
32
|
+
} | null;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export declare function createTaskAttachmentHelpers(deps: TaskAttachmentHelpersDeps): {
|
|
36
|
+
describeTaskAttachment: (taskId: string, attachmentInput: string | TaskAttachmentRecord) => McpTaskAttachmentDescriptor;
|
|
37
|
+
listTaskAttachmentDescriptors: (task: {
|
|
38
|
+
id: string;
|
|
39
|
+
attachments?: Array<string | TaskAttachmentRecord> | null;
|
|
40
|
+
}) => McpTaskAttachmentDescriptor[];
|
|
41
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export function createTaskAttachmentHelpers(deps) {
|
|
2
|
+
const isCanonicalDocumentAssetRecord = (asset) => deps.isValidCanonicalDocumentAsset(asset);
|
|
3
|
+
const describeTaskAttachment = (taskId, attachmentInput) => {
|
|
4
|
+
const attachment = deps.canonicalAssetHelpers.toTaskAttachmentRecord(attachmentInput);
|
|
5
|
+
const asset = deps.canonicalAssetHelpers.getAttachmentAssetRecord(attachment);
|
|
6
|
+
const storageKey = deps.canonicalAssetHelpers.normalizeAttachmentStorageKey(attachment.path, attachment.fsPath);
|
|
7
|
+
const assetId = String(attachment.assetId || asset?.assetId || '').trim() || undefined;
|
|
8
|
+
const source = (asset?.storageProvider === 'external' || /^https?:\/\//i.test(String(attachment.path || '').trim()))
|
|
9
|
+
? 'external'
|
|
10
|
+
: 'canonical';
|
|
11
|
+
const mimeType = String(asset?.mimeType
|
|
12
|
+
|| (storageKey && !/^https?:\/\//i.test(storageKey) ? deps.inferMimeTypeFromPath(storageKey) : '')
|
|
13
|
+
|| deps.inferMimeTypeFromPath(String(attachment.originalFilename || attachment.displayName || attachment.caption || attachment.path || ''))).trim() || 'application/octet-stream';
|
|
14
|
+
const kind = asset?.kind || deps.inferAssetKind(storageKey || String(attachment.path || ''), mimeType);
|
|
15
|
+
const isCanonicalDocument = Boolean(asset && isCanonicalDocumentAssetRecord(asset)) || (!asset && kind === 'document');
|
|
16
|
+
const referenceNumber = typeof attachment.referenceNumber === 'number'
|
|
17
|
+
? attachment.referenceNumber
|
|
18
|
+
: (typeof asset?.referenceNumber === 'number' ? asset.referenceNumber : null);
|
|
19
|
+
const referenceLabel = String(attachment.referenceLabel
|
|
20
|
+
|| (kind === 'image' ? deps.getImageReferenceLabel(asset) : '')
|
|
21
|
+
|| (isCanonicalDocument ? deps.getDocumentReferenceLabel(asset) : '')).trim() || null;
|
|
22
|
+
const readableAsText = source === 'canonical' && deps.isTextReadableMimeType(mimeType);
|
|
23
|
+
const canonicalDocumentHint = isCanonicalDocument && source === 'canonical' && asset
|
|
24
|
+
? deps.documentHelpers.buildCanonicalDocumentAttachmentHint(asset)
|
|
25
|
+
: null;
|
|
26
|
+
return {
|
|
27
|
+
attachmentId: assetId || deps.canonicalAssetHelpers.buildSyntheticAttachmentId(taskId, attachment),
|
|
28
|
+
...(assetId ? { assetId } : {}),
|
|
29
|
+
...(referenceNumber ? { referenceNumber } : {}),
|
|
30
|
+
...(referenceLabel ? { referenceLabel } : {}),
|
|
31
|
+
...(canonicalDocumentHint?.documentId ? { documentId: canonicalDocumentHint.documentId } : {}),
|
|
32
|
+
path: String(attachment.path || '').trim(),
|
|
33
|
+
...(storageKey && !/^https?:\/\//i.test(storageKey) ? { fsPath: storageKey } : (attachment.fsPath ? { fsPath: String(attachment.fsPath).trim() } : {})),
|
|
34
|
+
...(attachment.caption ? { caption: String(attachment.caption).trim() } : {}),
|
|
35
|
+
...(attachment.displayName ? { displayName: String(attachment.displayName).trim() } : {}),
|
|
36
|
+
...(attachment.originalFilename ? { originalFilename: String(attachment.originalFilename).trim() } : {}),
|
|
37
|
+
kind: isCanonicalDocument ? 'document' : kind,
|
|
38
|
+
mimeType,
|
|
39
|
+
sizeBytes: asset ? Number(asset.sizeBytes || 0) : null,
|
|
40
|
+
...(attachment.timestamp ? { timestamp: String(attachment.timestamp).trim() } : {}),
|
|
41
|
+
source,
|
|
42
|
+
readableAsText,
|
|
43
|
+
...(canonicalDocumentHint ? {
|
|
44
|
+
preferredNextTool: canonicalDocumentHint.preferredNextTool,
|
|
45
|
+
preferredWorkflow: canonicalDocumentHint.preferredWorkflow,
|
|
46
|
+
suggestedArgs: canonicalDocumentHint.suggestedArgs,
|
|
47
|
+
guidance: canonicalDocumentHint.guidance,
|
|
48
|
+
} : {}),
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
const listTaskAttachmentDescriptors = (task) => {
|
|
52
|
+
if (!task || !Array.isArray(task.attachments))
|
|
53
|
+
return [];
|
|
54
|
+
return task.attachments.map((attachment) => describeTaskAttachment(task.id, attachment));
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
describeTaskAttachment,
|
|
58
|
+
listTaskAttachmentDescriptors,
|
|
59
|
+
};
|
|
60
|
+
}
|