@taskforcehq/taskforce 0.3.306 → 0.3.308
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/README.md +0 -2
- package/dist/Taskforce.module.css +317 -27
- package/dist/TaskforceCore.js +1 -0
- package/dist/TaskforceCore.test.js +178 -0
- package/dist/components/activity/EntityActivityTimeline.d.ts +34 -0
- package/dist/components/activity/EntityActivityTimeline.js +81 -0
- package/dist/components/context/ContextAttachmentManager.js +15 -3
- package/dist/components/features/AgentsModule.d.ts +5 -1
- package/dist/components/features/AgentsModule.js +142 -39
- package/dist/components/features/AgentsModule.test.js +260 -11
- package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +3 -1
- package/dist/components/features/AnnotatedAttachmentWorkspace.js +118 -4
- package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +52 -0
- package/dist/components/features/DocumentIndex.d.ts +2 -2
- package/dist/components/features/DocumentIndex.js +3 -4
- package/dist/components/features/DocumentViewer.d.ts +3 -1
- package/dist/components/features/DocumentViewer.js +6 -5
- package/dist/components/features/DocumentWorkspace.d.ts +10 -2
- package/dist/components/features/DocumentWorkspace.js +15 -4
- package/dist/components/features/DocumentWorkspace.test.js +20 -3
- package/dist/components/features/TaskSettings.d.ts +2 -0
- package/dist/components/features/TaskSettings.js +83 -78
- package/dist/components/features/TaskSettings.test.js +171 -82
- package/dist/components/task/TaskActionHeader.js +11 -5
- package/dist/components/task/TaskActionHeader.test.js +10 -0
- package/dist/components/task/TaskCard.js +6 -2
- package/dist/components/task/TaskContextUpload.test.js +3 -3
- package/dist/components/task/TaskForm.js +14 -64
- package/dist/components/task/TaskKanban.js +0 -8
- package/dist/components/task/TaskList.js +5 -1
- package/dist/components/task/TaskList.test.js +4 -0
- package/dist/components/ui/HierarchyActionBadge.d.ts +10 -0
- package/dist/components/ui/HierarchyActionBadge.js +5 -0
- package/dist/components/views/AppShellHeader.d.ts +3 -1
- package/dist/components/views/AppShellHeader.js +27 -11
- package/dist/components/views/AppShellHeader.test.js +18 -1
- package/dist/components/views/PlanComparisonPage.d.ts +20 -15
- package/dist/components/views/PlanComparisonPage.js +221 -82
- package/dist/components/views/PlanComparisonPage.test.js +268 -14
- package/dist/components/views/PlansPage.js +16 -10
- package/dist/components/views/PlansPage.test.js +4 -2
- package/dist/components/views/StandaloneLayout.js +256 -69
- package/dist/components/views/WidgetView.js +10 -8
- package/dist/components/views/panels/PlanningDrawer.d.ts +19 -6
- package/dist/components/views/panels/PlanningDrawer.js +259 -51
- package/dist/components/views/panels/PlanningDrawer.test.js +392 -4
- package/dist/config/envSchema.js +1 -0
- package/dist/core/AiProfileService.d.ts +7 -0
- package/dist/core/AiProfileService.js +460 -35
- package/dist/core/AiProfiles.test.js +648 -2
- package/dist/core/AttachmentLinkService.d.ts +3 -0
- package/dist/core/AttachmentLinkService.js +58 -13
- package/dist/core/GlobalSettingsService.js +30 -0
- package/dist/core/GlobalSettingsService.test.d.ts +1 -0
- package/dist/core/GlobalSettingsService.test.js +45 -0
- package/dist/core/McpTokenService.d.ts +12 -0
- package/dist/core/McpTokenService.js +44 -1
- package/dist/core/PlanEntitlementService.d.ts +20 -0
- package/dist/core/PlanEntitlementService.js +215 -19
- package/dist/core/PlanFeatureCatalog.test.js +55 -0
- package/dist/core/PlanningEntities.test.js +366 -4
- package/dist/core/TaskActivityService.d.ts +28 -1
- package/dist/core/TaskActivityService.js +227 -46
- package/dist/core/TaskActivitySync.test.d.ts +1 -0
- package/dist/core/TaskActivitySync.test.js +90 -0
- package/dist/core/TaskAuditTimeline.test.js +96 -0
- package/dist/core/Taskforce.d.ts +204 -26
- package/dist/core/Taskforce.ids.test.js +58 -0
- package/dist/core/Taskforce.js +781 -71
- package/dist/core/Taskforce.mcpAuth.test.js +109 -0
- package/dist/core/types.d.ts +10 -0
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +2 -1
- package/dist/hooks/bootstrapTimeouts.d.ts +4 -0
- package/dist/hooks/bootstrapTimeouts.js +8 -0
- package/dist/hooks/bootstrapTimeouts.test.d.ts +1 -0
- package/dist/hooks/bootstrapTimeouts.test.js +12 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +3 -3
- package/dist/hooks/sync/orchestratorShared.js +61 -1
- package/dist/hooks/sync/recovery.js +8 -0
- package/dist/hooks/sync/recovery.test.js +5 -5
- package/dist/hooks/sync/transfers.d.ts +2 -1
- package/dist/hooks/sync/transfers.js +36 -9
- package/dist/hooks/sync/transfers.test.js +534 -2
- package/dist/hooks/useFilterSort.js +13 -13
- package/dist/hooks/useFilterSort.test.js +87 -1
- package/dist/hooks/useSyncOrchestrator.js +50 -5
- package/dist/hooks/useTaskforce.d.ts +85 -50
- package/dist/hooks/useTaskforce.js +44 -14
- package/dist/hooks/useTaskforce.runtime-routing.test.js +94 -0
- package/dist/hooks/useTaskforce.sync-behavior.test.js +99 -0
- package/dist/hooks/useWorkspaceSyncController.js +7 -1
- package/dist/hooks/useWorkspaceSyncController.test.js +48 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +14 -7
- package/dist/mcp/canonicalAssetHelpers.d.ts +16 -4
- package/dist/mcp/canonicalAssetHelpers.js +18 -10
- package/dist/mcp/canonicalAssetHelpers.test.d.ts +1 -0
- package/dist/mcp/canonicalAssetHelpers.test.js +67 -0
- package/dist/mcp/clientIntegrations.d.ts +1 -0
- package/dist/mcp/clientIntegrations.js +25 -0
- package/dist/mcp/clientIntegrations.test.js +14 -3
- package/dist/mcp/clientProfileDefaults.js +3 -3
- package/dist/mcp/collaborationRegistrar.js +7 -7
- package/dist/mcp/documentAssetRegistrar.js +113 -7
- package/dist/mcp/runtime.d.ts +1 -0
- package/dist/mcp/runtime.js +969 -122
- package/dist/mcp/runtime.test.js +1083 -11
- package/dist/mcp/taskAttachmentTypes.d.ts +1 -1
- package/dist/mcp/taskPlanningRegistrar.js +48 -0
- package/dist/mcp/toolRegistry.d.ts +56 -0
- package/dist/mcp/toolRegistry.js +8 -0
- package/dist/mcp/toolRegistry.test.js +4 -0
- package/dist/migrations/taskSchemaMigrations.js +58 -15
- package/dist/server/aiProfileAvatarAssets.d.ts +49 -0
- package/dist/server/aiProfileAvatarAssets.js +319 -0
- package/dist/server/aiProfileAvatarAssets.test.d.ts +1 -0
- package/dist/server/aiProfileAvatarAssets.test.js +213 -0
- package/dist/server/annotatedAttachmentsRoutes.test.js +66 -7
- package/dist/server/index.js +4 -0
- package/dist/server/index.test.js +6 -1
- package/dist/server/routes/admin.js +328 -9
- package/dist/server/routes/annotatedAttachments.js +51 -0
- package/dist/server/routes/auth.d.ts +11 -0
- package/dist/server/routes/auth.js +189 -27
- package/dist/server/routes/authSupport.d.ts +5 -0
- package/dist/server/routes/authSupport.js +13 -1
- package/dist/server/routes/billing.d.ts +1 -0
- package/dist/server/routes/billing.js +116 -63
- package/dist/server/routes/billing.test.js +162 -11
- package/dist/server/routes/documents.js +10 -0
- package/dist/server/routes/shared.js +0 -10
- package/dist/server/routes/sync.integration.test.js +221 -0
- package/dist/server/routes/sync.js +13 -3
- package/dist/server/routes/syncSnapshotRoutes.js +2 -0
- package/dist/server/routes/tasks.js +12 -9
- package/dist/server/routes.js +87 -8
- package/dist/server/routes.test.js +972 -23
- package/dist/shared/aiProfileSurfaceType.d.ts +1 -1
- package/dist/shared/aiProfileSurfaceType.js +10 -8
- package/dist/storage/objectStorage.d.ts +1 -0
- package/dist/storage/objectStorage.js +2 -1
- package/dist/storage/objectStorageClient.d.ts +1 -0
- package/dist/storage/objectStorageClient.js +41 -8
- package/dist/storage/objectStorageClient.test.js +26 -1
- package/dist/storage/objectStorageConfig.js +3 -1
- package/dist/storage/objectStorageConfig.test.js +13 -1
- package/dist/sync/collaborationSyncPayload.js +2 -0
- package/dist/sync/collaborationSyncPayload.test.js +1 -0
- package/dist/sync/collaborationSyncState.d.ts +3 -3
- package/dist/sync/collaborationSyncState.js +19 -12
- package/dist/sync/contentSyncPayload.d.ts +5 -1
- package/dist/sync/contentSyncPayload.js +9 -0
- package/dist/sync/contentSyncPayload.test.js +39 -0
- package/dist/sync/contentSyncState.d.ts +5 -1
- package/dist/sync/contentSyncState.js +231 -37
- package/dist/sync/planningSyncPayload.js +5 -0
- package/dist/sync/planningSyncPayload.test.js +24 -0
- package/dist/sync/syncApplyHandlers.d.ts +4 -4
- package/dist/sync/syncApplyHandlers.js +89 -31
- package/dist/sync/syncApplyHandlers.test.js +184 -4
- package/dist/sync/syncResourceAdapters.d.ts +2 -0
- package/dist/sync/syncResourceAdapters.js +2 -0
- package/dist/sync/syncService.d.ts +1 -0
- package/dist/sync/syncService.js +13 -1
- package/dist/sync/syncService.test.js +39 -0
- package/dist/sync/workspacePullFeed.d.ts +4 -0
- package/dist/sync/workspacePullFeed.js +129 -70
- package/dist/sync/workspacePullFeed.test.js +49 -0
- package/dist/sync/workspaceSyncModel.d.ts +28 -6
- package/dist/sync/workspaceSyncModel.js +159 -45
- package/dist/sync/workspaceSyncModel.test.js +269 -3
- package/dist/types.d.ts +33 -2
- package/dist/ui/agent-logos/ChatGPT.png +0 -0
- package/dist/ui/agent-logos/Gemini CLI.jpeg +0 -0
- package/dist/ui/agent-logos/antigravity.jpeg +0 -0
- package/dist/ui/agent-logos/claude-code.jpeg +0 -0
- package/dist/ui/agent-logos/codex.jpeg +0 -0
- package/dist/ui/agent-logos/cursor.png +0 -0
- package/dist/ui/agent-logos/openclaw.jpeg +0 -0
- package/dist/ui/agent-logos/windsurf.png +0 -0
- package/dist/ui/assets/AgentsModule-BS4Pi_nC.js +1 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BVKew-mH.css +1 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-CCckzWYZ.js +3 -0
- package/dist/ui/assets/ContextAttachmentManager-CfG_ER7C.js +3 -0
- package/dist/ui/assets/DocumentWorkspace-BVaWPA25.js +252 -0
- package/dist/ui/assets/DocumentWorkspace-D4rJKXzd.css +1 -0
- package/dist/ui/assets/EntityActivityTimeline-DSj0ThaK.js +1 -0
- package/dist/ui/assets/EntityActivityTimeline-DyqvvhuF.css +1 -0
- package/dist/ui/assets/{InitiativesModule-9E6ParrY.js → InitiativesModule-AxMkWtxH.js} +1 -1
- package/dist/ui/assets/PlansPage-B_z-D6Nh.css +1 -0
- package/dist/ui/assets/PlansPage-Ndt7mYfo.js +1 -0
- package/dist/ui/assets/TaskContextUpload-Dw4rTF4i.js +1 -0
- package/dist/ui/assets/TaskSettings-f2ga42_V.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-BA7jcEpH.js → WorkflowsModule-E4UL3mov.js} +4 -4
- package/dist/ui/assets/documentReferences-BOUcJm6-.js +1 -0
- package/dist/ui/assets/index-BvAbqx5Q.css +1 -0
- package/dist/ui/assets/index-MAHKvFqp.js +4 -0
- package/dist/ui/assets/{vendor-icons-DuEd_65c.js → vendor-icons-B32hGuF2.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/ui/og-image.png +0 -0
- package/dist/ui/product/agent manager.png +0 -0
- package/dist/ui/product/agent_manager_02.png +0 -0
- package/dist/utils/aiProfileEvents.d.ts +7 -0
- package/dist/utils/aiProfileEvents.js +6 -0
- package/dist/utils/assignees.d.ts +6 -0
- package/dist/utils/assignees.js +8 -1
- package/dist/utils/assignees.test.d.ts +1 -0
- package/dist/utils/assignees.test.js +25 -0
- package/dist/utils/avatarDisplay.d.ts +1 -0
- package/dist/utils/avatarDisplay.js +15 -0
- package/dist/utils/contextFiles.d.ts +3 -1
- package/dist/utils/contextFiles.js +2 -1
- package/dist/utils/taskActivity.d.ts +7 -1
- package/dist/utils/taskActivity.js +63 -1
- package/dist/utils/taskActivity.test.js +23 -1
- package/dist/utils/taskSearch.d.ts +22 -0
- package/dist/utils/taskSearch.js +87 -0
- package/dist/utils/taskSearch.test.d.ts +1 -0
- package/dist/utils/taskSearch.test.js +69 -0
- package/dist/utils/taskUtils.js +10 -3
- package/dist/utils/taskUtils.test.d.ts +1 -0
- package/dist/utils/taskUtils.test.js +19 -0
- package/package.json +1 -1
- package/dist/ui/assets/AgentsModule-DrLawwNl.js +0 -1
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +0 -1
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-Z9_whf7F.js +0 -3
- package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +0 -252
- package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +0 -1
- package/dist/ui/assets/PlansPage-CvfnMiWq.css +0 -1
- package/dist/ui/assets/PlansPage-DRXscYxH.js +0 -1
- package/dist/ui/assets/TaskSettings-DZX4jk7e.js +0 -9
- package/dist/ui/assets/index-9eT8e0Lu.css +0 -1
- package/dist/ui/assets/index-C6k75jr8.js +0 -6
|
@@ -4,6 +4,7 @@ const LEGACY_REMOVED_PLAN_FEATURE_KEYS = Object.freeze([
|
|
|
4
4
|
'documents.workspace_access'
|
|
5
5
|
]);
|
|
6
6
|
const LEGACY_WORKSPACES_FEATURE_KEY = 'workspace.switching';
|
|
7
|
+
const CUSTOM_PLAN_FEATURE_KEY_PREFIX = 'marketing.';
|
|
7
8
|
/**
|
|
8
9
|
* Handles all plan catalog, plan version, and account entitlement logic.
|
|
9
10
|
* Extracted from Taskforce.ts to reduce monolith size.
|
|
@@ -49,9 +50,105 @@ export class PlanEntitlementService {
|
|
|
49
50
|
return null;
|
|
50
51
|
return Math.max(0, Math.floor(numeric));
|
|
51
52
|
}
|
|
53
|
+
normalizeOptionalCatalogDescriptionVisible(raw) {
|
|
54
|
+
if (raw === null || raw === undefined || raw === '')
|
|
55
|
+
return null;
|
|
56
|
+
return raw === false || raw === 0 || raw === '0' ? false : true;
|
|
57
|
+
}
|
|
58
|
+
isBuiltInPlanFeatureKey(featureKey) {
|
|
59
|
+
return DEFAULT_PLAN_FEATURE_CATALOG.some((feature) => feature.featureKey === featureKey);
|
|
60
|
+
}
|
|
61
|
+
normalizeRequiredCatalogLabel(raw) {
|
|
62
|
+
return String(raw ?? '').trim();
|
|
63
|
+
}
|
|
64
|
+
slugifyCustomPlanFeatureLabel(label) {
|
|
65
|
+
const base = label
|
|
66
|
+
.trim()
|
|
67
|
+
.toLowerCase()
|
|
68
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
69
|
+
.replace(/^_+|_+$/g, '');
|
|
70
|
+
return base || generateUuidV7().replace(/-/g, '_');
|
|
71
|
+
}
|
|
72
|
+
featureKeyExists(featureKey) {
|
|
73
|
+
if (this.isBuiltInPlanFeatureKey(featureKey))
|
|
74
|
+
return true;
|
|
75
|
+
const row = this.db.prepare(`
|
|
76
|
+
SELECT 1
|
|
77
|
+
FROM plan_feature_catalog_overrides
|
|
78
|
+
WHERE tenant_id = ? AND feature_key = ?
|
|
79
|
+
LIMIT 1
|
|
80
|
+
`).get(this.tenantId, featureKey);
|
|
81
|
+
return Boolean(row);
|
|
82
|
+
}
|
|
83
|
+
generateUniqueCustomPlanFeatureKey(label) {
|
|
84
|
+
const base = `${CUSTOM_PLAN_FEATURE_KEY_PREFIX}${this.slugifyCustomPlanFeatureLabel(label)}`;
|
|
85
|
+
let featureKey = base;
|
|
86
|
+
let suffix = 2;
|
|
87
|
+
while (this.featureKeyExists(featureKey)) {
|
|
88
|
+
featureKey = `${base}_${suffix}`;
|
|
89
|
+
suffix += 1;
|
|
90
|
+
}
|
|
91
|
+
return featureKey;
|
|
92
|
+
}
|
|
93
|
+
listCustomPlanFeatureCatalogEntries() {
|
|
94
|
+
const rows = this.db.prepare(`
|
|
95
|
+
SELECT feature_key, label, description, public_label, public_description, public_description_visible, public_display_order
|
|
96
|
+
FROM plan_feature_catalog_overrides
|
|
97
|
+
WHERE tenant_id = ? AND is_custom = 1
|
|
98
|
+
ORDER BY updated_at ASC, created_at ASC, feature_key ASC
|
|
99
|
+
`).all(this.tenantId);
|
|
100
|
+
const features = [];
|
|
101
|
+
for (const row of rows) {
|
|
102
|
+
const featureKey = this.canonicalizePlanFeatureKey(row.feature_key);
|
|
103
|
+
const label = this.normalizeRequiredCatalogLabel(row.label);
|
|
104
|
+
if (!featureKey || !label)
|
|
105
|
+
continue;
|
|
106
|
+
features.push({
|
|
107
|
+
featureKey,
|
|
108
|
+
label,
|
|
109
|
+
description: this.normalizeRequiredCatalogLabel(row.description),
|
|
110
|
+
isCustom: true,
|
|
111
|
+
publicLabel: this.normalizeOptionalCatalogMarketingCopy(row.public_label),
|
|
112
|
+
publicDescription: this.normalizeOptionalCatalogMarketingCopy(row.public_description),
|
|
113
|
+
publicDescriptionVisible: this.normalizeOptionalCatalogDescriptionVisible(row.public_description_visible),
|
|
114
|
+
publicDisplayOrder: this.normalizeOptionalCatalogDisplayOrder(row.public_display_order),
|
|
115
|
+
allowedAccessModes: ['enabled', 'disabled'],
|
|
116
|
+
configTemplate: {}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return features;
|
|
120
|
+
}
|
|
121
|
+
getCustomPlanFeatureCatalogEntry(featureKeyRaw) {
|
|
122
|
+
const featureKey = this.canonicalizePlanFeatureKey(featureKeyRaw);
|
|
123
|
+
if (!featureKey)
|
|
124
|
+
return null;
|
|
125
|
+
const row = this.db.prepare(`
|
|
126
|
+
SELECT feature_key, label, description, public_label, public_description, public_description_visible, public_display_order
|
|
127
|
+
FROM plan_feature_catalog_overrides
|
|
128
|
+
WHERE tenant_id = ? AND feature_key = ? AND is_custom = 1
|
|
129
|
+
LIMIT 1
|
|
130
|
+
`).get(this.tenantId, featureKey);
|
|
131
|
+
if (!row)
|
|
132
|
+
return null;
|
|
133
|
+
const label = this.normalizeRequiredCatalogLabel(row.label);
|
|
134
|
+
if (!label)
|
|
135
|
+
return null;
|
|
136
|
+
return {
|
|
137
|
+
featureKey,
|
|
138
|
+
label,
|
|
139
|
+
description: this.normalizeRequiredCatalogLabel(row.description),
|
|
140
|
+
isCustom: true,
|
|
141
|
+
publicLabel: this.normalizeOptionalCatalogMarketingCopy(row.public_label),
|
|
142
|
+
publicDescription: this.normalizeOptionalCatalogMarketingCopy(row.public_description),
|
|
143
|
+
publicDescriptionVisible: this.normalizeOptionalCatalogDescriptionVisible(row.public_description_visible),
|
|
144
|
+
publicDisplayOrder: this.normalizeOptionalCatalogDisplayOrder(row.public_display_order),
|
|
145
|
+
allowedAccessModes: ['enabled', 'disabled'],
|
|
146
|
+
configTemplate: {}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
52
149
|
listPlanFeatureCatalogOverrideMap() {
|
|
53
150
|
const rows = this.db.prepare(`
|
|
54
|
-
SELECT feature_key, public_label, public_description, public_display_order
|
|
151
|
+
SELECT feature_key, public_label, public_description, public_description_visible, public_display_order, is_custom
|
|
55
152
|
FROM plan_feature_catalog_overrides
|
|
56
153
|
WHERE tenant_id = ?
|
|
57
154
|
`).all(this.tenantId);
|
|
@@ -61,12 +158,15 @@ export class PlanEntitlementService {
|
|
|
61
158
|
const featureKey = this.canonicalizePlanFeatureKey(originalFeatureKey);
|
|
62
159
|
if (!featureKey)
|
|
63
160
|
continue;
|
|
161
|
+
if (Number(row.is_custom || 0) === 1)
|
|
162
|
+
continue;
|
|
64
163
|
if (featureKey === WORKSPACES_FEATURE_KEY && originalFeatureKey === LEGACY_WORKSPACES_FEATURE_KEY && overrides.has(featureKey)) {
|
|
65
164
|
continue;
|
|
66
165
|
}
|
|
67
166
|
overrides.set(featureKey, {
|
|
68
167
|
publicLabel: this.normalizeOptionalCatalogMarketingCopy(row.public_label),
|
|
69
168
|
publicDescription: this.normalizeOptionalCatalogMarketingCopy(row.public_description),
|
|
169
|
+
publicDescriptionVisible: this.normalizeOptionalCatalogDescriptionVisible(row.public_description_visible),
|
|
70
170
|
publicDisplayOrder: this.normalizeOptionalCatalogDisplayOrder(row.public_display_order)
|
|
71
171
|
});
|
|
72
172
|
}
|
|
@@ -689,6 +789,7 @@ export class PlanEntitlementService {
|
|
|
689
789
|
WHERE bpm.tenant_id = ?
|
|
690
790
|
AND pv.plan_id = ?
|
|
691
791
|
AND bpm.active = 1
|
|
792
|
+
AND COALESCE(bpm.pricing_audience, 'public') = 'public'
|
|
692
793
|
`).get(this.tenantId, planId);
|
|
693
794
|
return Number(row?.count || 0) > 0;
|
|
694
795
|
}
|
|
@@ -702,6 +803,7 @@ export class PlanEntitlementService {
|
|
|
702
803
|
WHERE tenant_id = ?
|
|
703
804
|
AND plan_version_id = ?
|
|
704
805
|
AND active = 1
|
|
806
|
+
AND COALESCE(pricing_audience, 'public') = 'public'
|
|
705
807
|
AND pricing_type = 'free'
|
|
706
808
|
`).get(this.tenantId, planVersionId);
|
|
707
809
|
return Number(row?.count || 0) > 0;
|
|
@@ -1054,7 +1156,7 @@ export class PlanEntitlementService {
|
|
|
1054
1156
|
const mappingRow = this.db.prepare(`
|
|
1055
1157
|
SELECT plan_version_id
|
|
1056
1158
|
FROM billing_price_mappings
|
|
1057
|
-
WHERE tenant_id = ? AND stripe_price_id = ? AND active = 1
|
|
1159
|
+
WHERE tenant_id = ? AND stripe_price_id = ? AND active = 1 AND COALESCE(pricing_audience, 'public') = 'public'
|
|
1058
1160
|
ORDER BY updated_at DESC, created_at DESC
|
|
1059
1161
|
LIMIT 1
|
|
1060
1162
|
`).get(this.tenantId, stripePriceId);
|
|
@@ -1490,31 +1592,104 @@ export class PlanEntitlementService {
|
|
|
1490
1592
|
}
|
|
1491
1593
|
listPlanFeatureCatalog() {
|
|
1492
1594
|
const overrides = this.listPlanFeatureCatalogOverrideMap();
|
|
1493
|
-
return
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1595
|
+
return [
|
|
1596
|
+
...DEFAULT_PLAN_FEATURE_CATALOG.map((feature) => ({
|
|
1597
|
+
featureKey: feature.featureKey,
|
|
1598
|
+
label: feature.label,
|
|
1599
|
+
description: feature.description,
|
|
1600
|
+
isCustom: false,
|
|
1601
|
+
publicLabel: overrides.get(feature.featureKey)?.publicLabel ?? feature.publicLabel ?? null,
|
|
1602
|
+
publicDescription: overrides.get(feature.featureKey)?.publicDescription ?? feature.publicDescription ?? null,
|
|
1603
|
+
publicDescriptionVisible: overrides.get(feature.featureKey)?.publicDescriptionVisible ?? feature.publicDescriptionVisible ?? null,
|
|
1604
|
+
publicDisplayOrder: overrides.get(feature.featureKey)?.publicDisplayOrder ?? feature.publicDisplayOrder ?? null,
|
|
1605
|
+
allowedAccessModes: [...feature.allowedAccessModes],
|
|
1606
|
+
configTemplate: { ...feature.configTemplate },
|
|
1607
|
+
...(feature.dependsOn?.length ? { dependsOn: [...feature.dependsOn] } : {})
|
|
1608
|
+
})),
|
|
1609
|
+
...this.listCustomPlanFeatureCatalogEntries()
|
|
1610
|
+
];
|
|
1611
|
+
}
|
|
1612
|
+
createCustomPlanFeatureCatalogEntry(input) {
|
|
1613
|
+
const label = this.normalizeRequiredCatalogLabel(input.label);
|
|
1614
|
+
const description = this.normalizeRequiredCatalogLabel(input.description);
|
|
1615
|
+
if (!label)
|
|
1616
|
+
throw new TaskforceRuleError('Feature label is required', 400, 'PLAN_FEATURE_LABEL_REQUIRED');
|
|
1617
|
+
const featureKey = this.generateUniqueCustomPlanFeatureKey(label);
|
|
1618
|
+
const publicLabel = this.normalizeOptionalCatalogMarketingCopy(input.publicLabel);
|
|
1619
|
+
const publicDescription = this.normalizeOptionalCatalogMarketingCopy(input.publicDescription);
|
|
1620
|
+
const publicDescriptionVisible = this.normalizeOptionalCatalogDescriptionVisible(input.publicDescriptionVisible);
|
|
1621
|
+
const publicDisplayOrder = this.normalizeOptionalCatalogDisplayOrder(input.publicDisplayOrder);
|
|
1622
|
+
const now = new Date().toISOString();
|
|
1623
|
+
this.db.prepare(`
|
|
1624
|
+
INSERT INTO plan_feature_catalog_overrides (
|
|
1625
|
+
tenant_id,
|
|
1626
|
+
feature_key,
|
|
1627
|
+
label,
|
|
1628
|
+
description,
|
|
1629
|
+
is_custom,
|
|
1630
|
+
public_label,
|
|
1631
|
+
public_description,
|
|
1632
|
+
public_description_visible,
|
|
1633
|
+
public_display_order,
|
|
1634
|
+
created_at,
|
|
1635
|
+
updated_at
|
|
1636
|
+
)
|
|
1637
|
+
VALUES (?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?)
|
|
1638
|
+
`).run(this.tenantId, featureKey, label, description, publicLabel, publicDescription, publicDescriptionVisible === null ? null : (publicDescriptionVisible ? 1 : 0), publicDisplayOrder, now, now);
|
|
1639
|
+
return this.getCustomPlanFeatureCatalogEntry(featureKey) || {
|
|
1640
|
+
featureKey,
|
|
1641
|
+
label,
|
|
1642
|
+
description,
|
|
1643
|
+
isCustom: true,
|
|
1644
|
+
publicLabel,
|
|
1645
|
+
publicDescription,
|
|
1646
|
+
publicDescriptionVisible,
|
|
1647
|
+
publicDisplayOrder,
|
|
1648
|
+
allowedAccessModes: ['enabled', 'disabled'],
|
|
1649
|
+
configTemplate: {}
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
deleteCustomPlanFeatureCatalogEntry(featureKeyRaw) {
|
|
1653
|
+
const featureKey = this.canonicalizePlanFeatureKey(featureKeyRaw);
|
|
1654
|
+
if (!featureKey)
|
|
1655
|
+
throw new TaskforceRuleError('featureKey is required', 400, 'FEATURE_KEY_REQUIRED');
|
|
1656
|
+
if (this.isBuiltInPlanFeatureKey(featureKey)) {
|
|
1657
|
+
throw new TaskforceRuleError('Built-in plan features cannot be deleted', 400, 'PLAN_FEATURE_DELETE_BUILTIN_FORBIDDEN');
|
|
1658
|
+
}
|
|
1659
|
+
const existing = this.getCustomPlanFeatureCatalogEntry(featureKey);
|
|
1660
|
+
if (!existing)
|
|
1661
|
+
throw new TaskforceRuleError('Unknown custom plan feature key', 404, 'PLAN_FEATURE_NOT_FOUND');
|
|
1662
|
+
const tx = this.db.transaction(() => {
|
|
1663
|
+
this.db.prepare(`
|
|
1664
|
+
DELETE FROM plan_feature_catalog_overrides
|
|
1665
|
+
WHERE tenant_id = ? AND feature_key = ? AND is_custom = 1
|
|
1666
|
+
`).run(this.tenantId, featureKey);
|
|
1667
|
+
this.db.prepare(`
|
|
1668
|
+
DELETE FROM plan_feature_matrix
|
|
1669
|
+
WHERE tenant_id = ? AND feature_key = ?
|
|
1670
|
+
`).run(this.tenantId, featureKey);
|
|
1671
|
+
});
|
|
1672
|
+
tx();
|
|
1504
1673
|
}
|
|
1505
1674
|
updatePlanFeatureCatalogMarketingCopy(input) {
|
|
1506
1675
|
const featureKey = this.canonicalizePlanFeatureKey(input.featureKey);
|
|
1507
1676
|
if (!featureKey)
|
|
1508
1677
|
throw new TaskforceRuleError('featureKey is required', 400, 'FEATURE_KEY_REQUIRED');
|
|
1509
1678
|
const catalogFeature = DEFAULT_PLAN_FEATURE_CATALOG.find((feature) => feature.featureKey === featureKey);
|
|
1510
|
-
|
|
1679
|
+
const customFeature = this.getCustomPlanFeatureCatalogEntry(featureKey);
|
|
1680
|
+
if (!catalogFeature && !customFeature)
|
|
1511
1681
|
throw new TaskforceRuleError('Unknown plan feature key', 404, 'PLAN_FEATURE_NOT_FOUND');
|
|
1682
|
+
const label = this.normalizeRequiredCatalogLabel(input.label ?? customFeature?.label ?? '');
|
|
1683
|
+
const description = this.normalizeRequiredCatalogLabel(input.description ?? customFeature?.description ?? '');
|
|
1684
|
+
if (customFeature && !label)
|
|
1685
|
+
throw new TaskforceRuleError('Feature label is required', 400, 'PLAN_FEATURE_LABEL_REQUIRED');
|
|
1512
1686
|
const publicLabel = this.normalizeOptionalCatalogMarketingCopy(input.publicLabel);
|
|
1513
1687
|
const publicDescription = this.normalizeOptionalCatalogMarketingCopy(input.publicDescription);
|
|
1688
|
+
const publicDescriptionVisible = this.normalizeOptionalCatalogDescriptionVisible(input.publicDescriptionVisible);
|
|
1514
1689
|
const publicDisplayOrder = this.normalizeOptionalCatalogDisplayOrder(input.publicDisplayOrder);
|
|
1515
1690
|
const now = new Date().toISOString();
|
|
1516
1691
|
const tx = this.db.transaction(() => {
|
|
1517
|
-
if (!publicLabel && !publicDescription && publicDisplayOrder === null) {
|
|
1692
|
+
if (!customFeature && !publicLabel && !publicDescription && publicDescriptionVisible === null && publicDisplayOrder === null) {
|
|
1518
1693
|
this.db.prepare(`
|
|
1519
1694
|
DELETE FROM plan_feature_catalog_overrides
|
|
1520
1695
|
WHERE tenant_id = ? AND feature_key = ?
|
|
@@ -1525,32 +1700,53 @@ export class PlanEntitlementService {
|
|
|
1525
1700
|
INSERT INTO plan_feature_catalog_overrides (
|
|
1526
1701
|
tenant_id,
|
|
1527
1702
|
feature_key,
|
|
1703
|
+
label,
|
|
1704
|
+
description,
|
|
1705
|
+
is_custom,
|
|
1528
1706
|
public_label,
|
|
1529
1707
|
public_description,
|
|
1708
|
+
public_description_visible,
|
|
1530
1709
|
public_display_order,
|
|
1531
1710
|
created_at,
|
|
1532
1711
|
updated_at
|
|
1533
1712
|
)
|
|
1534
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1713
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1535
1714
|
ON CONFLICT (tenant_id, feature_key) DO UPDATE SET
|
|
1715
|
+
label = excluded.label,
|
|
1716
|
+
description = excluded.description,
|
|
1717
|
+
is_custom = excluded.is_custom,
|
|
1536
1718
|
public_label = excluded.public_label,
|
|
1537
1719
|
public_description = excluded.public_description,
|
|
1720
|
+
public_description_visible = excluded.public_description_visible,
|
|
1538
1721
|
public_display_order = excluded.public_display_order,
|
|
1539
1722
|
updated_at = excluded.updated_at
|
|
1540
|
-
`).run(this.tenantId, featureKey, publicLabel, publicDescription, publicDisplayOrder, now, now);
|
|
1723
|
+
`).run(this.tenantId, featureKey, customFeature ? label : null, customFeature ? description : null, customFeature ? 1 : 0, publicLabel, publicDescription, publicDescriptionVisible === null ? null : (publicDescriptionVisible ? 1 : 0), publicDisplayOrder, now, now);
|
|
1541
1724
|
});
|
|
1542
1725
|
tx();
|
|
1543
|
-
return this.listPlanFeatureCatalog().find((feature) => feature.featureKey === featureKey) || {
|
|
1726
|
+
return this.listPlanFeatureCatalog().find((feature) => feature.featureKey === featureKey) || (customFeature ? {
|
|
1727
|
+
featureKey,
|
|
1728
|
+
label,
|
|
1729
|
+
description,
|
|
1730
|
+
isCustom: true,
|
|
1731
|
+
publicLabel,
|
|
1732
|
+
publicDescription,
|
|
1733
|
+
publicDescriptionVisible,
|
|
1734
|
+
publicDisplayOrder,
|
|
1735
|
+
allowedAccessModes: ['enabled', 'disabled'],
|
|
1736
|
+
configTemplate: {}
|
|
1737
|
+
} : {
|
|
1544
1738
|
featureKey: catalogFeature.featureKey,
|
|
1545
1739
|
label: catalogFeature.label,
|
|
1546
1740
|
description: catalogFeature.description,
|
|
1741
|
+
isCustom: false,
|
|
1547
1742
|
publicLabel,
|
|
1548
1743
|
publicDescription,
|
|
1744
|
+
publicDescriptionVisible,
|
|
1549
1745
|
publicDisplayOrder,
|
|
1550
1746
|
allowedAccessModes: [...catalogFeature.allowedAccessModes],
|
|
1551
1747
|
configTemplate: { ...catalogFeature.configTemplate },
|
|
1552
1748
|
...(catalogFeature.dependsOn?.length ? { dependsOn: [...catalogFeature.dependsOn] } : {})
|
|
1553
|
-
};
|
|
1749
|
+
});
|
|
1554
1750
|
}
|
|
1555
1751
|
listPlanVersions(planId) {
|
|
1556
1752
|
const normalizedPlanId = String(planId || '').trim().toLowerCase();
|
|
@@ -17,33 +17,40 @@ describe('TaskforceCore plan feature catalog marketing copy', () => {
|
|
|
17
17
|
expect(initialFeature?.label).toBe('AI Profiles');
|
|
18
18
|
expect(initialFeature?.publicLabel ?? null).toBeNull();
|
|
19
19
|
expect(initialFeature?.publicDescription ?? null).toBeNull();
|
|
20
|
+
expect(initialFeature?.publicDescriptionVisible ?? null).toBeNull();
|
|
20
21
|
expect(initialFeature?.publicDisplayOrder).toBe(20);
|
|
21
22
|
const updated = core.updatePlanFeatureCatalogMarketingCopy({
|
|
22
23
|
featureKey: 'collaboration.ai_profiles',
|
|
23
24
|
publicLabel: 'AI teammates',
|
|
24
25
|
publicDescription: 'Add reusable AI teammates to your workspace.',
|
|
26
|
+
publicDescriptionVisible: false,
|
|
25
27
|
publicDisplayOrder: 24
|
|
26
28
|
});
|
|
27
29
|
expect(updated.publicLabel).toBe('AI teammates');
|
|
28
30
|
expect(updated.publicDescription).toBe('Add reusable AI teammates to your workspace.');
|
|
31
|
+
expect(updated.publicDescriptionVisible).toBe(false);
|
|
29
32
|
expect(updated.publicDisplayOrder).toBe(24);
|
|
30
33
|
const persisted = core.listPlanFeatureCatalog().find((feature) => feature.featureKey === 'collaboration.ai_profiles');
|
|
31
34
|
expect(persisted?.publicLabel).toBe('AI teammates');
|
|
32
35
|
expect(persisted?.publicDescription).toBe('Add reusable AI teammates to your workspace.');
|
|
36
|
+
expect(persisted?.publicDescriptionVisible).toBe(false);
|
|
33
37
|
expect(persisted?.publicDisplayOrder).toBe(24);
|
|
34
38
|
const cleared = core.updatePlanFeatureCatalogMarketingCopy({
|
|
35
39
|
featureKey: 'collaboration.ai_profiles',
|
|
36
40
|
publicLabel: ' ',
|
|
37
41
|
publicDescription: '',
|
|
42
|
+
publicDescriptionVisible: null,
|
|
38
43
|
publicDisplayOrder: null
|
|
39
44
|
});
|
|
40
45
|
expect(cleared.publicLabel ?? null).toBeNull();
|
|
41
46
|
expect(cleared.publicDescription ?? null).toBeNull();
|
|
47
|
+
expect(cleared.publicDescriptionVisible ?? null).toBeNull();
|
|
42
48
|
expect(cleared.publicDisplayOrder).toBe(20);
|
|
43
49
|
const fallback = core.listPlanFeatureCatalog().find((feature) => feature.featureKey === 'collaboration.ai_profiles');
|
|
44
50
|
expect(fallback?.label).toBe('AI Profiles');
|
|
45
51
|
expect(fallback?.publicLabel ?? null).toBeNull();
|
|
46
52
|
expect(fallback?.publicDescription ?? null).toBeNull();
|
|
53
|
+
expect(fallback?.publicDescriptionVisible ?? null).toBeNull();
|
|
47
54
|
expect(fallback?.publicDisplayOrder).toBe(20);
|
|
48
55
|
core.close();
|
|
49
56
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
@@ -81,4 +88,52 @@ describe('TaskforceCore plan feature catalog marketing copy', () => {
|
|
|
81
88
|
core.close();
|
|
82
89
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
83
90
|
});
|
|
91
|
+
it('creates, updates, and deletes custom feature catalog entries', () => {
|
|
92
|
+
const projectRoot = makeProjectRoot();
|
|
93
|
+
const core = new TaskforceCore({
|
|
94
|
+
projectRoot,
|
|
95
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
96
|
+
});
|
|
97
|
+
const created = core.createCustomPlanFeatureCatalogEntry({
|
|
98
|
+
label: 'Unlimited Local Workspaces',
|
|
99
|
+
description: 'Run unlimited local workspaces on your device.',
|
|
100
|
+
publicDescriptionVisible: false,
|
|
101
|
+
publicDisplayOrder: 12
|
|
102
|
+
});
|
|
103
|
+
expect(created.featureKey).toMatch(/^marketing\./);
|
|
104
|
+
expect(created.isCustom).toBe(true);
|
|
105
|
+
expect(created.label).toBe('Unlimited Local Workspaces');
|
|
106
|
+
expect(created.description).toBe('Run unlimited local workspaces on your device.');
|
|
107
|
+
expect(created.publicDescriptionVisible).toBe(false);
|
|
108
|
+
expect(created.publicDisplayOrder).toBe(12);
|
|
109
|
+
const updated = core.updatePlanFeatureCatalogMarketingCopy({
|
|
110
|
+
featureKey: created.featureKey,
|
|
111
|
+
label: 'Unlimited Local Workspaces',
|
|
112
|
+
description: 'Run unlimited local workspaces offline on your device.',
|
|
113
|
+
publicLabel: 'Unlimited Local Workspaces',
|
|
114
|
+
publicDescription: 'Create as many fully local workspaces as you need.',
|
|
115
|
+
publicDescriptionVisible: true,
|
|
116
|
+
publicDisplayOrder: 14
|
|
117
|
+
});
|
|
118
|
+
expect(updated.isCustom).toBe(true);
|
|
119
|
+
expect(updated.description).toBe('Run unlimited local workspaces offline on your device.');
|
|
120
|
+
expect(updated.publicLabel).toBe('Unlimited Local Workspaces');
|
|
121
|
+
expect(updated.publicDescription).toBe('Create as many fully local workspaces as you need.');
|
|
122
|
+
expect(updated.publicDescriptionVisible).toBe(true);
|
|
123
|
+
expect(updated.publicDisplayOrder).toBe(14);
|
|
124
|
+
const plan = core.upsertPlanCatalog({ planId: 'free', displayName: 'Free', status: 'active' });
|
|
125
|
+
expect(plan.planId).toBe('free');
|
|
126
|
+
core.updatePlanVersionFeatures({
|
|
127
|
+
planVersionId: 'free-v1',
|
|
128
|
+
features: [
|
|
129
|
+
{ featureKey: created.featureKey, access: 'enabled', config: {} }
|
|
130
|
+
]
|
|
131
|
+
});
|
|
132
|
+
expect(core.getPlanVersionFeatures('free', 'free-v1').features[created.featureKey]?.access).toBe('enabled');
|
|
133
|
+
core.deleteCustomPlanFeatureCatalogEntry(created.featureKey);
|
|
134
|
+
expect(core.listPlanFeatureCatalog().some((feature) => feature.featureKey === created.featureKey)).toBe(false);
|
|
135
|
+
expect(core.getPlanVersionFeatures('free', 'free-v1').features[created.featureKey]).toBeUndefined();
|
|
136
|
+
core.close();
|
|
137
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
138
|
+
});
|
|
84
139
|
});
|