claude-cli-advanced-starter-pack 1.0.12 → 1.0.13

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.
@@ -0,0 +1,457 @@
1
+ /**
2
+ * Smart Merge Utility
3
+ *
4
+ * Provides asset comparison, diff generation, and merge exploration
5
+ * for the CCASP update system. When users have customized assets
6
+ * (commands, skills, agents, hooks), this module helps them understand
7
+ * what changes an update would bring and offers intelligent merge options.
8
+ */
9
+
10
+ import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
11
+ import { join, basename, dirname } from 'path';
12
+ import { fileURLToPath } from 'url';
13
+ import { execSync } from 'child_process';
14
+ import {
15
+ loadUsageTracking,
16
+ getCustomizedUsedAssets,
17
+ isAssetCustomized,
18
+ } from './version-check.js';
19
+
20
+ const __filename = fileURLToPath(import.meta.url);
21
+ const __dirname = dirname(__filename);
22
+
23
+ /**
24
+ * Asset type to file path mapping
25
+ */
26
+ const ASSET_PATHS = {
27
+ commands: {
28
+ local: (projectDir, name) => join(projectDir, '.claude', 'commands', `${name}.md`),
29
+ template: (name) => join(__dirname, '..', '..', 'templates', 'commands', `${name}.template.md`),
30
+ extension: '.md',
31
+ },
32
+ skills: {
33
+ local: (projectDir, name) => join(projectDir, '.claude', 'skills', name, 'SKILL.md'),
34
+ template: (name) => join(__dirname, '..', '..', 'templates', 'skills', name, 'SKILL.template.md'),
35
+ extension: '.md',
36
+ },
37
+ agents: {
38
+ local: (projectDir, name) => join(projectDir, '.claude', 'agents', `${name}.md`),
39
+ template: (name) => join(__dirname, '..', '..', 'templates', 'agents', `${name}.template.md`),
40
+ extension: '.md',
41
+ },
42
+ hooks: {
43
+ local: (projectDir, name) => join(projectDir, '.claude', 'hooks', `${name}.js`),
44
+ template: (name) => join(__dirname, '..', '..', 'templates', 'hooks', `${name}.template.js`),
45
+ extension: '.js',
46
+ },
47
+ };
48
+
49
+ /**
50
+ * Get the local (user's) version of an asset
51
+ */
52
+ export function getLocalAsset(assetType, assetName, projectDir = process.cwd()) {
53
+ const pathConfig = ASSET_PATHS[assetType];
54
+ if (!pathConfig) return null;
55
+
56
+ const localPath = pathConfig.local(projectDir, assetName);
57
+
58
+ if (!existsSync(localPath)) {
59
+ return null;
60
+ }
61
+
62
+ try {
63
+ return {
64
+ path: localPath,
65
+ content: readFileSync(localPath, 'utf8'),
66
+ stats: statSync(localPath),
67
+ };
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Get the template (package) version of an asset
75
+ */
76
+ export function getTemplateAsset(assetType, assetName) {
77
+ const pathConfig = ASSET_PATHS[assetType];
78
+ if (!pathConfig) return null;
79
+
80
+ const templatePath = pathConfig.template(assetName);
81
+
82
+ if (!existsSync(templatePath)) {
83
+ return null;
84
+ }
85
+
86
+ try {
87
+ return {
88
+ path: templatePath,
89
+ content: readFileSync(templatePath, 'utf8'),
90
+ stats: statSync(templatePath),
91
+ };
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Compare two versions of an asset and generate a diff summary
99
+ * Returns an object with change analysis
100
+ */
101
+ export function compareAssetVersions(localContent, templateContent) {
102
+ if (!localContent || !templateContent) {
103
+ return {
104
+ identical: false,
105
+ hasChanges: true,
106
+ error: 'Missing content for comparison',
107
+ };
108
+ }
109
+
110
+ // Normalize line endings
111
+ const normalizedLocal = localContent.replace(/\r\n/g, '\n').trim();
112
+ const normalizedTemplate = templateContent.replace(/\r\n/g, '\n').trim();
113
+
114
+ if (normalizedLocal === normalizedTemplate) {
115
+ return {
116
+ identical: true,
117
+ hasChanges: false,
118
+ summary: 'No differences found',
119
+ };
120
+ }
121
+
122
+ // Split into lines for analysis
123
+ const localLines = normalizedLocal.split('\n');
124
+ const templateLines = normalizedTemplate.split('\n');
125
+
126
+ // Simple line-by-line diff analysis
127
+ const changes = {
128
+ added: [],
129
+ removed: [],
130
+ modified: [],
131
+ };
132
+
133
+ // Create line maps for comparison
134
+ const localLineSet = new Set(localLines);
135
+ const templateLineSet = new Set(templateLines);
136
+
137
+ // Lines in template but not in local (would be added by update)
138
+ for (const line of templateLines) {
139
+ if (!localLineSet.has(line) && line.trim()) {
140
+ changes.added.push(line);
141
+ }
142
+ }
143
+
144
+ // Lines in local but not in template (user customizations)
145
+ for (const line of localLines) {
146
+ if (!templateLineSet.has(line) && line.trim()) {
147
+ changes.removed.push(line);
148
+ }
149
+ }
150
+
151
+ // Analyze change significance
152
+ const significance = analyzeChangeSignificance(changes, localContent, templateContent);
153
+
154
+ return {
155
+ identical: false,
156
+ hasChanges: true,
157
+ changes,
158
+ significance,
159
+ stats: {
160
+ localLines: localLines.length,
161
+ templateLines: templateLines.length,
162
+ addedLines: changes.added.length,
163
+ removedLines: changes.removed.length,
164
+ },
165
+ summary: generateChangeSummary(changes, significance),
166
+ };
167
+ }
168
+
169
+ /**
170
+ * Analyze the significance of changes
171
+ */
172
+ function analyzeChangeSignificance(changes, localContent, templateContent) {
173
+ const significance = {
174
+ level: 'low', // low, medium, high
175
+ reasons: [],
176
+ };
177
+
178
+ // Check for structural changes (high significance)
179
+ const structuralPatterns = [
180
+ /^#{1,3}\s/, // Markdown headers
181
+ /^---$/, // YAML frontmatter
182
+ /^export\s+(default\s+)?function/, // Function exports
183
+ /^module\.exports/, // CommonJS exports
184
+ /^import\s+/, // Import statements
185
+ /^const\s+\w+\s*=\s*require/, // Require statements
186
+ ];
187
+
188
+ for (const line of changes.added.concat(changes.removed)) {
189
+ for (const pattern of structuralPatterns) {
190
+ if (pattern.test(line)) {
191
+ significance.level = 'high';
192
+ significance.reasons.push('Structural changes detected');
193
+ break;
194
+ }
195
+ }
196
+ }
197
+
198
+ // Check for configuration changes (medium significance)
199
+ const configPatterns = [
200
+ /^\s*"?\w+"?\s*:\s*/, // JSON/YAML config
201
+ /^[A-Z_]+\s*=/, // Environment variables
202
+ /timeout|limit|threshold|max|min/i, // Configuration values
203
+ ];
204
+
205
+ if (significance.level !== 'high') {
206
+ for (const line of changes.added.concat(changes.removed)) {
207
+ for (const pattern of configPatterns) {
208
+ if (pattern.test(line)) {
209
+ significance.level = 'medium';
210
+ significance.reasons.push('Configuration changes detected');
211
+ break;
212
+ }
213
+ }
214
+ }
215
+ }
216
+
217
+ // Check for comment-only changes (low significance)
218
+ const commentPatterns = [
219
+ /^\/\//, // JS comments
220
+ /^\/\*/, // Block comment start
221
+ /^\*/, // Block comment middle
222
+ /^#(?!#)/, // Shell/Python/Ruby comments (not markdown headers)
223
+ /^<!--/, // HTML comments
224
+ ];
225
+
226
+ const allChangesAreComments = [...changes.added, ...changes.removed].every((line) =>
227
+ commentPatterns.some((p) => p.test(line.trim()))
228
+ );
229
+
230
+ if (allChangesAreComments && changes.added.length + changes.removed.length > 0) {
231
+ significance.level = 'low';
232
+ significance.reasons = ['Only comment changes'];
233
+ }
234
+
235
+ // Volume-based significance boost
236
+ const totalChanges = changes.added.length + changes.removed.length;
237
+ const totalLines = Math.max(localContent.split('\n').length, templateContent.split('\n').length);
238
+ const changeRatio = totalChanges / totalLines;
239
+
240
+ if (changeRatio > 0.5 && significance.level !== 'high') {
241
+ significance.level = 'high';
242
+ significance.reasons.push(`Large change volume (${Math.round(changeRatio * 100)}% of file)`);
243
+ } else if (changeRatio > 0.2 && significance.level === 'low') {
244
+ significance.level = 'medium';
245
+ significance.reasons.push(`Moderate change volume (${Math.round(changeRatio * 100)}% of file)`);
246
+ }
247
+
248
+ return significance;
249
+ }
250
+
251
+ /**
252
+ * Generate a human-readable summary of changes
253
+ */
254
+ function generateChangeSummary(changes, significance) {
255
+ const parts = [];
256
+
257
+ if (changes.added.length > 0) {
258
+ parts.push(`${changes.added.length} line(s) would be added`);
259
+ }
260
+
261
+ if (changes.removed.length > 0) {
262
+ parts.push(`${changes.removed.length} line(s) would be removed/replaced`);
263
+ }
264
+
265
+ if (significance.reasons.length > 0) {
266
+ parts.push(significance.reasons[0]);
267
+ }
268
+
269
+ return parts.join('; ') || 'Minor changes';
270
+ }
271
+
272
+ /**
273
+ * Generate a detailed diff for display
274
+ * Uses unified diff format if git is available, otherwise simple comparison
275
+ */
276
+ export function generateDetailedDiff(localPath, templatePath) {
277
+ try {
278
+ // Try using git diff for nice formatting
279
+ const diff = execSync(
280
+ `git diff --no-index --color=never "${templatePath}" "${localPath}"`,
281
+ {
282
+ encoding: 'utf8',
283
+ timeout: 5000,
284
+ stdio: ['pipe', 'pipe', 'pipe'],
285
+ }
286
+ );
287
+ return diff;
288
+ } catch (error) {
289
+ // git diff returns exit code 1 when files differ (which is expected)
290
+ if (error.stdout) {
291
+ return error.stdout;
292
+ }
293
+
294
+ // Fall back to simple comparison
295
+ const local = readFileSync(localPath, 'utf8');
296
+ const template = readFileSync(templatePath, 'utf8');
297
+
298
+ return `--- Template (update)\n+++ Local (current)\n\n` +
299
+ `Template version:\n${template}\n\n` +
300
+ `Local version:\n${local}`;
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Get all assets that need merge consideration during update
306
+ * Returns assets that are:
307
+ * 1. Used by the user (tracked in usage-tracking.json)
308
+ * 2. Customized (differ from template)
309
+ * 3. Have updates available (template has changed)
310
+ */
311
+ export function getAssetsNeedingMerge(projectDir = process.cwd()) {
312
+ const customizedAssets = getCustomizedUsedAssets(projectDir);
313
+ const assetsNeedingMerge = {
314
+ commands: [],
315
+ skills: [],
316
+ agents: [],
317
+ hooks: [],
318
+ };
319
+
320
+ for (const [assetType, assets] of Object.entries(customizedAssets)) {
321
+ for (const [assetName, usageData] of Object.entries(assets)) {
322
+ const local = getLocalAsset(assetType, assetName, projectDir);
323
+ const template = getTemplateAsset(assetType, assetName);
324
+
325
+ // Skip if no template exists (user-created asset)
326
+ if (!template) continue;
327
+
328
+ // Skip if no local exists (shouldn't happen, but safety check)
329
+ if (!local) continue;
330
+
331
+ const comparison = compareAssetVersions(local.content, template.content);
332
+
333
+ // Only include if there are actual differences
334
+ if (comparison.hasChanges && !comparison.identical) {
335
+ assetsNeedingMerge[assetType].push({
336
+ name: assetName,
337
+ usageData,
338
+ comparison,
339
+ localPath: local.path,
340
+ templatePath: template.path,
341
+ });
342
+ }
343
+ }
344
+ }
345
+
346
+ return assetsNeedingMerge;
347
+ }
348
+
349
+ /**
350
+ * Generate a merge exploration prompt for Claude
351
+ * This creates a structured prompt that helps Claude explain the merge options
352
+ */
353
+ export function generateMergeExplanation(assetType, assetName, comparison, localContent, templateContent) {
354
+ const assetLabel = assetType.slice(0, -1); // Remove 's' (commands -> command)
355
+
356
+ let explanation = `## Merge Analysis: ${assetName} (${assetLabel})\n\n`;
357
+
358
+ // Significance indicator
359
+ const significanceEmoji = {
360
+ low: '🟢',
361
+ medium: '🟡',
362
+ high: '🔴',
363
+ };
364
+
365
+ explanation += `**Change Significance:** ${significanceEmoji[comparison.significance.level]} ${comparison.significance.level.toUpperCase()}\n\n`;
366
+
367
+ // Summary
368
+ explanation += `### Summary\n`;
369
+ explanation += `${comparison.summary}\n\n`;
370
+
371
+ // Stats
372
+ explanation += `### Change Statistics\n`;
373
+ explanation += `- Your version: ${comparison.stats.localLines} lines\n`;
374
+ explanation += `- Update version: ${comparison.stats.templateLines} lines\n`;
375
+ explanation += `- Lines that would be added: ${comparison.stats.addedLines}\n`;
376
+ explanation += `- Lines that would be removed/changed: ${comparison.stats.removedLines}\n\n`;
377
+
378
+ // Key changes preview
379
+ if (comparison.changes.added.length > 0) {
380
+ explanation += `### New in Update (would be added)\n`;
381
+ explanation += '```\n';
382
+ explanation += comparison.changes.added.slice(0, 10).join('\n');
383
+ if (comparison.changes.added.length > 10) {
384
+ explanation += `\n... and ${comparison.changes.added.length - 10} more lines`;
385
+ }
386
+ explanation += '\n```\n\n';
387
+ }
388
+
389
+ if (comparison.changes.removed.length > 0) {
390
+ explanation += `### Your Customizations (would be replaced)\n`;
391
+ explanation += '```\n';
392
+ explanation += comparison.changes.removed.slice(0, 10).join('\n');
393
+ if (comparison.changes.removed.length > 10) {
394
+ explanation += `\n... and ${comparison.changes.removed.length - 10} more lines`;
395
+ }
396
+ explanation += '\n```\n\n';
397
+ }
398
+
399
+ // Recommendations
400
+ explanation += `### Recommendation\n`;
401
+
402
+ if (comparison.significance.level === 'low') {
403
+ explanation += `This update has minor changes. You can likely **replace** safely, `;
404
+ explanation += `but review the diff if your customizations are important.\n`;
405
+ } else if (comparison.significance.level === 'medium') {
406
+ explanation += `This update has moderate changes. Consider **exploring the merge** to `;
407
+ explanation += `understand what would change and preserve your customizations.\n`;
408
+ } else {
409
+ explanation += `This update has significant structural changes. **Strongly recommend** `;
410
+ explanation += `exploring the merge carefully before deciding. Your customizations may `;
411
+ explanation += `be incompatible with the new version.\n`;
412
+ }
413
+
414
+ return explanation;
415
+ }
416
+
417
+ /**
418
+ * Generate options display for merge decision
419
+ */
420
+ export function formatMergeOptions(assetName, useCount) {
421
+ return `
422
+ ┌─────────────────────────────────────────────────────────────┐
423
+ │ ${assetName.padEnd(55)} │
424
+ │ Used ${useCount} time(s) • Customized │
425
+ ├─────────────────────────────────────────────────────────────┤
426
+ │ [E] Explore merge - Claude analyzes both versions │
427
+ │ [R] Replace - Use new version (lose customizations) │
428
+ │ [S] Skip - Keep your version (miss update) │
429
+ │ [D] Show diff - View raw differences │
430
+ └─────────────────────────────────────────────────────────────┘
431
+ `;
432
+ }
433
+
434
+ /**
435
+ * Get count of assets needing merge attention
436
+ */
437
+ export function getMergeAttentionCount(projectDir = process.cwd()) {
438
+ const assets = getAssetsNeedingMerge(projectDir);
439
+ let count = 0;
440
+
441
+ for (const assetList of Object.values(assets)) {
442
+ count += assetList.length;
443
+ }
444
+
445
+ return count;
446
+ }
447
+
448
+ export default {
449
+ getLocalAsset,
450
+ getTemplateAsset,
451
+ compareAssetVersions,
452
+ generateDetailedDiff,
453
+ getAssetsNeedingMerge,
454
+ generateMergeExplanation,
455
+ formatMergeOptions,
456
+ getMergeAttentionCount,
457
+ };
@@ -87,6 +87,210 @@ export function saveUpdateState(state, projectDir = process.cwd()) {
87
87
  writeFileSync(statePath, JSON.stringify(state, null, 2), 'utf8');
88
88
  }
89
89
 
90
+ // ============================================================================
91
+ // USAGE TRACKING SYSTEM
92
+ // Tracks which commands, skills, agents, and hooks the user has used
93
+ // ============================================================================
94
+
95
+ /**
96
+ * Get the usage tracking file path
97
+ */
98
+ function getUsageTrackingPath(projectDir = process.cwd()) {
99
+ return join(projectDir, '.claude', 'config', 'usage-tracking.json');
100
+ }
101
+
102
+ /**
103
+ * Default usage tracking structure
104
+ */
105
+ function getDefaultUsageTracking() {
106
+ return {
107
+ version: '1.0.0',
108
+ assets: {
109
+ commands: {},
110
+ skills: {},
111
+ agents: {},
112
+ hooks: {},
113
+ },
114
+ _lastModified: new Date().toISOString(),
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Load the usage tracking data
120
+ */
121
+ export function loadUsageTracking(projectDir = process.cwd()) {
122
+ const trackingPath = getUsageTrackingPath(projectDir);
123
+
124
+ if (!existsSync(trackingPath)) {
125
+ return getDefaultUsageTracking();
126
+ }
127
+
128
+ try {
129
+ const data = JSON.parse(readFileSync(trackingPath, 'utf8'));
130
+ // Ensure all required keys exist
131
+ return {
132
+ ...getDefaultUsageTracking(),
133
+ ...data,
134
+ assets: {
135
+ commands: {},
136
+ skills: {},
137
+ agents: {},
138
+ hooks: {},
139
+ ...data.assets,
140
+ },
141
+ };
142
+ } catch {
143
+ return getDefaultUsageTracking();
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Save the usage tracking data
149
+ */
150
+ export function saveUsageTracking(tracking, projectDir = process.cwd()) {
151
+ const trackingPath = getUsageTrackingPath(projectDir);
152
+ const configDir = dirname(trackingPath);
153
+
154
+ if (!existsSync(configDir)) {
155
+ mkdirSync(configDir, { recursive: true });
156
+ }
157
+
158
+ tracking._lastModified = new Date().toISOString();
159
+ writeFileSync(trackingPath, JSON.stringify(tracking, null, 2), 'utf8');
160
+ }
161
+
162
+ /**
163
+ * Track usage of an asset (command, skill, agent, or hook)
164
+ * @param {string} assetType - 'commands' | 'skills' | 'agents' | 'hooks'
165
+ * @param {string} assetName - Name of the asset (e.g., 'create-task-list')
166
+ * @param {object} options - Additional options
167
+ * @param {boolean} options.customized - Whether the asset has been customized by user
168
+ * @param {string} projectDir - Project directory
169
+ */
170
+ export function trackAssetUsage(assetType, assetName, options = {}, projectDir = process.cwd()) {
171
+ const tracking = loadUsageTracking(projectDir);
172
+
173
+ if (!tracking.assets[assetType]) {
174
+ tracking.assets[assetType] = {};
175
+ }
176
+
177
+ const existing = tracking.assets[assetType][assetName] || {
178
+ firstUsed: new Date().toISOString(),
179
+ useCount: 0,
180
+ customized: false,
181
+ };
182
+
183
+ tracking.assets[assetType][assetName] = {
184
+ ...existing,
185
+ lastUsed: new Date().toISOString(),
186
+ useCount: existing.useCount + 1,
187
+ customized: options.customized !== undefined ? options.customized : existing.customized,
188
+ };
189
+
190
+ saveUsageTracking(tracking, projectDir);
191
+
192
+ return tracking.assets[assetType][assetName];
193
+ }
194
+
195
+ /**
196
+ * Mark an asset as customized (user has modified it from the template)
197
+ */
198
+ export function markAssetCustomized(assetType, assetName, projectDir = process.cwd()) {
199
+ const tracking = loadUsageTracking(projectDir);
200
+
201
+ if (!tracking.assets[assetType]) {
202
+ tracking.assets[assetType] = {};
203
+ }
204
+
205
+ if (!tracking.assets[assetType][assetName]) {
206
+ tracking.assets[assetType][assetName] = {
207
+ firstUsed: new Date().toISOString(),
208
+ lastUsed: new Date().toISOString(),
209
+ useCount: 0,
210
+ customized: true,
211
+ };
212
+ } else {
213
+ tracking.assets[assetType][assetName].customized = true;
214
+ }
215
+
216
+ saveUsageTracking(tracking, projectDir);
217
+ }
218
+
219
+ /**
220
+ * Get all assets that have been used
221
+ * @param {object} options - Filter options
222
+ * @param {boolean} options.customizedOnly - Only return customized assets
223
+ * @param {string[]} options.assetTypes - Filter by asset types
224
+ */
225
+ export function getUsedAssets(options = {}, projectDir = process.cwd()) {
226
+ const tracking = loadUsageTracking(projectDir);
227
+ const { customizedOnly = false, assetTypes = ['commands', 'skills', 'agents', 'hooks'] } = options;
228
+
229
+ const result = {};
230
+
231
+ for (const type of assetTypes) {
232
+ if (!tracking.assets[type]) continue;
233
+
234
+ result[type] = {};
235
+
236
+ for (const [name, data] of Object.entries(tracking.assets[type])) {
237
+ if (customizedOnly && !data.customized) continue;
238
+ result[type][name] = data;
239
+ }
240
+ }
241
+
242
+ return result;
243
+ }
244
+
245
+ /**
246
+ * Get assets that are both used AND customized (for smart merge detection)
247
+ */
248
+ export function getCustomizedUsedAssets(projectDir = process.cwd()) {
249
+ return getUsedAssets({ customizedOnly: true }, projectDir);
250
+ }
251
+
252
+ /**
253
+ * Check if an asset has been customized
254
+ */
255
+ export function isAssetCustomized(assetType, assetName, projectDir = process.cwd()) {
256
+ const tracking = loadUsageTracking(projectDir);
257
+ return tracking.assets[assetType]?.[assetName]?.customized === true;
258
+ }
259
+
260
+ /**
261
+ * Get usage statistics summary
262
+ */
263
+ export function getUsageStats(projectDir = process.cwd()) {
264
+ const tracking = loadUsageTracking(projectDir);
265
+
266
+ const stats = {
267
+ totalAssets: 0,
268
+ totalUsage: 0,
269
+ customizedCount: 0,
270
+ byType: {},
271
+ };
272
+
273
+ for (const [type, assets] of Object.entries(tracking.assets)) {
274
+ const typeStats = {
275
+ count: Object.keys(assets).length,
276
+ totalUsage: 0,
277
+ customized: 0,
278
+ };
279
+
280
+ for (const data of Object.values(assets)) {
281
+ typeStats.totalUsage += data.useCount || 0;
282
+ if (data.customized) typeStats.customized++;
283
+ }
284
+
285
+ stats.byType[type] = typeStats;
286
+ stats.totalAssets += typeStats.count;
287
+ stats.totalUsage += typeStats.totalUsage;
288
+ stats.customizedCount += typeStats.customized;
289
+ }
290
+
291
+ return stats;
292
+ }
293
+
90
294
  /**
91
295
  * Compare two semantic versions
92
296
  * Returns: 1 if v1 > v2, -1 if v1 < v2, 0 if equal
@@ -509,4 +713,13 @@ export default {
509
713
  getAvailableFeatures,
510
714
  formatUpdateBanner,
511
715
  formatUpdateMarkdown,
716
+ // Usage tracking exports
717
+ loadUsageTracking,
718
+ saveUsageTracking,
719
+ trackAssetUsage,
720
+ markAssetCustomized,
721
+ getUsedAssets,
722
+ getCustomizedUsedAssets,
723
+ isAssetCustomized,
724
+ getUsageStats,
512
725
  };