myaidev-method 0.2.12 → 0.2.15

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.
Files changed (28) hide show
  1. package/.claude/CLAUDE.md +46 -0
  2. package/.claude/agents/content-production-coordinator.md +111 -0
  3. package/.claude/agents/proprietary-content-verifier.md +96 -0
  4. package/.claude/agents/visual-content-generator.md +520 -0
  5. package/.claude/commands/myai-coordinate-content.md +136 -0
  6. package/.claude/settings.local.json +3 -2
  7. package/.env.example +33 -0
  8. package/CHANGELOG.md +64 -0
  9. package/CONTENT_CREATION_GUIDE.md +3399 -0
  10. package/DEVELOPER_USE_CASES.md +2085 -0
  11. package/README.md +209 -2
  12. package/VISUAL_GENERATION_FILE_ORGANIZATION.md +105 -0
  13. package/bin/cli.js +46 -0
  14. package/package.json +17 -2
  15. package/src/lib/asset-management.js +532 -0
  16. package/src/lib/visual-config-utils.js +424 -0
  17. package/src/lib/visual-generation-utils.js +668 -0
  18. package/src/scripts/configure-visual-apis.js +413 -0
  19. package/src/scripts/generate-visual-cli.js +279 -0
  20. package/src/templates/claude/agents/content-production-coordinator.md +111 -0
  21. package/src/templates/claude/agents/content-writer.md +209 -4
  22. package/src/templates/claude/agents/proprietary-content-verifier.md +96 -0
  23. package/src/templates/claude/agents/visual-content-generator.md +520 -0
  24. package/src/templates/claude/commands/myai-content-writer.md +33 -8
  25. package/src/templates/claude/commands/myai-coordinate-content.md +136 -0
  26. package/src/templates/claude/commands/myai-generate-visual.md +318 -0
  27. package/src/templates/codex/commands/myai-generate-visual.md +307 -0
  28. package/src/templates/gemini/commands/myai-generate-visual.md +200 -0
@@ -0,0 +1,532 @@
1
+ /**
2
+ * Asset Management Utilities
3
+ *
4
+ * Handles file storage, organization, and tracking for generated visual content.
5
+ * Supports images and videos with automatic directory organization by date.
6
+ *
7
+ * Platform support: Claude Code, Gemini CLI, Codex CLI
8
+ *
9
+ * @module asset-management
10
+ */
11
+
12
+ import fs from 'fs-extra';
13
+ import path from 'path';
14
+ import dotenv from 'dotenv';
15
+
16
+ dotenv.config();
17
+
18
+ // Default asset storage path
19
+ const DEFAULT_ASSETS_PATH = './content-assets';
20
+
21
+ // Usage tracking file
22
+ const USAGE_STATS_FILE = '.visual-usage-stats.json';
23
+
24
+ /**
25
+ * Get configured assets path from environment or use default
26
+ *
27
+ * @returns {string} Assets directory path
28
+ */
29
+ export function getAssetsPath() {
30
+ return process.env.VISUAL_ASSETS_PATH || DEFAULT_ASSETS_PATH;
31
+ }
32
+
33
+ /**
34
+ * Create asset directory structure for a specific date
35
+ * Creates: content-assets/images/YYYY-MM-DD/ and content-assets/videos/YYYY-MM-DD/
36
+ *
37
+ * @param {Date} date - Date for directory organization (defaults to today)
38
+ * @returns {Promise<Object>} Created directory paths
39
+ */
40
+ export async function createAssetDirectory(date = new Date()) {
41
+ const assetsPath = getAssetsPath();
42
+ const dateStr = formatDate(date);
43
+
44
+ const imagePath = path.join(assetsPath, 'images', dateStr);
45
+ const videoPath = path.join(assetsPath, 'videos', dateStr);
46
+
47
+ await fs.ensureDir(imagePath);
48
+ await fs.ensureDir(videoPath);
49
+
50
+ return {
51
+ images: imagePath,
52
+ videos: videoPath,
53
+ date: dateStr
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Generate filename for asset based on type and description
59
+ *
60
+ * @param {string} type - Asset type (hero, illustration, diagram, screenshot, video)
61
+ * @param {string} slug - Descriptive slug (will be sanitized)
62
+ * @param {string} extension - File extension (png, jpg, mp4, etc.)
63
+ * @returns {string} Generated filename
64
+ */
65
+ export function generateFilename(type, slug, extension = 'png') {
66
+ // Sanitize slug: lowercase, replace spaces/special chars with hyphens
67
+ const sanitized = slug
68
+ .toLowerCase()
69
+ .replace(/[^a-z0-9]+/g, '-')
70
+ .replace(/^-+|-+$/g, '')
71
+ .substring(0, 50); // Max 50 chars
72
+
73
+ const timestamp = Date.now().toString().slice(-6); // Last 6 digits for uniqueness
74
+
75
+ return `${type}-${sanitized}-${timestamp}.${extension}`;
76
+ }
77
+
78
+ /**
79
+ * Save image to content-assets directory
80
+ *
81
+ * @param {Buffer} buffer - Image data buffer
82
+ * @param {Object} metadata - Image metadata
83
+ * @param {string} metadata.type - Image type (hero, illustration, diagram, screenshot)
84
+ * @param {string} metadata.description - Image description for filename
85
+ * @param {string} metadata.service - Service used (gemini, imagen, dalle)
86
+ * @param {number} metadata.cost - Generation cost
87
+ * @param {string} metadata.prompt - Original prompt
88
+ * @param {Date} metadata.date - Creation date (defaults to now)
89
+ * @returns {Promise<Object>} Saved file information
90
+ */
91
+ export async function saveImage(buffer, metadata) {
92
+ const {
93
+ type = 'general',
94
+ description,
95
+ service,
96
+ cost = 0,
97
+ prompt,
98
+ date = new Date()
99
+ } = metadata;
100
+
101
+ // Create directory structure
102
+ const dirs = await createAssetDirectory(date);
103
+
104
+ // Generate filename
105
+ const filename = generateFilename(type, description, 'png');
106
+ const filepath = path.join(dirs.images, filename);
107
+
108
+ // Save image
109
+ await fs.writeFile(filepath, buffer);
110
+
111
+ // Track usage
112
+ await trackCost(service, cost, {
113
+ type: 'image',
114
+ filename,
115
+ prompt,
116
+ date
117
+ });
118
+
119
+ const relativePath = path.relative(process.cwd(), filepath);
120
+
121
+ return {
122
+ filepath,
123
+ relativePath,
124
+ filename,
125
+ type,
126
+ service,
127
+ cost,
128
+ size: buffer.length,
129
+ date: date.toISOString()
130
+ };
131
+ }
132
+
133
+ /**
134
+ * Save video to content-assets directory
135
+ *
136
+ * @param {Buffer} buffer - Video data buffer
137
+ * @param {Object} metadata - Video metadata
138
+ * @param {string} metadata.description - Video description for filename
139
+ * @param {string} metadata.service - Service used (veo)
140
+ * @param {number} metadata.cost - Generation cost
141
+ * @param {string} metadata.prompt - Original prompt
142
+ * @param {number} metadata.duration - Video duration in seconds
143
+ * @param {Date} metadata.date - Creation date (defaults to now)
144
+ * @returns {Promise<Object>} Saved file information
145
+ */
146
+ export async function saveVideo(buffer, metadata) {
147
+ const {
148
+ description,
149
+ service,
150
+ cost = 0,
151
+ prompt,
152
+ duration = 0,
153
+ date = new Date()
154
+ } = metadata;
155
+
156
+ // Create directory structure
157
+ const dirs = await createAssetDirectory(date);
158
+
159
+ // Generate filename
160
+ const filename = generateFilename('video', description, 'mp4');
161
+ const filepath = path.join(dirs.videos, filename);
162
+
163
+ // Save video
164
+ await fs.writeFile(filepath, buffer);
165
+
166
+ // Track usage
167
+ await trackCost(service, cost, {
168
+ type: 'video',
169
+ filename,
170
+ prompt,
171
+ duration,
172
+ date
173
+ });
174
+
175
+ const relativePath = path.relative(process.cwd(), filepath);
176
+
177
+ return {
178
+ filepath,
179
+ relativePath,
180
+ filename,
181
+ service,
182
+ cost,
183
+ duration,
184
+ size: buffer.length,
185
+ date: date.toISOString()
186
+ };
187
+ }
188
+
189
+ /**
190
+ * Generate markdown reference for image
191
+ *
192
+ * @param {string} filepath - Relative or absolute file path
193
+ * @param {string} altText - Alt text for accessibility
194
+ * @param {string} caption - Optional caption
195
+ * @returns {string} Markdown image reference
196
+ */
197
+ export function generateMarkdownReference(filepath, altText, caption = null) {
198
+ // Convert to relative path if absolute
199
+ const relativePath = filepath.startsWith('./') || filepath.startsWith('../')
200
+ ? filepath
201
+ : `./${filepath}`;
202
+
203
+ let markdown = `![${altText}](${relativePath})`;
204
+
205
+ if (caption) {
206
+ markdown += `\n*${caption}*`;
207
+ }
208
+
209
+ return markdown;
210
+ }
211
+
212
+ /**
213
+ * Track cost and usage statistics
214
+ *
215
+ * @param {string} service - Service name
216
+ * @param {number} cost - Cost in USD
217
+ * @param {Object} metadata - Additional metadata to track
218
+ * @returns {Promise<void>}
219
+ */
220
+ export async function trackCost(service, cost, metadata = {}) {
221
+ const statsPath = path.join(process.cwd(), USAGE_STATS_FILE);
222
+
223
+ // Load existing stats
224
+ let stats = {
225
+ totalCost: 0,
226
+ daily: {},
227
+ monthly: {},
228
+ byService: {},
229
+ history: []
230
+ };
231
+
232
+ if (await fs.pathExists(statsPath)) {
233
+ stats = await fs.readJSON(statsPath);
234
+ }
235
+
236
+ // Get date keys
237
+ const now = new Date();
238
+ const dateKey = formatDate(now);
239
+ const monthKey = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
240
+
241
+ // Update totals
242
+ stats.totalCost += cost;
243
+
244
+ // Update daily
245
+ stats.daily[dateKey] = (stats.daily[dateKey] || 0) + cost;
246
+
247
+ // Update monthly
248
+ stats.monthly[monthKey] = (stats.monthly[monthKey] || 0) + cost;
249
+
250
+ // Update by service
251
+ if (!stats.byService[service]) {
252
+ stats.byService[service] = { count: 0, cost: 0 };
253
+ }
254
+ stats.byService[service].count += 1;
255
+ stats.byService[service].cost += cost;
256
+
257
+ // Add to history
258
+ stats.history.push({
259
+ service,
260
+ cost,
261
+ date: now.toISOString(),
262
+ ...metadata
263
+ });
264
+
265
+ // Keep only last 1000 history entries
266
+ if (stats.history.length > 1000) {
267
+ stats.history = stats.history.slice(-1000);
268
+ }
269
+
270
+ // Save stats
271
+ await fs.writeJSON(statsPath, stats, { spaces: 2 });
272
+ }
273
+
274
+ /**
275
+ * Get usage statistics
276
+ *
277
+ * @returns {Promise<Object>} Usage statistics
278
+ */
279
+ export async function getUsageStats() {
280
+ const statsPath = path.join(process.cwd(), USAGE_STATS_FILE);
281
+
282
+ if (!await fs.pathExists(statsPath)) {
283
+ return {
284
+ totalCost: 0,
285
+ daily: {},
286
+ monthly: {},
287
+ byService: {},
288
+ history: []
289
+ };
290
+ }
291
+
292
+ return await fs.readJSON(statsPath);
293
+ }
294
+
295
+ /**
296
+ * Get today's usage cost
297
+ *
298
+ * @returns {Promise<number>} Today's cost in USD
299
+ */
300
+ export async function getTodaysCost() {
301
+ const stats = await getUsageStats();
302
+ const today = formatDate(new Date());
303
+ return stats.daily[today] || 0;
304
+ }
305
+
306
+ /**
307
+ * Get this month's usage cost
308
+ *
309
+ * @returns {Promise<number>} This month's cost in USD
310
+ */
311
+ export async function getMonthCost() {
312
+ const stats = await getUsageStats();
313
+ const now = new Date();
314
+ const monthKey = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
315
+ return stats.monthly[monthKey] || 0;
316
+ }
317
+
318
+ /**
319
+ * Check if cost exceeds budget limits
320
+ *
321
+ * @param {number} estimatedCost - Estimated cost for next operation
322
+ * @returns {Promise<Object>} Budget check result
323
+ */
324
+ export async function checkBudgetLimit(estimatedCost) {
325
+ const dailyBudget = parseFloat(process.env.VISUAL_DAILY_BUDGET || 5.0);
326
+ const monthlyBudget = parseFloat(process.env.VISUAL_MONTHLY_BUDGET || 50.0);
327
+ const warnThreshold = parseFloat(process.env.VISUAL_WARN_THRESHOLD || 0.8);
328
+
329
+ const todaysCost = await getTodaysCost();
330
+ const monthCost = await getMonthCost();
331
+
332
+ const newDailyCost = todaysCost + estimatedCost;
333
+ const newMonthlyCost = monthCost + estimatedCost;
334
+
335
+ const result = {
336
+ dailyBudget,
337
+ monthlyBudget,
338
+ todaysCost,
339
+ monthCost,
340
+ newDailyCost,
341
+ newMonthlyCost,
342
+ estimatedCost,
343
+ exceedsDailyBudget: newDailyCost > dailyBudget,
344
+ exceedsMonthlyBudget: newMonthlyCost > monthlyBudget,
345
+ dailyWarning: newDailyCost > (dailyBudget * warnThreshold),
346
+ monthlyWarning: newMonthlyCost > (monthlyBudget * warnThreshold),
347
+ canProceed: newDailyCost <= dailyBudget && newMonthlyCost <= monthlyBudget
348
+ };
349
+
350
+ return result;
351
+ }
352
+
353
+ /**
354
+ * Generate usage report
355
+ *
356
+ * @param {string} period - Report period (today, week, month, all)
357
+ * @returns {Promise<string>} Formatted usage report
358
+ */
359
+ export async function generateUsageReport(period = 'today') {
360
+ const stats = await getUsageStats();
361
+ const now = new Date();
362
+
363
+ let report = 'šŸ“Š Visual Content Generation Usage Report\n\n';
364
+
365
+ // Daily budget info
366
+ const dailyBudget = parseFloat(process.env.VISUAL_DAILY_BUDGET || 5.0);
367
+ const monthlyBudget = parseFloat(process.env.VISUAL_MONTHLY_BUDGET || 50.0);
368
+ const todaysCost = await getTodaysCost();
369
+ const monthCost = await getMonthCost();
370
+
371
+ report += `šŸ’° Budget Status:\n`;
372
+ report += ` Today: $${todaysCost.toFixed(2)} / $${dailyBudget.toFixed(2)} (${((todaysCost/dailyBudget)*100).toFixed(0)}%)\n`;
373
+ report += ` Month: $${monthCost.toFixed(2)} / $${monthlyBudget.toFixed(2)} (${((monthCost/monthlyBudget)*100).toFixed(0)}%)\n\n`;
374
+
375
+ // Service breakdown
376
+ report += `šŸŽØ By Service:\n`;
377
+ for (const [service, data] of Object.entries(stats.byService)) {
378
+ report += ` ${service}: ${data.count} generations, $${data.cost.toFixed(2)}\n`;
379
+ }
380
+
381
+ report += `\nšŸ’µ Total Lifetime Cost: $${stats.totalCost.toFixed(2)}\n`;
382
+
383
+ return report;
384
+ }
385
+
386
+ /**
387
+ * Clean up old assets (optional maintenance)
388
+ *
389
+ * @param {number} daysOld - Delete assets older than this many days
390
+ * @returns {Promise<Object>} Cleanup results
391
+ */
392
+ export async function cleanupOldAssets(daysOld = 30) {
393
+ const assetsPath = getAssetsPath();
394
+ const cutoffDate = new Date();
395
+ cutoffDate.setDate(cutoffDate.getDate() - daysOld);
396
+
397
+ let deletedCount = 0;
398
+ let freedSpace = 0;
399
+
400
+ const imagePath = path.join(assetsPath, 'images');
401
+ const videoPath = path.join(assetsPath, 'videos');
402
+
403
+ // Clean images
404
+ if (await fs.pathExists(imagePath)) {
405
+ const dateDirs = await fs.readdir(imagePath);
406
+
407
+ for (const dateDir of dateDirs) {
408
+ const dirDate = new Date(dateDir);
409
+
410
+ if (dirDate < cutoffDate) {
411
+ const fullPath = path.join(imagePath, dateDir);
412
+ const files = await fs.readdir(fullPath);
413
+
414
+ for (const file of files) {
415
+ const filePath = path.join(fullPath, file);
416
+ const stats = await fs.stat(filePath);
417
+ freedSpace += stats.size;
418
+ deletedCount++;
419
+ }
420
+
421
+ await fs.remove(fullPath);
422
+ }
423
+ }
424
+ }
425
+
426
+ // Clean videos
427
+ if (await fs.pathExists(videoPath)) {
428
+ const dateDirs = await fs.readdir(videoPath);
429
+
430
+ for (const dateDir of dateDirs) {
431
+ const dirDate = new Date(dateDir);
432
+
433
+ if (dirDate < cutoffDate) {
434
+ const fullPath = path.join(videoPath, dateDir);
435
+ const files = await fs.readdir(fullPath);
436
+
437
+ for (const file of files) {
438
+ const filePath = path.join(fullPath, file);
439
+ const stats = await fs.stat(filePath);
440
+ freedSpace += stats.size;
441
+ deletedCount++;
442
+ }
443
+
444
+ await fs.remove(fullPath);
445
+ }
446
+ }
447
+ }
448
+
449
+ return {
450
+ deletedCount,
451
+ freedSpace,
452
+ freedSpaceMB: (freedSpace / 1024 / 1024).toFixed(2)
453
+ };
454
+ }
455
+
456
+ /**
457
+ * Format date as YYYY-MM-DD
458
+ *
459
+ * @param {Date} date - Date to format
460
+ * @returns {string} Formatted date string
461
+ */
462
+ function formatDate(date) {
463
+ const year = date.getFullYear();
464
+ const month = String(date.getMonth() + 1).padStart(2, '0');
465
+ const day = String(date.getDate()).padStart(2, '0');
466
+ return `${year}-${month}-${day}`;
467
+ }
468
+
469
+ /**
470
+ * List all generated assets
471
+ *
472
+ * @param {Object} options - Filter options
473
+ * @param {string} options.type - Filter by type (image, video)
474
+ * @param {string} options.date - Filter by date (YYYY-MM-DD)
475
+ * @param {number} options.limit - Limit results
476
+ * @returns {Promise<Array>} List of assets
477
+ */
478
+ export async function listAssets(options = {}) {
479
+ const { type, date, limit } = options;
480
+ const assetsPath = getAssetsPath();
481
+ const assets = [];
482
+
483
+ // Determine which directories to scan
484
+ const dirs = [];
485
+ if (!type || type === 'image') {
486
+ dirs.push({ type: 'image', path: path.join(assetsPath, 'images') });
487
+ }
488
+ if (!type || type === 'video') {
489
+ dirs.push({ type: 'video', path: path.join(assetsPath, 'videos') });
490
+ }
491
+
492
+ // Scan directories
493
+ for (const dir of dirs) {
494
+ if (!await fs.pathExists(dir.path)) continue;
495
+
496
+ const dateDirs = await fs.readdir(dir.path);
497
+
498
+ for (const dateDir of dateDirs) {
499
+ // Filter by date if specified
500
+ if (date && dateDir !== date) continue;
501
+
502
+ const fullPath = path.join(dir.path, dateDir);
503
+ const files = await fs.readdir(fullPath);
504
+
505
+ for (const file of files) {
506
+ const filePath = path.join(fullPath, file);
507
+ const stats = await fs.stat(filePath);
508
+ const relativePath = path.relative(process.cwd(), filePath);
509
+
510
+ assets.push({
511
+ type: dir.type,
512
+ filename: file,
513
+ path: filePath,
514
+ relativePath,
515
+ date: dateDir,
516
+ size: stats.size,
517
+ created: stats.birthtime
518
+ });
519
+
520
+ // Check limit
521
+ if (limit && assets.length >= limit) {
522
+ return assets;
523
+ }
524
+ }
525
+ }
526
+ }
527
+
528
+ // Sort by created date (newest first)
529
+ assets.sort((a, b) => b.created - a.created);
530
+
531
+ return assets;
532
+ }