myaidev-method 0.3.4 → 0.3.5
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/.claude-plugin/plugin.json +0 -1
- package/.env.example +5 -4
- package/CHANGELOG.md +2 -2
- package/CONTENT_CREATION_GUIDE.md +489 -3211
- package/DEVELOPER_USE_CASES.md +1 -1
- package/MODULAR_INSTALLATION.md +2 -2
- package/README.md +39 -33
- package/TECHNICAL_ARCHITECTURE.md +1 -1
- package/USER_GUIDE.md +242 -190
- package/agents/content-editor-agent.md +90 -0
- package/agents/content-planner-agent.md +97 -0
- package/agents/content-research-agent.md +62 -0
- package/agents/content-seo-agent.md +101 -0
- package/agents/content-writer-agent.md +69 -0
- package/agents/infographic-analyzer-agent.md +63 -0
- package/agents/infographic-designer-agent.md +72 -0
- package/bin/cli.js +776 -422
- package/{content-rules.example.md → content-rules-example.md} +2 -2
- package/dist/mcp/health-check.js +82 -68
- package/dist/mcp/mcp-config.json +8 -0
- package/dist/mcp/openstack-server.js +1746 -1262
- package/dist/server/.tsbuildinfo +1 -1
- package/extension.json +21 -4
- package/package.json +181 -184
- package/skills/company-config/SKILL.md +133 -0
- package/skills/configure/SKILL.md +1 -1
- package/skills/myai-configurator/SKILL.md +77 -0
- package/skills/myai-configurator/content-creation-configurator/SKILL.md +516 -0
- package/skills/myai-configurator/content-maintenance-configurator/SKILL.md +397 -0
- package/skills/myai-content-enrichment/SKILL.md +114 -0
- package/skills/myai-content-ideation/SKILL.md +288 -0
- package/skills/myai-content-ideation/evals/evals.json +182 -0
- package/skills/myai-content-production-coordinator/SKILL.md +946 -0
- package/skills/{content-rules-setup → myai-content-rules-setup}/SKILL.md +1 -1
- package/skills/{content-verifier → myai-content-verifier}/SKILL.md +1 -1
- package/skills/myai-content-writer/SKILL.md +333 -0
- package/skills/{infographic → myai-infographic}/SKILL.md +1 -1
- package/skills/myai-proprietary-content-verifier/SKILL.md +175 -0
- package/skills/myai-proprietary-content-verifier/evals/evals.json +36 -0
- package/skills/myai-skill-builder/SKILL.md +699 -0
- package/skills/myai-skill-builder/agents/analyzer-agent.md +137 -0
- package/skills/myai-skill-builder/agents/comparator-agent.md +77 -0
- package/skills/myai-skill-builder/agents/grader-agent.md +103 -0
- package/skills/myai-skill-builder/assets/eval_review.html +131 -0
- package/skills/myai-skill-builder/references/schemas.md +211 -0
- package/skills/myai-skill-builder/scripts/aggregate_benchmark.py +190 -0
- package/skills/myai-skill-builder/scripts/generate_review.py +381 -0
- package/skills/myai-skill-builder/scripts/package_skill.py +91 -0
- package/skills/myai-skill-builder/scripts/run_eval.py +105 -0
- package/skills/myai-skill-builder/scripts/run_loop.py +211 -0
- package/skills/myai-skill-builder/scripts/utils.py +123 -0
- package/skills/myai-visual-generator/SKILL.md +125 -0
- package/skills/myai-visual-generator/evals/evals.json +155 -0
- package/skills/myai-visual-generator/references/infographic-pipeline.md +73 -0
- package/skills/myai-visual-generator/references/research-visuals.md +57 -0
- package/skills/myai-visual-generator/references/services.md +89 -0
- package/skills/myai-visual-generator/scripts/visual-generation-utils.js +1272 -0
- package/skills/myaidev-figma/SKILL.md +212 -0
- package/skills/myaidev-figma/capture.js +133 -0
- package/skills/myaidev-figma/crawl.js +130 -0
- package/skills/myaidev-figma-configure/SKILL.md +130 -0
- package/skills/openstack-manager/SKILL.md +1 -1
- package/skills/payloadcms-publisher/SKILL.md +141 -77
- package/skills/payloadcms-publisher/references/field-mapping.md +142 -0
- package/skills/payloadcms-publisher/references/lexical-format.md +97 -0
- package/skills/security-auditor/SKILL.md +1 -1
- package/src/cli/commands/addon.js +105 -7
- package/src/config/workflows.js +172 -228
- package/src/lib/ascii-banner.js +197 -182
- package/src/lib/{content-coordinator.js → content-production-coordinator.js} +649 -459
- package/src/lib/installation-detector.js +93 -59
- package/src/lib/payloadcms-utils.js +285 -510
- package/src/lib/workflow-installer.js +55 -0
- package/src/mcp/health-check.js +82 -68
- package/src/mcp/openstack-server.js +1746 -1262
- package/src/scripts/configure-visual-apis.js +224 -173
- package/src/scripts/configure-wordpress-mcp.js +96 -66
- package/src/scripts/init/install.js +109 -85
- package/src/scripts/init-project.js +138 -67
- package/src/scripts/utils/write-content.js +67 -52
- package/src/scripts/wordpress/publish-to-wordpress.js +128 -128
- package/src/templates/claude/CLAUDE.md +19 -12
- package/hooks/hooks.json +0 -26
- package/skills/content-coordinator/SKILL.md +0 -130
- package/skills/content-enrichment/SKILL.md +0 -80
- package/skills/content-writer/SKILL.md +0 -285
- package/skills/skill-builder/SKILL.md +0 -417
- package/skills/visual-generator/SKILL.md +0 -140
- /package/skills/{content-writer → myai-content-writer}/agents/editor-agent.md +0 -0
- /package/skills/{content-writer → myai-content-writer}/agents/planner-agent.md +0 -0
- /package/skills/{content-writer → myai-content-writer}/agents/research-agent.md +0 -0
- /package/skills/{content-writer → myai-content-writer}/agents/seo-agent.md +0 -0
- /package/skills/{content-writer → myai-content-writer}/agents/visual-planner-agent.md +0 -0
- /package/skills/{content-writer → myai-content-writer}/agents/writer-agent.md +0 -0
|
@@ -5,64 +5,64 @@
|
|
|
5
5
|
* Provides checkpoint/resume capability, error isolation, progress tracking,
|
|
6
6
|
* multi-platform publishing, analytics, webhooks, and queue management.
|
|
7
7
|
*
|
|
8
|
-
* @module content-coordinator
|
|
8
|
+
* @module content-production-coordinator
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import fs from
|
|
12
|
-
import path from
|
|
13
|
-
import https from
|
|
14
|
-
import http from
|
|
15
|
-
import os from
|
|
11
|
+
import fs from "fs/promises";
|
|
12
|
+
import path from "path";
|
|
13
|
+
import https from "https";
|
|
14
|
+
import http from "http";
|
|
15
|
+
import os from "os";
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Workflow phases for the content production coordinator
|
|
19
19
|
*/
|
|
20
20
|
const PHASES = {
|
|
21
|
-
INITIALIZE:
|
|
22
|
-
VERIFY:
|
|
23
|
-
CATEGORIZE:
|
|
24
|
-
REPORT:
|
|
25
|
-
NOTIFY:
|
|
26
|
-
PUBLISH:
|
|
27
|
-
COMPLETE:
|
|
21
|
+
INITIALIZE: "initialize",
|
|
22
|
+
VERIFY: "verify",
|
|
23
|
+
CATEGORIZE: "categorize",
|
|
24
|
+
REPORT: "report",
|
|
25
|
+
NOTIFY: "notify",
|
|
26
|
+
PUBLISH: "publish",
|
|
27
|
+
COMPLETE: "complete",
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Content item status values
|
|
32
32
|
*/
|
|
33
33
|
const ITEM_STATUS = {
|
|
34
|
-
PENDING:
|
|
35
|
-
VERIFYING:
|
|
36
|
-
VERIFIED:
|
|
37
|
-
READY:
|
|
38
|
-
NEEDS_REVIEW:
|
|
39
|
-
PUBLISHING:
|
|
40
|
-
PUBLISHED:
|
|
41
|
-
SCHEDULED:
|
|
42
|
-
FAILED:
|
|
43
|
-
SKIPPED:
|
|
34
|
+
PENDING: "pending",
|
|
35
|
+
VERIFYING: "verifying",
|
|
36
|
+
VERIFIED: "verified",
|
|
37
|
+
READY: "ready",
|
|
38
|
+
NEEDS_REVIEW: "needs_review",
|
|
39
|
+
PUBLISHING: "publishing",
|
|
40
|
+
PUBLISHED: "published",
|
|
41
|
+
SCHEDULED: "scheduled",
|
|
42
|
+
FAILED: "failed",
|
|
43
|
+
SKIPPED: "skipped",
|
|
44
44
|
};
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* Redundancy score thresholds
|
|
48
48
|
*/
|
|
49
49
|
const REDUNDANCY_THRESHOLDS = {
|
|
50
|
-
MINIMAL:
|
|
51
|
-
LOW:
|
|
52
|
-
MEDIUM:
|
|
53
|
-
HIGH:
|
|
50
|
+
MINIMAL: "minimal",
|
|
51
|
+
LOW: "low",
|
|
52
|
+
MEDIUM: "medium",
|
|
53
|
+
HIGH: "high",
|
|
54
54
|
};
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
57
|
* Supported publishing platforms
|
|
58
58
|
*/
|
|
59
59
|
const PLATFORMS = {
|
|
60
|
-
WORDPRESS:
|
|
61
|
-
PAYLOADCMS:
|
|
62
|
-
STATIC:
|
|
63
|
-
DOCUSAURUS:
|
|
64
|
-
MINTLIFY:
|
|
65
|
-
ASTRO:
|
|
60
|
+
WORDPRESS: "wordpress",
|
|
61
|
+
PAYLOADCMS: "payloadcms",
|
|
62
|
+
STATIC: "static",
|
|
63
|
+
DOCUSAURUS: "docusaurus",
|
|
64
|
+
MINTLIFY: "mintlify",
|
|
65
|
+
ASTRO: "astro",
|
|
66
66
|
};
|
|
67
67
|
|
|
68
68
|
/**
|
|
@@ -73,9 +73,9 @@ const DEFAULT_CONFIG = {
|
|
|
73
73
|
retryAttempts: 2,
|
|
74
74
|
retryDelay: 1000,
|
|
75
75
|
checkpointInterval: 5000,
|
|
76
|
-
stateFileName:
|
|
77
|
-
queueFileName:
|
|
78
|
-
outputDir:
|
|
76
|
+
stateFileName: ".content-production-coordinator-state.json",
|
|
77
|
+
queueFileName: ".content-queue.json",
|
|
78
|
+
outputDir: ".",
|
|
79
79
|
dryRun: false,
|
|
80
80
|
force: false,
|
|
81
81
|
verbose: false,
|
|
@@ -83,21 +83,21 @@ const DEFAULT_CONFIG = {
|
|
|
83
83
|
contentRulesPath: null,
|
|
84
84
|
// Webhooks
|
|
85
85
|
webhookUrl: null,
|
|
86
|
-
webhookEvents: [
|
|
86
|
+
webhookEvents: ["complete", "error"],
|
|
87
87
|
// Analytics
|
|
88
88
|
enableAnalytics: true,
|
|
89
89
|
// Platform
|
|
90
90
|
defaultPlatform: PLATFORMS.WORDPRESS,
|
|
91
91
|
// Cron/Scheduling
|
|
92
92
|
cronMode: false,
|
|
93
|
-
lockFileName:
|
|
94
|
-
lastRunFileName:
|
|
93
|
+
lockFileName: ".content-production-coordinator.lock",
|
|
94
|
+
lastRunFileName: ".content-production-coordinator-lastrun.json",
|
|
95
95
|
minRunInterval: 0, // Minimum seconds between runs (0 = no limit)
|
|
96
96
|
quietMode: false, // Suppress non-error output for cron
|
|
97
97
|
// WordPress Scheduling
|
|
98
98
|
useWordPressScheduling: true, // Use WP native scheduling for future posts
|
|
99
99
|
defaultPublishDelay: 0, // Hours to delay publication (0 = immediate)
|
|
100
|
-
publishSpreadInterval: 0 // Hours between scheduled posts (0 = no spreading)
|
|
100
|
+
publishSpreadInterval: 0, // Hours between scheduled posts (0 = no spreading)
|
|
101
101
|
};
|
|
102
102
|
|
|
103
103
|
/**
|
|
@@ -118,13 +118,13 @@ class ContentRulesManager {
|
|
|
118
118
|
if (!this.rulesPath) return null;
|
|
119
119
|
|
|
120
120
|
try {
|
|
121
|
-
const content = await fs.readFile(this.rulesPath,
|
|
121
|
+
const content = await fs.readFile(this.rulesPath, "utf8");
|
|
122
122
|
this.rules = this.parseContentRules(content);
|
|
123
123
|
this.loaded = true;
|
|
124
124
|
return this.rules;
|
|
125
125
|
} catch (err) {
|
|
126
|
-
if (err.code !==
|
|
127
|
-
console.error(
|
|
126
|
+
if (err.code !== "ENOENT") {
|
|
127
|
+
console.error("[ContentRules] Error loading rules:", err.message);
|
|
128
128
|
}
|
|
129
129
|
return null;
|
|
130
130
|
}
|
|
@@ -142,28 +142,28 @@ class ContentRulesManager {
|
|
|
142
142
|
writingStyle: {},
|
|
143
143
|
seoGuidelines: {},
|
|
144
144
|
formatting: {},
|
|
145
|
-
contentBoundaries: {}
|
|
145
|
+
contentBoundaries: {},
|
|
146
146
|
};
|
|
147
147
|
|
|
148
148
|
// Extract sections using markdown headers
|
|
149
149
|
const sections = content.split(/^## /m).slice(1);
|
|
150
150
|
|
|
151
151
|
for (const section of sections) {
|
|
152
|
-
const lines = section.split(
|
|
152
|
+
const lines = section.split("\n");
|
|
153
153
|
const header = lines[0].trim().toLowerCase();
|
|
154
|
-
const body = lines.slice(1).join(
|
|
154
|
+
const body = lines.slice(1).join("\n").trim();
|
|
155
155
|
|
|
156
|
-
if (header.includes(
|
|
156
|
+
if (header.includes("brand") || header.includes("identity")) {
|
|
157
157
|
rules.brandIdentity = this.parseSection(body);
|
|
158
|
-
} else if (header.includes(
|
|
158
|
+
} else if (header.includes("voice") || header.includes("tone")) {
|
|
159
159
|
rules.voiceTone = this.parseSection(body);
|
|
160
|
-
} else if (header.includes(
|
|
160
|
+
} else if (header.includes("writing") || header.includes("style")) {
|
|
161
161
|
rules.writingStyle = this.parseSection(body);
|
|
162
|
-
} else if (header.includes(
|
|
162
|
+
} else if (header.includes("seo")) {
|
|
163
163
|
rules.seoGuidelines = this.parseSection(body);
|
|
164
|
-
} else if (header.includes(
|
|
164
|
+
} else if (header.includes("format")) {
|
|
165
165
|
rules.formatting = this.parseSection(body);
|
|
166
|
-
} else if (header.includes(
|
|
166
|
+
} else if (header.includes("boundar") || header.includes("avoid")) {
|
|
167
167
|
rules.contentBoundaries = this.parseSection(body);
|
|
168
168
|
}
|
|
169
169
|
}
|
|
@@ -178,13 +178,13 @@ class ContentRulesManager {
|
|
|
178
178
|
*/
|
|
179
179
|
parseSection(body) {
|
|
180
180
|
const result = {};
|
|
181
|
-
const lines = body.split(
|
|
181
|
+
const lines = body.split("\n");
|
|
182
182
|
|
|
183
183
|
for (const line of lines) {
|
|
184
184
|
// Parse list items: - **Key**: Value
|
|
185
185
|
const match = line.match(/^[-*]\s*\*\*([^*]+)\*\*:\s*(.+)$/);
|
|
186
186
|
if (match) {
|
|
187
|
-
const key = match[1].trim().toLowerCase().replace(/\s+/g,
|
|
187
|
+
const key = match[1].trim().toLowerCase().replace(/\s+/g, "_");
|
|
188
188
|
result[key] = match[2].trim();
|
|
189
189
|
}
|
|
190
190
|
// Parse simple list items: - Value
|
|
@@ -209,7 +209,7 @@ class ContentRulesManager {
|
|
|
209
209
|
brandVoice: this.rules.voiceTone,
|
|
210
210
|
writingStyle: this.rules.writingStyle,
|
|
211
211
|
contentBoundaries: this.rules.contentBoundaries,
|
|
212
|
-
checkBrandAlignment: true
|
|
212
|
+
checkBrandAlignment: true,
|
|
213
213
|
};
|
|
214
214
|
}
|
|
215
215
|
|
|
@@ -223,7 +223,7 @@ class ContentRulesManager {
|
|
|
223
223
|
return {
|
|
224
224
|
brandIdentity: this.rules.brandIdentity,
|
|
225
225
|
seoGuidelines: this.rules.seoGuidelines,
|
|
226
|
-
formatting: this.rules.formatting
|
|
226
|
+
formatting: this.rules.formatting,
|
|
227
227
|
};
|
|
228
228
|
}
|
|
229
229
|
}
|
|
@@ -238,27 +238,27 @@ class AnalyticsTracker {
|
|
|
238
238
|
startTime: null,
|
|
239
239
|
endTime: null,
|
|
240
240
|
duration: null,
|
|
241
|
-
phases: {}
|
|
241
|
+
phases: {},
|
|
242
242
|
},
|
|
243
243
|
items: {
|
|
244
244
|
total: 0,
|
|
245
245
|
verified: 0,
|
|
246
246
|
published: 0,
|
|
247
247
|
failed: 0,
|
|
248
|
-
skipped: 0
|
|
248
|
+
skipped: 0,
|
|
249
249
|
},
|
|
250
250
|
performance: {
|
|
251
251
|
avgVerificationTime: 0,
|
|
252
252
|
avgPublishTime: 0,
|
|
253
253
|
totalVerificationTime: 0,
|
|
254
|
-
totalPublishTime: 0
|
|
254
|
+
totalPublishTime: 0,
|
|
255
255
|
},
|
|
256
256
|
quality: {
|
|
257
257
|
avgRedundancyScore: 0,
|
|
258
258
|
avgUniquenessScore: 0,
|
|
259
|
-
contentQualityTrend: []
|
|
259
|
+
contentQualityTrend: [],
|
|
260
260
|
},
|
|
261
|
-
errors: []
|
|
261
|
+
errors: [],
|
|
262
262
|
};
|
|
263
263
|
this.phaseTimers = {};
|
|
264
264
|
}
|
|
@@ -275,7 +275,8 @@ class AnalyticsTracker {
|
|
|
275
275
|
*/
|
|
276
276
|
endWorkflow() {
|
|
277
277
|
this.metrics.workflow.endTime = Date.now();
|
|
278
|
-
this.metrics.workflow.duration =
|
|
278
|
+
this.metrics.workflow.duration =
|
|
279
|
+
this.metrics.workflow.endTime - this.metrics.workflow.startTime;
|
|
279
280
|
}
|
|
280
281
|
|
|
281
282
|
/**
|
|
@@ -294,7 +295,7 @@ class AnalyticsTracker {
|
|
|
294
295
|
if (this.phaseTimers[phase]) {
|
|
295
296
|
this.metrics.workflow.phases[phase] = {
|
|
296
297
|
duration: Date.now() - this.phaseTimers[phase],
|
|
297
|
-
completedAt: new Date().toISOString()
|
|
298
|
+
completedAt: new Date().toISOString(),
|
|
298
299
|
};
|
|
299
300
|
}
|
|
300
301
|
}
|
|
@@ -308,18 +309,19 @@ class AnalyticsTracker {
|
|
|
308
309
|
this.metrics.items.verified++;
|
|
309
310
|
this.metrics.performance.totalVerificationTime += duration;
|
|
310
311
|
this.metrics.performance.avgVerificationTime =
|
|
311
|
-
this.metrics.performance.totalVerificationTime /
|
|
312
|
+
this.metrics.performance.totalVerificationTime /
|
|
313
|
+
this.metrics.items.verified;
|
|
312
314
|
|
|
313
315
|
// Track quality metrics
|
|
314
316
|
if (item.verification) {
|
|
315
317
|
const scores = { minimal: 1, low: 2, medium: 3, high: 4 };
|
|
316
|
-
const score = (item.verification.redundancyScore ||
|
|
318
|
+
const score = (item.verification.redundancyScore || "").toLowerCase();
|
|
317
319
|
if (scores[score]) {
|
|
318
320
|
this.metrics.quality.contentQualityTrend.push({
|
|
319
321
|
item: item.metadata.title,
|
|
320
322
|
redundancyScore: scores[score],
|
|
321
323
|
uniquenessScore: item.verification.uniquenessScore || 0,
|
|
322
|
-
timestamp: new Date().toISOString()
|
|
324
|
+
timestamp: new Date().toISOString(),
|
|
323
325
|
});
|
|
324
326
|
|
|
325
327
|
// Update averages
|
|
@@ -327,7 +329,8 @@ class AnalyticsTracker {
|
|
|
327
329
|
this.metrics.quality.avgRedundancyScore =
|
|
328
330
|
trend.reduce((sum, t) => sum + t.redundancyScore, 0) / trend.length;
|
|
329
331
|
this.metrics.quality.avgUniquenessScore =
|
|
330
|
-
trend.reduce((sum, t) => sum + (t.uniquenessScore || 0), 0) /
|
|
332
|
+
trend.reduce((sum, t) => sum + (t.uniquenessScore || 0), 0) /
|
|
333
|
+
trend.length;
|
|
331
334
|
}
|
|
332
335
|
}
|
|
333
336
|
}
|
|
@@ -353,10 +356,10 @@ class AnalyticsTracker {
|
|
|
353
356
|
trackError(item, error, phase) {
|
|
354
357
|
this.metrics.items.failed++;
|
|
355
358
|
this.metrics.errors.push({
|
|
356
|
-
item: item.metadata?.title ||
|
|
359
|
+
item: item.metadata?.title || "Unknown",
|
|
357
360
|
error: error.message,
|
|
358
361
|
phase,
|
|
359
|
-
timestamp: new Date().toISOString()
|
|
362
|
+
timestamp: new Date().toISOString(),
|
|
360
363
|
});
|
|
361
364
|
}
|
|
362
365
|
|
|
@@ -373,19 +376,28 @@ class AnalyticsTracker {
|
|
|
373
376
|
* @returns {Object} Analytics summary
|
|
374
377
|
*/
|
|
375
378
|
getSummary() {
|
|
376
|
-
const successRate =
|
|
377
|
-
|
|
378
|
-
|
|
379
|
+
const successRate =
|
|
380
|
+
this.metrics.items.total > 0
|
|
381
|
+
? (
|
|
382
|
+
(this.metrics.items.published / this.metrics.items.total) *
|
|
383
|
+
100
|
|
384
|
+
).toFixed(1)
|
|
385
|
+
: 0;
|
|
379
386
|
|
|
380
387
|
return {
|
|
381
388
|
...this.metrics,
|
|
382
389
|
summary: {
|
|
383
390
|
successRate: `${successRate}%`,
|
|
384
391
|
totalDuration: this.formatDuration(this.metrics.workflow.duration),
|
|
385
|
-
avgVerificationTime: this.formatDuration(
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
392
|
+
avgVerificationTime: this.formatDuration(
|
|
393
|
+
this.metrics.performance.avgVerificationTime,
|
|
394
|
+
),
|
|
395
|
+
avgPublishTime: this.formatDuration(
|
|
396
|
+
this.metrics.performance.avgPublishTime,
|
|
397
|
+
),
|
|
398
|
+
avgQualityScore:
|
|
399
|
+
(5 - this.metrics.quality.avgRedundancyScore).toFixed(2) + "/4",
|
|
400
|
+
},
|
|
389
401
|
};
|
|
390
402
|
}
|
|
391
403
|
|
|
@@ -395,7 +407,7 @@ class AnalyticsTracker {
|
|
|
395
407
|
* @returns {string} Formatted duration
|
|
396
408
|
*/
|
|
397
409
|
formatDuration(ms) {
|
|
398
|
-
if (!ms) return
|
|
410
|
+
if (!ms) return "N/A";
|
|
399
411
|
const seconds = Math.floor(ms / 1000);
|
|
400
412
|
const minutes = Math.floor(seconds / 60);
|
|
401
413
|
const hours = Math.floor(minutes / 60);
|
|
@@ -412,7 +424,7 @@ class AnalyticsTracker {
|
|
|
412
424
|
class WebhookNotifier {
|
|
413
425
|
constructor(config) {
|
|
414
426
|
this.url = config.webhookUrl;
|
|
415
|
-
this.events = config.webhookEvents || [
|
|
427
|
+
this.events = config.webhookEvents || ["complete", "error"];
|
|
416
428
|
this.enabled = !!this.url;
|
|
417
429
|
}
|
|
418
430
|
|
|
@@ -430,7 +442,7 @@ class WebhookNotifier {
|
|
|
430
442
|
const data = JSON.stringify({
|
|
431
443
|
event,
|
|
432
444
|
timestamp: new Date().toISOString(),
|
|
433
|
-
payload
|
|
445
|
+
payload,
|
|
434
446
|
});
|
|
435
447
|
|
|
436
448
|
return new Promise((resolve) => {
|
|
@@ -438,23 +450,23 @@ class WebhookNotifier {
|
|
|
438
450
|
const urlObj = new URL(this.url);
|
|
439
451
|
const options = {
|
|
440
452
|
hostname: urlObj.hostname,
|
|
441
|
-
port: urlObj.port || (urlObj.protocol ===
|
|
453
|
+
port: urlObj.port || (urlObj.protocol === "https:" ? 443 : 80),
|
|
442
454
|
path: urlObj.pathname + urlObj.search,
|
|
443
|
-
method:
|
|
455
|
+
method: "POST",
|
|
444
456
|
headers: {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
}
|
|
457
|
+
"Content-Type": "application/json",
|
|
458
|
+
"Content-Length": Buffer.byteLength(data),
|
|
459
|
+
"User-Agent": "ContentCoordinator/1.0",
|
|
460
|
+
},
|
|
449
461
|
};
|
|
450
462
|
|
|
451
|
-
const protocol = urlObj.protocol ===
|
|
463
|
+
const protocol = urlObj.protocol === "https:" ? https : http;
|
|
452
464
|
const req = protocol.request(options, (res) => {
|
|
453
465
|
resolve(res.statusCode >= 200 && res.statusCode < 300);
|
|
454
466
|
});
|
|
455
467
|
|
|
456
|
-
req.on(
|
|
457
|
-
console.error(
|
|
468
|
+
req.on("error", (err) => {
|
|
469
|
+
console.error("[Webhook] Error:", err.message);
|
|
458
470
|
resolve(false);
|
|
459
471
|
});
|
|
460
472
|
|
|
@@ -466,7 +478,7 @@ class WebhookNotifier {
|
|
|
466
478
|
req.write(data);
|
|
467
479
|
req.end();
|
|
468
480
|
} catch (err) {
|
|
469
|
-
console.error(
|
|
481
|
+
console.error("[Webhook] Error:", err.message);
|
|
470
482
|
resolve(false);
|
|
471
483
|
}
|
|
472
484
|
});
|
|
@@ -477,7 +489,7 @@ class WebhookNotifier {
|
|
|
477
489
|
* @param {Object} info - Workflow info
|
|
478
490
|
*/
|
|
479
491
|
async notifyStart(info) {
|
|
480
|
-
await this.notify(
|
|
492
|
+
await this.notify("start", info);
|
|
481
493
|
}
|
|
482
494
|
|
|
483
495
|
/**
|
|
@@ -485,7 +497,7 @@ class WebhookNotifier {
|
|
|
485
497
|
* @param {Object} results - Final results
|
|
486
498
|
*/
|
|
487
499
|
async notifyComplete(results) {
|
|
488
|
-
await this.notify(
|
|
500
|
+
await this.notify("complete", results);
|
|
489
501
|
}
|
|
490
502
|
|
|
491
503
|
/**
|
|
@@ -493,7 +505,7 @@ class WebhookNotifier {
|
|
|
493
505
|
* @param {Object} error - Error info
|
|
494
506
|
*/
|
|
495
507
|
async notifyError(error) {
|
|
496
|
-
await this.notify(
|
|
508
|
+
await this.notify("error", error);
|
|
497
509
|
}
|
|
498
510
|
|
|
499
511
|
/**
|
|
@@ -501,7 +513,7 @@ class WebhookNotifier {
|
|
|
501
513
|
* @param {Object} item - Published item info
|
|
502
514
|
*/
|
|
503
515
|
async notifyPublished(item) {
|
|
504
|
-
await this.notify(
|
|
516
|
+
await this.notify("published", item);
|
|
505
517
|
}
|
|
506
518
|
}
|
|
507
519
|
|
|
@@ -512,10 +524,10 @@ class QueueManager {
|
|
|
512
524
|
constructor(queuePath) {
|
|
513
525
|
this.queuePath = queuePath;
|
|
514
526
|
this.queue = {
|
|
515
|
-
version:
|
|
527
|
+
version: "1.0",
|
|
516
528
|
created: null,
|
|
517
529
|
updated: null,
|
|
518
|
-
items: []
|
|
530
|
+
items: [],
|
|
519
531
|
};
|
|
520
532
|
}
|
|
521
533
|
|
|
@@ -525,12 +537,12 @@ class QueueManager {
|
|
|
525
537
|
*/
|
|
526
538
|
async load() {
|
|
527
539
|
try {
|
|
528
|
-
const data = await fs.readFile(this.queuePath,
|
|
540
|
+
const data = await fs.readFile(this.queuePath, "utf8");
|
|
529
541
|
this.queue = JSON.parse(data);
|
|
530
542
|
return this.queue;
|
|
531
543
|
} catch (err) {
|
|
532
|
-
if (err.code !==
|
|
533
|
-
console.error(
|
|
544
|
+
if (err.code !== "ENOENT") {
|
|
545
|
+
console.error("[QueueManager] Error loading queue:", err.message);
|
|
534
546
|
}
|
|
535
547
|
this.queue.created = new Date().toISOString();
|
|
536
548
|
return this.queue;
|
|
@@ -555,11 +567,11 @@ class QueueManager {
|
|
|
555
567
|
const queueItem = {
|
|
556
568
|
id: `item-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
557
569
|
addedAt: new Date().toISOString(),
|
|
558
|
-
status:
|
|
559
|
-
priority: item.priority ||
|
|
570
|
+
status: "pending",
|
|
571
|
+
priority: item.priority || "normal",
|
|
560
572
|
filePath: item.filePath,
|
|
561
573
|
metadata: item.metadata || {},
|
|
562
|
-
...item
|
|
574
|
+
...item,
|
|
563
575
|
};
|
|
564
576
|
|
|
565
577
|
this.queue.items.push(queueItem);
|
|
@@ -573,7 +585,7 @@ class QueueManager {
|
|
|
573
585
|
* @returns {boolean} Whether item was removed
|
|
574
586
|
*/
|
|
575
587
|
async removeItem(id) {
|
|
576
|
-
const index = this.queue.items.findIndex(item => item.id === id);
|
|
588
|
+
const index = this.queue.items.findIndex((item) => item.id === id);
|
|
577
589
|
if (index === -1) return false;
|
|
578
590
|
|
|
579
591
|
this.queue.items.splice(index, 1);
|
|
@@ -588,7 +600,7 @@ class QueueManager {
|
|
|
588
600
|
* @param {Object} [extra] - Additional data to merge
|
|
589
601
|
*/
|
|
590
602
|
async updateItemStatus(id, status, extra = {}) {
|
|
591
|
-
const item = this.queue.items.find(i => i.id === id);
|
|
603
|
+
const item = this.queue.items.find((i) => i.id === id);
|
|
592
604
|
if (item) {
|
|
593
605
|
item.status = status;
|
|
594
606
|
item.updatedAt = new Date().toISOString();
|
|
@@ -604,7 +616,7 @@ class QueueManager {
|
|
|
604
616
|
getPendingItems() {
|
|
605
617
|
const priorityOrder = { high: 0, normal: 1, low: 2 };
|
|
606
618
|
return this.queue.items
|
|
607
|
-
.filter(item => item.status ===
|
|
619
|
+
.filter((item) => item.status === "pending")
|
|
608
620
|
.sort((a, b) => {
|
|
609
621
|
const pA = priorityOrder[a.priority] ?? 1;
|
|
610
622
|
const pB = priorityOrder[b.priority] ?? 1;
|
|
@@ -624,14 +636,15 @@ class QueueManager {
|
|
|
624
636
|
processing: 0,
|
|
625
637
|
completed: 0,
|
|
626
638
|
failed: 0,
|
|
627
|
-
byPriority: { high: 0, normal: 0, low: 0 }
|
|
639
|
+
byPriority: { high: 0, normal: 0, low: 0 },
|
|
628
640
|
};
|
|
629
641
|
|
|
630
642
|
for (const item of this.queue.items) {
|
|
631
|
-
if (item.status ===
|
|
632
|
-
else if (item.status ===
|
|
633
|
-
else if (item.status ===
|
|
634
|
-
|
|
643
|
+
if (item.status === "pending") stats.pending++;
|
|
644
|
+
else if (item.status === "processing") stats.processing++;
|
|
645
|
+
else if (item.status === "completed" || item.status === "published")
|
|
646
|
+
stats.completed++;
|
|
647
|
+
else if (item.status === "failed") stats.failed++;
|
|
635
648
|
|
|
636
649
|
if (stats.byPriority[item.priority] !== undefined) {
|
|
637
650
|
stats.byPriority[item.priority]++;
|
|
@@ -648,7 +661,7 @@ class QueueManager {
|
|
|
648
661
|
async clearCompleted() {
|
|
649
662
|
const before = this.queue.items.length;
|
|
650
663
|
this.queue.items = this.queue.items.filter(
|
|
651
|
-
item => ![
|
|
664
|
+
(item) => !["completed", "published"].includes(item.status),
|
|
652
665
|
);
|
|
653
666
|
await this.save();
|
|
654
667
|
return before - this.queue.items.length;
|
|
@@ -671,78 +684,80 @@ class PlatformPublisher {
|
|
|
671
684
|
initializePlatforms() {
|
|
672
685
|
// WordPress publisher
|
|
673
686
|
this.platforms.set(PLATFORMS.WORDPRESS, {
|
|
674
|
-
name:
|
|
687
|
+
name: "WordPress",
|
|
675
688
|
publish: async (item, publishFn) => {
|
|
676
689
|
return publishFn(item, {
|
|
677
690
|
platform: PLATFORMS.WORDPRESS,
|
|
678
|
-
endpoint:
|
|
691
|
+
endpoint: "wp-json/wp/v2/posts",
|
|
679
692
|
});
|
|
680
693
|
},
|
|
681
694
|
validateConfig: () => {
|
|
682
|
-
return !!(
|
|
683
|
-
|
|
695
|
+
return !!(
|
|
696
|
+
process.env.WORDPRESS_URL && process.env.WORDPRESS_APP_PASSWORD
|
|
697
|
+
);
|
|
698
|
+
},
|
|
684
699
|
});
|
|
685
700
|
|
|
686
701
|
// PayloadCMS publisher
|
|
687
702
|
this.platforms.set(PLATFORMS.PAYLOADCMS, {
|
|
688
|
-
name:
|
|
703
|
+
name: "PayloadCMS",
|
|
689
704
|
publish: async (item, publishFn) => {
|
|
690
705
|
return publishFn(item, {
|
|
691
706
|
platform: PLATFORMS.PAYLOADCMS,
|
|
692
|
-
endpoint:
|
|
707
|
+
endpoint: "api/posts",
|
|
693
708
|
});
|
|
694
709
|
},
|
|
695
710
|
validateConfig: () => {
|
|
696
711
|
return !!(process.env.PAYLOADCMS_URL && process.env.PAYLOADCMS_EMAIL);
|
|
697
|
-
}
|
|
712
|
+
},
|
|
698
713
|
});
|
|
699
714
|
|
|
700
715
|
// Static site generator (writes to file)
|
|
701
716
|
this.platforms.set(PLATFORMS.STATIC, {
|
|
702
|
-
name:
|
|
717
|
+
name: "Static",
|
|
703
718
|
publish: async (item, publishFn) => {
|
|
704
719
|
return publishFn(item, {
|
|
705
720
|
platform: PLATFORMS.STATIC,
|
|
706
|
-
outputDir: this.config.staticOutputDir ||
|
|
721
|
+
outputDir: this.config.staticOutputDir || "./content",
|
|
707
722
|
});
|
|
708
723
|
},
|
|
709
|
-
validateConfig: () => true
|
|
724
|
+
validateConfig: () => true,
|
|
710
725
|
});
|
|
711
726
|
|
|
712
727
|
// Docusaurus
|
|
713
728
|
this.platforms.set(PLATFORMS.DOCUSAURUS, {
|
|
714
|
-
name:
|
|
729
|
+
name: "Docusaurus",
|
|
715
730
|
publish: async (item, publishFn) => {
|
|
716
731
|
return publishFn(item, {
|
|
717
732
|
platform: PLATFORMS.DOCUSAURUS,
|
|
718
|
-
outputDir: this.config.docusaurusDir ||
|
|
733
|
+
outputDir: this.config.docusaurusDir || "./docs",
|
|
719
734
|
});
|
|
720
735
|
},
|
|
721
|
-
validateConfig: () => true
|
|
736
|
+
validateConfig: () => true,
|
|
722
737
|
});
|
|
723
738
|
|
|
724
739
|
// Mintlify
|
|
725
740
|
this.platforms.set(PLATFORMS.MINTLIFY, {
|
|
726
|
-
name:
|
|
741
|
+
name: "Mintlify",
|
|
727
742
|
publish: async (item, publishFn) => {
|
|
728
743
|
return publishFn(item, {
|
|
729
744
|
platform: PLATFORMS.MINTLIFY,
|
|
730
|
-
outputDir: this.config.mintlifyDir ||
|
|
745
|
+
outputDir: this.config.mintlifyDir || "./docs",
|
|
731
746
|
});
|
|
732
747
|
},
|
|
733
|
-
validateConfig: () => true
|
|
748
|
+
validateConfig: () => true,
|
|
734
749
|
});
|
|
735
750
|
|
|
736
751
|
// Astro
|
|
737
752
|
this.platforms.set(PLATFORMS.ASTRO, {
|
|
738
|
-
name:
|
|
753
|
+
name: "Astro",
|
|
739
754
|
publish: async (item, publishFn) => {
|
|
740
755
|
return publishFn(item, {
|
|
741
756
|
platform: PLATFORMS.ASTRO,
|
|
742
|
-
outputDir: this.config.astroDir ||
|
|
757
|
+
outputDir: this.config.astroDir || "./src/content",
|
|
743
758
|
});
|
|
744
759
|
},
|
|
745
|
-
validateConfig: () => true
|
|
760
|
+
validateConfig: () => true,
|
|
746
761
|
});
|
|
747
762
|
}
|
|
748
763
|
|
|
@@ -752,7 +767,10 @@ class PlatformPublisher {
|
|
|
752
767
|
* @returns {Object|null} Platform publisher
|
|
753
768
|
*/
|
|
754
769
|
getPublisher(platform) {
|
|
755
|
-
return
|
|
770
|
+
return (
|
|
771
|
+
this.platforms.get(platform) ||
|
|
772
|
+
this.platforms.get(this.config.defaultPlatform)
|
|
773
|
+
);
|
|
756
774
|
}
|
|
757
775
|
|
|
758
776
|
/**
|
|
@@ -805,7 +823,7 @@ class CronHelper {
|
|
|
805
823
|
try {
|
|
806
824
|
// Check if lock file exists and is stale
|
|
807
825
|
try {
|
|
808
|
-
const lockData = await fs.readFile(this.lockPath,
|
|
826
|
+
const lockData = await fs.readFile(this.lockPath, "utf8");
|
|
809
827
|
const lock = JSON.parse(lockData);
|
|
810
828
|
|
|
811
829
|
// Check if lock is stale (older than 1 hour)
|
|
@@ -813,31 +831,35 @@ class CronHelper {
|
|
|
813
831
|
const maxAge = 60 * 60 * 1000; // 1 hour
|
|
814
832
|
|
|
815
833
|
if (lockAge > maxAge) {
|
|
816
|
-
console.error(
|
|
834
|
+
console.error("[CronHelper] Stale lock detected, removing...");
|
|
817
835
|
await fs.unlink(this.lockPath);
|
|
818
836
|
} else {
|
|
819
837
|
// Lock is held by another process
|
|
820
838
|
if (!this.config.quietMode) {
|
|
821
|
-
console.log(
|
|
839
|
+
console.log(
|
|
840
|
+
`[CronHelper] Lock held by PID ${lock.pid} since ${lock.timestamp}`,
|
|
841
|
+
);
|
|
822
842
|
}
|
|
823
843
|
return false;
|
|
824
844
|
}
|
|
825
845
|
} catch (err) {
|
|
826
846
|
// Lock file doesn't exist, we can proceed
|
|
827
|
-
if (err.code !==
|
|
847
|
+
if (err.code !== "ENOENT") throw err;
|
|
828
848
|
}
|
|
829
849
|
|
|
830
850
|
// Create lock file
|
|
831
851
|
const lockData = {
|
|
832
852
|
pid: process.pid,
|
|
833
853
|
timestamp: new Date().toISOString(),
|
|
834
|
-
hostname: os.hostname()
|
|
854
|
+
hostname: os.hostname(),
|
|
835
855
|
};
|
|
836
856
|
|
|
837
|
-
await fs.writeFile(this.lockPath, JSON.stringify(lockData, null, 2), {
|
|
857
|
+
await fs.writeFile(this.lockPath, JSON.stringify(lockData, null, 2), {
|
|
858
|
+
flag: "wx",
|
|
859
|
+
});
|
|
838
860
|
return true;
|
|
839
861
|
} catch (err) {
|
|
840
|
-
if (err.code ===
|
|
862
|
+
if (err.code === "EEXIST") {
|
|
841
863
|
// Another process created the lock first
|
|
842
864
|
return false;
|
|
843
865
|
}
|
|
@@ -853,8 +875,8 @@ class CronHelper {
|
|
|
853
875
|
try {
|
|
854
876
|
await fs.unlink(this.lockPath);
|
|
855
877
|
} catch (err) {
|
|
856
|
-
if (err.code !==
|
|
857
|
-
console.error(
|
|
878
|
+
if (err.code !== "ENOENT") {
|
|
879
|
+
console.error("[CronHelper] Error releasing lock:", err.message);
|
|
858
880
|
}
|
|
859
881
|
}
|
|
860
882
|
}
|
|
@@ -869,19 +891,21 @@ class CronHelper {
|
|
|
869
891
|
}
|
|
870
892
|
|
|
871
893
|
try {
|
|
872
|
-
const data = await fs.readFile(this.lastRunPath,
|
|
894
|
+
const data = await fs.readFile(this.lastRunPath, "utf8");
|
|
873
895
|
const lastRunInfo = JSON.parse(data);
|
|
874
896
|
const lastRun = new Date(lastRunInfo.timestamp);
|
|
875
897
|
const elapsed = (Date.now() - lastRun.getTime()) / 1000;
|
|
876
898
|
|
|
877
899
|
if (elapsed < this.config.minRunInterval) {
|
|
878
|
-
const nextRunTime = new Date(
|
|
900
|
+
const nextRunTime = new Date(
|
|
901
|
+
lastRun.getTime() + this.config.minRunInterval * 1000,
|
|
902
|
+
);
|
|
879
903
|
return { canRun: false, nextRunTime, lastRun };
|
|
880
904
|
}
|
|
881
905
|
|
|
882
906
|
return { canRun: true, nextRunTime: null, lastRun };
|
|
883
907
|
} catch (err) {
|
|
884
|
-
if (err.code ===
|
|
908
|
+
if (err.code === "ENOENT") {
|
|
885
909
|
// No last run file, first run
|
|
886
910
|
return { canRun: true, nextRunTime: null, lastRun: null };
|
|
887
911
|
}
|
|
@@ -902,8 +926,8 @@ class CronHelper {
|
|
|
902
926
|
total: results.stats?.totalItems || 0,
|
|
903
927
|
published: results.stats?.published || 0,
|
|
904
928
|
failed: results.stats?.failed || 0,
|
|
905
|
-
duration: results.duration
|
|
906
|
-
}
|
|
929
|
+
duration: results.duration,
|
|
930
|
+
},
|
|
907
931
|
};
|
|
908
932
|
|
|
909
933
|
await fs.writeFile(this.lastRunPath, JSON.stringify(runInfo, null, 2));
|
|
@@ -915,10 +939,10 @@ class CronHelper {
|
|
|
915
939
|
*/
|
|
916
940
|
async getLastRun() {
|
|
917
941
|
try {
|
|
918
|
-
const data = await fs.readFile(this.lastRunPath,
|
|
942
|
+
const data = await fs.readFile(this.lastRunPath, "utf8");
|
|
919
943
|
return JSON.parse(data);
|
|
920
944
|
} catch (err) {
|
|
921
|
-
if (err.code ===
|
|
945
|
+
if (err.code === "ENOENT") return null;
|
|
922
946
|
throw err;
|
|
923
947
|
}
|
|
924
948
|
}
|
|
@@ -930,15 +954,16 @@ class CronHelper {
|
|
|
930
954
|
*/
|
|
931
955
|
generateCrontabEntry(options = {}) {
|
|
932
956
|
const {
|
|
933
|
-
schedule =
|
|
957
|
+
schedule = "0 */6 * * *", // Default: every 6 hours
|
|
934
958
|
workDir = process.cwd(),
|
|
935
|
-
contentDir =
|
|
936
|
-
logFile =
|
|
937
|
-
user = process.env.USER ||
|
|
938
|
-
extraFlags =
|
|
959
|
+
contentDir = "content-queue",
|
|
960
|
+
logFile = "/var/log/content-production-coordinator.log",
|
|
961
|
+
user = process.env.USER || "ubuntu",
|
|
962
|
+
extraFlags = "",
|
|
939
963
|
} = options;
|
|
940
964
|
|
|
941
|
-
const command =
|
|
965
|
+
const command =
|
|
966
|
+
`cd ${workDir} && /usr/bin/npx myaidev-method coordinate-content ${contentDir} --cron --force ${extraFlags}`.trim();
|
|
942
967
|
|
|
943
968
|
return `# Content Coordinator - Automated publishing
|
|
944
969
|
# Schedule: ${this.describeCronSchedule(schedule)}
|
|
@@ -946,7 +971,7 @@ class CronHelper {
|
|
|
946
971
|
${schedule} ${user} ${command} >> ${logFile} 2>&1
|
|
947
972
|
|
|
948
973
|
# Alternative with flock for extra safety:
|
|
949
|
-
# ${schedule} ${user} /usr/bin/flock -n /tmp/content-coordinator.lock ${command} >> ${logFile} 2>&1
|
|
974
|
+
# ${schedule} ${user} /usr/bin/flock -n /tmp/content-production-coordinator.lock ${command} >> ${logFile} 2>&1
|
|
950
975
|
`;
|
|
951
976
|
}
|
|
952
977
|
|
|
@@ -956,23 +981,23 @@ ${schedule} ${user} ${command} >> ${logFile} 2>&1
|
|
|
956
981
|
* @returns {string} Human-readable description
|
|
957
982
|
*/
|
|
958
983
|
describeCronSchedule(schedule) {
|
|
959
|
-
const parts = schedule.split(
|
|
984
|
+
const parts = schedule.split(" ");
|
|
960
985
|
if (parts.length !== 5) return schedule;
|
|
961
986
|
|
|
962
987
|
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
|
|
963
988
|
|
|
964
989
|
// Common patterns
|
|
965
|
-
if (schedule ===
|
|
966
|
-
if (schedule ===
|
|
967
|
-
if (schedule ===
|
|
968
|
-
if (schedule ===
|
|
969
|
-
if (schedule ===
|
|
970
|
-
if (schedule ===
|
|
971
|
-
if (schedule ===
|
|
972
|
-
if (schedule ===
|
|
973
|
-
if (schedule ===
|
|
974
|
-
if (schedule ===
|
|
975
|
-
if (schedule ===
|
|
990
|
+
if (schedule === "0 * * * *") return "Every hour";
|
|
991
|
+
if (schedule === "*/15 * * * *") return "Every 15 minutes";
|
|
992
|
+
if (schedule === "*/30 * * * *") return "Every 30 minutes";
|
|
993
|
+
if (schedule === "0 */2 * * *") return "Every 2 hours";
|
|
994
|
+
if (schedule === "0 */6 * * *") return "Every 6 hours";
|
|
995
|
+
if (schedule === "0 */12 * * *") return "Every 12 hours";
|
|
996
|
+
if (schedule === "0 0 * * *") return "Daily at midnight";
|
|
997
|
+
if (schedule === "0 9 * * *") return "Daily at 9:00 AM";
|
|
998
|
+
if (schedule === "0 9 * * 1-5") return "Weekdays at 9:00 AM";
|
|
999
|
+
if (schedule === "0 0 * * 0") return "Weekly on Sunday at midnight";
|
|
1000
|
+
if (schedule === "0 0 1 * *") return "Monthly on the 1st at midnight";
|
|
976
1001
|
|
|
977
1002
|
return schedule;
|
|
978
1003
|
}
|
|
@@ -984,35 +1009,35 @@ ${schedule} ${user} ${command} >> ${logFile} 2>&1
|
|
|
984
1009
|
getSuggestedSchedules() {
|
|
985
1010
|
return [
|
|
986
1011
|
{
|
|
987
|
-
name:
|
|
988
|
-
schedule:
|
|
989
|
-
description:
|
|
1012
|
+
name: "High frequency",
|
|
1013
|
+
schedule: "*/30 * * * *",
|
|
1014
|
+
description: "Every 30 minutes - for time-sensitive content",
|
|
990
1015
|
},
|
|
991
1016
|
{
|
|
992
|
-
name:
|
|
993
|
-
schedule:
|
|
994
|
-
description:
|
|
1017
|
+
name: "Regular",
|
|
1018
|
+
schedule: "0 */6 * * *",
|
|
1019
|
+
description: "Every 6 hours - balanced approach",
|
|
995
1020
|
},
|
|
996
1021
|
{
|
|
997
|
-
name:
|
|
998
|
-
schedule:
|
|
999
|
-
description:
|
|
1022
|
+
name: "Daily morning",
|
|
1023
|
+
schedule: "0 9 * * *",
|
|
1024
|
+
description: "Daily at 9:00 AM - publish during business hours",
|
|
1000
1025
|
},
|
|
1001
1026
|
{
|
|
1002
|
-
name:
|
|
1003
|
-
schedule:
|
|
1004
|
-
description:
|
|
1027
|
+
name: "Weekdays only",
|
|
1028
|
+
schedule: "0 9 * * 1-5",
|
|
1029
|
+
description: "Weekdays at 9:00 AM - avoid weekend publishing",
|
|
1005
1030
|
},
|
|
1006
1031
|
{
|
|
1007
|
-
name:
|
|
1008
|
-
schedule:
|
|
1009
|
-
description:
|
|
1032
|
+
name: "Twice daily",
|
|
1033
|
+
schedule: "0 9,17 * * *",
|
|
1034
|
+
description: "9:00 AM and 5:00 PM - morning and evening",
|
|
1010
1035
|
},
|
|
1011
1036
|
{
|
|
1012
|
-
name:
|
|
1013
|
-
schedule:
|
|
1014
|
-
description:
|
|
1015
|
-
}
|
|
1037
|
+
name: "Weekly",
|
|
1038
|
+
schedule: "0 9 * * 1",
|
|
1039
|
+
description: "Every Monday at 9:00 AM - weekly content drops",
|
|
1040
|
+
},
|
|
1016
1041
|
];
|
|
1017
1042
|
}
|
|
1018
1043
|
|
|
@@ -1027,16 +1052,16 @@ ${schedule} ${user} ${command} >> ${logFile} 2>&1
|
|
|
1027
1052
|
if (parts.length !== 5) {
|
|
1028
1053
|
return {
|
|
1029
1054
|
valid: false,
|
|
1030
|
-
error: `Expected 5 fields (minute hour day month weekday), got ${parts.length}
|
|
1055
|
+
error: `Expected 5 fields (minute hour day month weekday), got ${parts.length}`,
|
|
1031
1056
|
};
|
|
1032
1057
|
}
|
|
1033
1058
|
|
|
1034
1059
|
const ranges = [
|
|
1035
|
-
{ name:
|
|
1036
|
-
{ name:
|
|
1037
|
-
{ name:
|
|
1038
|
-
{ name:
|
|
1039
|
-
{ name:
|
|
1060
|
+
{ name: "minute", min: 0, max: 59 },
|
|
1061
|
+
{ name: "hour", min: 0, max: 23 },
|
|
1062
|
+
{ name: "day of month", min: 1, max: 31 },
|
|
1063
|
+
{ name: "month", min: 1, max: 12 },
|
|
1064
|
+
{ name: "day of week", min: 0, max: 7 },
|
|
1040
1065
|
];
|
|
1041
1066
|
|
|
1042
1067
|
for (let i = 0; i < 5; i++) {
|
|
@@ -1044,32 +1069,47 @@ ${schedule} ${user} ${command} >> ${logFile} 2>&1
|
|
|
1044
1069
|
const range = ranges[i];
|
|
1045
1070
|
|
|
1046
1071
|
// Skip wildcards
|
|
1047
|
-
if (part ===
|
|
1072
|
+
if (part === "*") continue;
|
|
1048
1073
|
|
|
1049
1074
|
// Check step values (*/n)
|
|
1050
|
-
if (part.startsWith(
|
|
1075
|
+
if (part.startsWith("*/")) {
|
|
1051
1076
|
const step = parseInt(part.slice(2), 10);
|
|
1052
1077
|
if (isNaN(step) || step < 1) {
|
|
1053
|
-
return {
|
|
1078
|
+
return {
|
|
1079
|
+
valid: false,
|
|
1080
|
+
error: `Invalid step value in ${range.name}: ${part}`,
|
|
1081
|
+
};
|
|
1054
1082
|
}
|
|
1055
1083
|
continue;
|
|
1056
1084
|
}
|
|
1057
1085
|
|
|
1058
1086
|
// Check ranges (n-m)
|
|
1059
|
-
if (part.includes(
|
|
1060
|
-
const [start, end] = part.split(
|
|
1061
|
-
if (
|
|
1062
|
-
|
|
1087
|
+
if (part.includes("-")) {
|
|
1088
|
+
const [start, end] = part.split("-").map((n) => parseInt(n, 10));
|
|
1089
|
+
if (
|
|
1090
|
+
isNaN(start) ||
|
|
1091
|
+
isNaN(end) ||
|
|
1092
|
+
start < range.min ||
|
|
1093
|
+
end > range.max ||
|
|
1094
|
+
start > end
|
|
1095
|
+
) {
|
|
1096
|
+
return {
|
|
1097
|
+
valid: false,
|
|
1098
|
+
error: `Invalid range in ${range.name}: ${part}`,
|
|
1099
|
+
};
|
|
1063
1100
|
}
|
|
1064
1101
|
continue;
|
|
1065
1102
|
}
|
|
1066
1103
|
|
|
1067
1104
|
// Check lists (n,m,...)
|
|
1068
|
-
if (part.includes(
|
|
1069
|
-
const values = part.split(
|
|
1105
|
+
if (part.includes(",")) {
|
|
1106
|
+
const values = part.split(",").map((n) => parseInt(n, 10));
|
|
1070
1107
|
for (const val of values) {
|
|
1071
1108
|
if (isNaN(val) || val < range.min || val > range.max) {
|
|
1072
|
-
return {
|
|
1109
|
+
return {
|
|
1110
|
+
valid: false,
|
|
1111
|
+
error: `Invalid value in ${range.name} list: ${val}`,
|
|
1112
|
+
};
|
|
1073
1113
|
}
|
|
1074
1114
|
}
|
|
1075
1115
|
continue;
|
|
@@ -1078,7 +1118,10 @@ ${schedule} ${user} ${command} >> ${logFile} 2>&1
|
|
|
1078
1118
|
// Check single value
|
|
1079
1119
|
const val = parseInt(part, 10);
|
|
1080
1120
|
if (isNaN(val) || val < range.min || val > range.max) {
|
|
1081
|
-
return {
|
|
1121
|
+
return {
|
|
1122
|
+
valid: false,
|
|
1123
|
+
error: `Invalid ${range.name}: ${part} (must be ${range.min}-${range.max})`,
|
|
1124
|
+
};
|
|
1082
1125
|
}
|
|
1083
1126
|
}
|
|
1084
1127
|
|
|
@@ -1110,14 +1153,19 @@ class ScheduleManager {
|
|
|
1110
1153
|
calculatePublishDate(item, index, totalItems) {
|
|
1111
1154
|
// Check if item has explicit publish date in front matter
|
|
1112
1155
|
if (item.metadata.publish_date || item.metadata.scheduled_date) {
|
|
1113
|
-
const explicitDate = new Date(
|
|
1156
|
+
const explicitDate = new Date(
|
|
1157
|
+
item.metadata.publish_date || item.metadata.scheduled_date,
|
|
1158
|
+
);
|
|
1114
1159
|
if (!isNaN(explicitDate.getTime())) {
|
|
1115
1160
|
return explicitDate;
|
|
1116
1161
|
}
|
|
1117
1162
|
}
|
|
1118
1163
|
|
|
1119
1164
|
// If publish immediately
|
|
1120
|
-
if (
|
|
1165
|
+
if (
|
|
1166
|
+
this.config.defaultPublishDelay === 0 &&
|
|
1167
|
+
this.config.publishSpreadInterval === 0
|
|
1168
|
+
) {
|
|
1121
1169
|
return new Date(); // Now
|
|
1122
1170
|
}
|
|
1123
1171
|
|
|
@@ -1127,7 +1175,9 @@ class ScheduleManager {
|
|
|
1127
1175
|
|
|
1128
1176
|
// Apply spread interval if configured
|
|
1129
1177
|
if (this.config.publishSpreadInterval > 0) {
|
|
1130
|
-
baseTime.setHours(
|
|
1178
|
+
baseTime.setHours(
|
|
1179
|
+
baseTime.getHours() + index * this.config.publishSpreadInterval,
|
|
1180
|
+
);
|
|
1131
1181
|
}
|
|
1132
1182
|
|
|
1133
1183
|
return baseTime;
|
|
@@ -1142,9 +1192,9 @@ class ScheduleManager {
|
|
|
1142
1192
|
const now = new Date();
|
|
1143
1193
|
// If publish date is in the future (more than 1 minute from now), use 'future' status
|
|
1144
1194
|
if (publishDate.getTime() > now.getTime() + 60000) {
|
|
1145
|
-
return
|
|
1195
|
+
return "future";
|
|
1146
1196
|
}
|
|
1147
|
-
return
|
|
1197
|
+
return "publish";
|
|
1148
1198
|
}
|
|
1149
1199
|
|
|
1150
1200
|
/**
|
|
@@ -1171,8 +1221,8 @@ class ScheduleManager {
|
|
|
1171
1221
|
useWordPressScheduling: this.config.useWordPressScheduling,
|
|
1172
1222
|
publishDate: this.formatForWordPress(publishDate),
|
|
1173
1223
|
status: status,
|
|
1174
|
-
isScheduled: status ===
|
|
1175
|
-
scheduledFor: publishDate.toLocaleString()
|
|
1224
|
+
isScheduled: status === "future",
|
|
1225
|
+
scheduledFor: publishDate.toLocaleString(),
|
|
1176
1226
|
};
|
|
1177
1227
|
}
|
|
1178
1228
|
|
|
@@ -1188,7 +1238,7 @@ class ScheduleManager {
|
|
|
1188
1238
|
url: result.url,
|
|
1189
1239
|
scheduledFor: scheduleContext.publishDate,
|
|
1190
1240
|
status: scheduleContext.status,
|
|
1191
|
-
platform: item.metadata.target_platform
|
|
1241
|
+
platform: item.metadata.target_platform,
|
|
1192
1242
|
});
|
|
1193
1243
|
}
|
|
1194
1244
|
|
|
@@ -1197,17 +1247,20 @@ class ScheduleManager {
|
|
|
1197
1247
|
* @returns {Object} Scheduling summary
|
|
1198
1248
|
*/
|
|
1199
1249
|
getSummary() {
|
|
1200
|
-
const immediate = this.scheduledPosts.filter(p => p.status ===
|
|
1201
|
-
const future = this.scheduledPosts.filter(p => p.status ===
|
|
1250
|
+
const immediate = this.scheduledPosts.filter((p) => p.status === "publish");
|
|
1251
|
+
const future = this.scheduledPosts.filter((p) => p.status === "future");
|
|
1202
1252
|
|
|
1203
1253
|
return {
|
|
1204
1254
|
total: this.scheduledPosts.length,
|
|
1205
1255
|
publishedImmediately: immediate.length,
|
|
1206
1256
|
scheduledForFuture: future.length,
|
|
1207
1257
|
posts: this.scheduledPosts,
|
|
1208
|
-
nextScheduled:
|
|
1209
|
-
|
|
1210
|
-
|
|
1258
|
+
nextScheduled:
|
|
1259
|
+
future.length > 0
|
|
1260
|
+
? future.sort(
|
|
1261
|
+
(a, b) => new Date(a.scheduledFor) - new Date(b.scheduledFor),
|
|
1262
|
+
)[0]
|
|
1263
|
+
: null,
|
|
1211
1264
|
};
|
|
1212
1265
|
}
|
|
1213
1266
|
|
|
@@ -1235,14 +1288,14 @@ Generated: ${new Date().toISOString()}
|
|
|
1235
1288
|
|-------|---------------|----------|
|
|
1236
1289
|
`;
|
|
1237
1290
|
const futurePosts = this.scheduledPosts
|
|
1238
|
-
.filter(p => p.status ===
|
|
1291
|
+
.filter((p) => p.status === "future")
|
|
1239
1292
|
.sort((a, b) => new Date(a.scheduledFor) - new Date(b.scheduledFor));
|
|
1240
1293
|
|
|
1241
1294
|
for (const post of futurePosts) {
|
|
1242
1295
|
const date = new Date(post.scheduledFor).toLocaleString();
|
|
1243
1296
|
report += `| ${post.title} | ${date} | ${post.platform} |\n`;
|
|
1244
1297
|
}
|
|
1245
|
-
report +=
|
|
1298
|
+
report += "\n";
|
|
1246
1299
|
}
|
|
1247
1300
|
|
|
1248
1301
|
if (summary.publishedImmediately > 0) {
|
|
@@ -1251,7 +1304,9 @@ Generated: ${new Date().toISOString()}
|
|
|
1251
1304
|
| Title | URL | Platform |
|
|
1252
1305
|
|-------|-----|----------|
|
|
1253
1306
|
`;
|
|
1254
|
-
const immediatePosts = this.scheduledPosts.filter(
|
|
1307
|
+
const immediatePosts = this.scheduledPosts.filter(
|
|
1308
|
+
(p) => p.status === "publish",
|
|
1309
|
+
);
|
|
1255
1310
|
|
|
1256
1311
|
for (const post of immediatePosts) {
|
|
1257
1312
|
report += `| ${post.title} | ${post.url} | ${post.platform} |\n`;
|
|
@@ -1282,7 +1337,7 @@ class ContentCoordinator {
|
|
|
1282
1337
|
this.analytics = new AnalyticsTracker();
|
|
1283
1338
|
this.webhook = new WebhookNotifier(this.config);
|
|
1284
1339
|
this.queue = new QueueManager(
|
|
1285
|
-
path.join(this.config.outputDir, this.config.queueFileName)
|
|
1340
|
+
path.join(this.config.outputDir, this.config.queueFileName),
|
|
1286
1341
|
);
|
|
1287
1342
|
this.publisher = new PlatformPublisher(this.config);
|
|
1288
1343
|
this.cronHelper = new CronHelper(this.config);
|
|
@@ -1293,7 +1348,7 @@ class ContentCoordinator {
|
|
|
1293
1348
|
onPhaseChange: null,
|
|
1294
1349
|
onItemComplete: null,
|
|
1295
1350
|
onError: null,
|
|
1296
|
-
onConfirmationNeeded: null
|
|
1351
|
+
onConfirmationNeeded: null,
|
|
1297
1352
|
};
|
|
1298
1353
|
}
|
|
1299
1354
|
|
|
@@ -1311,12 +1366,12 @@ class ContentCoordinator {
|
|
|
1311
1366
|
ready: [],
|
|
1312
1367
|
needsReview: [],
|
|
1313
1368
|
failed: [],
|
|
1314
|
-
published: []
|
|
1369
|
+
published: [],
|
|
1315
1370
|
},
|
|
1316
1371
|
reports: {
|
|
1317
1372
|
readyForPublishing: null,
|
|
1318
1373
|
needsReview: null,
|
|
1319
|
-
analytics: null
|
|
1374
|
+
analytics: null,
|
|
1320
1375
|
},
|
|
1321
1376
|
stats: {
|
|
1322
1377
|
totalItems: 0,
|
|
@@ -1324,11 +1379,11 @@ class ContentCoordinator {
|
|
|
1324
1379
|
ready: 0,
|
|
1325
1380
|
needsReview: 0,
|
|
1326
1381
|
published: 0,
|
|
1327
|
-
failed: 0
|
|
1382
|
+
failed: 0,
|
|
1328
1383
|
},
|
|
1329
1384
|
errors: [],
|
|
1330
1385
|
checkpoints: [],
|
|
1331
|
-
contentRulesLoaded: false
|
|
1386
|
+
contentRulesLoaded: false,
|
|
1332
1387
|
};
|
|
1333
1388
|
}
|
|
1334
1389
|
|
|
@@ -1345,15 +1400,18 @@ class ContentCoordinator {
|
|
|
1345
1400
|
* @returns {Promise<boolean>} Whether state was loaded
|
|
1346
1401
|
*/
|
|
1347
1402
|
async loadCheckpoint() {
|
|
1348
|
-
const statePath = path.join(
|
|
1403
|
+
const statePath = path.join(
|
|
1404
|
+
this.config.outputDir,
|
|
1405
|
+
this.config.stateFileName,
|
|
1406
|
+
);
|
|
1349
1407
|
try {
|
|
1350
|
-
const data = await fs.readFile(statePath,
|
|
1408
|
+
const data = await fs.readFile(statePath, "utf8");
|
|
1351
1409
|
this.state = JSON.parse(data);
|
|
1352
|
-
this.log(
|
|
1410
|
+
this.log("Loaded checkpoint from", statePath);
|
|
1353
1411
|
return true;
|
|
1354
1412
|
} catch (err) {
|
|
1355
|
-
if (err.code !==
|
|
1356
|
-
this.logError(
|
|
1413
|
+
if (err.code !== "ENOENT") {
|
|
1414
|
+
this.logError("Error loading checkpoint:", err.message);
|
|
1357
1415
|
}
|
|
1358
1416
|
return false;
|
|
1359
1417
|
}
|
|
@@ -1364,19 +1422,22 @@ class ContentCoordinator {
|
|
|
1364
1422
|
* @returns {Promise<void>}
|
|
1365
1423
|
*/
|
|
1366
1424
|
async saveCheckpoint() {
|
|
1367
|
-
const statePath = path.join(
|
|
1425
|
+
const statePath = path.join(
|
|
1426
|
+
this.config.outputDir,
|
|
1427
|
+
this.config.stateFileName,
|
|
1428
|
+
);
|
|
1368
1429
|
this.state.lastUpdated = new Date().toISOString();
|
|
1369
1430
|
this.state.checkpoints.push({
|
|
1370
1431
|
phase: this.state.phase,
|
|
1371
1432
|
timestamp: this.state.lastUpdated,
|
|
1372
|
-
stats: { ...this.state.stats }
|
|
1433
|
+
stats: { ...this.state.stats },
|
|
1373
1434
|
});
|
|
1374
1435
|
|
|
1375
1436
|
try {
|
|
1376
1437
|
await fs.writeFile(statePath, JSON.stringify(this.state, null, 2));
|
|
1377
|
-
this.log(
|
|
1438
|
+
this.log("Saved checkpoint to", statePath);
|
|
1378
1439
|
} catch (err) {
|
|
1379
|
-
this.logError(
|
|
1440
|
+
this.logError("Error saving checkpoint:", err.message);
|
|
1380
1441
|
}
|
|
1381
1442
|
}
|
|
1382
1443
|
|
|
@@ -1385,13 +1446,16 @@ class ContentCoordinator {
|
|
|
1385
1446
|
* @returns {Promise<void>}
|
|
1386
1447
|
*/
|
|
1387
1448
|
async clearCheckpoint() {
|
|
1388
|
-
const statePath = path.join(
|
|
1449
|
+
const statePath = path.join(
|
|
1450
|
+
this.config.outputDir,
|
|
1451
|
+
this.config.stateFileName,
|
|
1452
|
+
);
|
|
1389
1453
|
try {
|
|
1390
1454
|
await fs.unlink(statePath);
|
|
1391
|
-
this.log(
|
|
1455
|
+
this.log("Cleared checkpoint file");
|
|
1392
1456
|
} catch (err) {
|
|
1393
|
-
if (err.code !==
|
|
1394
|
-
this.logError(
|
|
1457
|
+
if (err.code !== "ENOENT") {
|
|
1458
|
+
this.logError("Error clearing checkpoint:", err.message);
|
|
1395
1459
|
}
|
|
1396
1460
|
}
|
|
1397
1461
|
}
|
|
@@ -1404,22 +1468,22 @@ class ContentCoordinator {
|
|
|
1404
1468
|
// Try default locations if not specified
|
|
1405
1469
|
const possiblePaths = [
|
|
1406
1470
|
this.config.contentRulesPath,
|
|
1407
|
-
path.join(this.workDir,
|
|
1408
|
-
path.join(this.workDir,
|
|
1409
|
-
path.join(process.cwd(),
|
|
1471
|
+
path.join(this.workDir, "content-rules.md"),
|
|
1472
|
+
path.join(this.workDir, "..", "content-rules.md"),
|
|
1473
|
+
path.join(process.cwd(), "content-rules.md"),
|
|
1410
1474
|
].filter(Boolean);
|
|
1411
1475
|
|
|
1412
1476
|
for (const rulesPath of possiblePaths) {
|
|
1413
1477
|
this.contentRules = new ContentRulesManager(rulesPath);
|
|
1414
1478
|
const rules = await this.contentRules.load();
|
|
1415
1479
|
if (rules) {
|
|
1416
|
-
this.log(
|
|
1480
|
+
this.log("Loaded content rules from", rulesPath);
|
|
1417
1481
|
this.state.contentRulesLoaded = true;
|
|
1418
1482
|
return;
|
|
1419
1483
|
}
|
|
1420
1484
|
}
|
|
1421
1485
|
|
|
1422
|
-
this.log(
|
|
1486
|
+
this.log("No content-rules.md found, proceeding without brand voice rules");
|
|
1423
1487
|
}
|
|
1424
1488
|
|
|
1425
1489
|
/**
|
|
@@ -1428,17 +1492,17 @@ class ContentCoordinator {
|
|
|
1428
1492
|
* @returns {Promise<Object>} Parsed content item
|
|
1429
1493
|
*/
|
|
1430
1494
|
async parseContentFile(filePath) {
|
|
1431
|
-
const content = await fs.readFile(filePath,
|
|
1495
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
1432
1496
|
const item = {
|
|
1433
1497
|
id: path.basename(filePath, path.extname(filePath)),
|
|
1434
1498
|
filePath,
|
|
1435
1499
|
status: ITEM_STATUS.PENDING,
|
|
1436
1500
|
metadata: {},
|
|
1437
|
-
content:
|
|
1501
|
+
content: "",
|
|
1438
1502
|
verification: null,
|
|
1439
1503
|
publishedUrl: null,
|
|
1440
1504
|
errors: [],
|
|
1441
|
-
timing: {}
|
|
1505
|
+
timing: {},
|
|
1442
1506
|
};
|
|
1443
1507
|
|
|
1444
1508
|
// Parse YAML front matter
|
|
@@ -1449,14 +1513,17 @@ class ContentCoordinator {
|
|
|
1449
1513
|
item.content = frontMatterMatch[2].trim();
|
|
1450
1514
|
|
|
1451
1515
|
// Parse front matter fields
|
|
1452
|
-
const lines = frontMatter.split(
|
|
1516
|
+
const lines = frontMatter.split("\n");
|
|
1453
1517
|
let currentKey = null;
|
|
1454
1518
|
let arrayMode = false;
|
|
1455
1519
|
|
|
1456
1520
|
for (const line of lines) {
|
|
1457
1521
|
// Check for array continuation
|
|
1458
1522
|
if (arrayMode && line.match(/^\s+-\s+/)) {
|
|
1459
|
-
const value = line
|
|
1523
|
+
const value = line
|
|
1524
|
+
.replace(/^\s+-\s+/, "")
|
|
1525
|
+
.trim()
|
|
1526
|
+
.replace(/^["']|["']$/g, "");
|
|
1460
1527
|
if (!Array.isArray(item.metadata[currentKey])) {
|
|
1461
1528
|
item.metadata[currentKey] = [];
|
|
1462
1529
|
}
|
|
@@ -1465,13 +1532,17 @@ class ContentCoordinator {
|
|
|
1465
1532
|
}
|
|
1466
1533
|
|
|
1467
1534
|
arrayMode = false;
|
|
1468
|
-
const colonIndex = line.indexOf(
|
|
1535
|
+
const colonIndex = line.indexOf(":");
|
|
1469
1536
|
if (colonIndex > 0) {
|
|
1470
|
-
const key = line
|
|
1537
|
+
const key = line
|
|
1538
|
+
.slice(0, colonIndex)
|
|
1539
|
+
.trim()
|
|
1540
|
+
.toLowerCase()
|
|
1541
|
+
.replace(/\s+/g, "_");
|
|
1471
1542
|
let value = line.slice(colonIndex + 1).trim();
|
|
1472
1543
|
|
|
1473
1544
|
// Check if this starts an array
|
|
1474
|
-
if (value ===
|
|
1545
|
+
if (value === "" || value === "[]") {
|
|
1475
1546
|
currentKey = key;
|
|
1476
1547
|
arrayMode = true;
|
|
1477
1548
|
item.metadata[key] = [];
|
|
@@ -1479,8 +1550,10 @@ class ContentCoordinator {
|
|
|
1479
1550
|
}
|
|
1480
1551
|
|
|
1481
1552
|
// Remove quotes if present
|
|
1482
|
-
if (
|
|
1483
|
-
|
|
1553
|
+
if (
|
|
1554
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
1555
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
1556
|
+
) {
|
|
1484
1557
|
value = value.slice(1, -1);
|
|
1485
1558
|
}
|
|
1486
1559
|
|
|
@@ -1500,31 +1573,86 @@ class ContentCoordinator {
|
|
|
1500
1573
|
}
|
|
1501
1574
|
|
|
1502
1575
|
// Set defaults for required fields
|
|
1503
|
-
item.metadata.title =
|
|
1504
|
-
|
|
1505
|
-
item.metadata.
|
|
1506
|
-
item.metadata.
|
|
1576
|
+
item.metadata.title =
|
|
1577
|
+
item.metadata.title || path.basename(filePath, path.extname(filePath));
|
|
1578
|
+
item.metadata.status = item.metadata.status || "pending";
|
|
1579
|
+
item.metadata.target_platform =
|
|
1580
|
+
item.metadata.target_platform ||
|
|
1581
|
+
item.metadata.publish ||
|
|
1582
|
+
this.config.defaultPlatform;
|
|
1583
|
+
item.metadata.priority = item.metadata.priority || "normal";
|
|
1584
|
+
item.metadata.mode = item.metadata.mode || "new";
|
|
1585
|
+
|
|
1586
|
+
// Extract job directory context from file path (e.g. content-queue/job20260223_01/)
|
|
1587
|
+
const parentDir = path.basename(path.dirname(filePath));
|
|
1588
|
+
if (/^job\d{8}_\d{2}$/.test(parentDir) || /^job\d{8,14}$/.test(parentDir)) {
|
|
1589
|
+
item.metadata.jobDir = parentDir;
|
|
1590
|
+
}
|
|
1507
1591
|
|
|
1508
1592
|
return item;
|
|
1509
1593
|
}
|
|
1510
1594
|
|
|
1511
1595
|
/**
|
|
1512
|
-
* Discover content files in work directory
|
|
1596
|
+
* Discover content files recursively in work directory and job subdirectories
|
|
1513
1597
|
* @returns {Promise<string[]>} Array of file paths
|
|
1514
1598
|
*/
|
|
1515
1599
|
async discoverContentFiles() {
|
|
1516
1600
|
const files = [];
|
|
1517
|
-
|
|
1601
|
+
await this._scanDirectory(this.workDir, files);
|
|
1602
|
+
return files.sort((a, b) => {
|
|
1603
|
+
const dirA = path.dirname(a);
|
|
1604
|
+
const dirB = path.dirname(b);
|
|
1605
|
+
if (dirA !== dirB) return dirA.localeCompare(dirB);
|
|
1606
|
+
return a.localeCompare(b);
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
/**
|
|
1611
|
+
* Recursively scan a directory for markdown content files
|
|
1612
|
+
* @param {string} dir - Directory to scan
|
|
1613
|
+
* @param {string[]} files - Accumulator array
|
|
1614
|
+
* @returns {Promise<void>}
|
|
1615
|
+
*/
|
|
1616
|
+
async _scanDirectory(dir, files) {
|
|
1617
|
+
let entries;
|
|
1618
|
+
try {
|
|
1619
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
1620
|
+
} catch (err) {
|
|
1621
|
+
if (err.code === "ENOENT") return;
|
|
1622
|
+
throw err;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
const EXCLUDED_DIRS = new Set([
|
|
1626
|
+
"node_modules",
|
|
1627
|
+
".git",
|
|
1628
|
+
".claude",
|
|
1629
|
+
".gemini",
|
|
1630
|
+
".codex",
|
|
1631
|
+
".agents",
|
|
1632
|
+
"dist",
|
|
1633
|
+
"build",
|
|
1634
|
+
"coverage",
|
|
1635
|
+
"vendor",
|
|
1636
|
+
"skills",
|
|
1637
|
+
"agents",
|
|
1638
|
+
".myaidev-method",
|
|
1639
|
+
".content-session",
|
|
1640
|
+
"platforms",
|
|
1641
|
+
]);
|
|
1518
1642
|
|
|
1519
1643
|
for (const entry of entries) {
|
|
1520
|
-
if (entry.
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1644
|
+
if (entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
|
|
1645
|
+
|
|
1646
|
+
const fullPath = path.join(dir, entry.name);
|
|
1647
|
+
|
|
1648
|
+
if (entry.isDirectory()) {
|
|
1649
|
+
if (!EXCLUDED_DIRS.has(entry.name)) {
|
|
1650
|
+
await this._scanDirectory(fullPath, files);
|
|
1651
|
+
}
|
|
1652
|
+
} else if (entry.isFile() && /\.(md|markdown)$/i.test(entry.name)) {
|
|
1653
|
+
files.push(fullPath);
|
|
1524
1654
|
}
|
|
1525
1655
|
}
|
|
1526
|
-
|
|
1527
|
-
return files.sort();
|
|
1528
1656
|
}
|
|
1529
1657
|
|
|
1530
1658
|
/**
|
|
@@ -1558,7 +1686,7 @@ class ContentCoordinator {
|
|
|
1558
1686
|
status: ITEM_STATUS.FAILED,
|
|
1559
1687
|
metadata: { title: path.basename(filePath) },
|
|
1560
1688
|
errors: [err.message],
|
|
1561
|
-
timing: {}
|
|
1689
|
+
timing: {},
|
|
1562
1690
|
});
|
|
1563
1691
|
}
|
|
1564
1692
|
}
|
|
@@ -1571,7 +1699,7 @@ class ContentCoordinator {
|
|
|
1571
1699
|
await this.webhook.notifyStart({
|
|
1572
1700
|
totalItems: this.state.stats.totalItems,
|
|
1573
1701
|
workDir: this.workDir,
|
|
1574
|
-
contentRulesLoaded: this.state.contentRulesLoaded
|
|
1702
|
+
contentRulesLoaded: this.state.contentRulesLoaded,
|
|
1575
1703
|
});
|
|
1576
1704
|
|
|
1577
1705
|
await this.saveCheckpoint();
|
|
@@ -1587,10 +1715,12 @@ class ContentCoordinator {
|
|
|
1587
1715
|
this.analytics.startPhase(PHASES.VERIFY);
|
|
1588
1716
|
|
|
1589
1717
|
const pendingItems = this.state.items.filter(
|
|
1590
|
-
item => item.status === ITEM_STATUS.PENDING
|
|
1718
|
+
(item) => item.status === ITEM_STATUS.PENDING,
|
|
1591
1719
|
);
|
|
1592
1720
|
|
|
1593
|
-
this.log(
|
|
1721
|
+
this.log(
|
|
1722
|
+
`Verifying ${pendingItems.length} items with concurrency ${this.config.concurrency}`,
|
|
1723
|
+
);
|
|
1594
1724
|
|
|
1595
1725
|
// Get content rules context for verification
|
|
1596
1726
|
const rulesContext = this.contentRules.getVerificationContext();
|
|
@@ -1599,37 +1729,41 @@ class ContentCoordinator {
|
|
|
1599
1729
|
for (let i = 0; i < pendingItems.length; i += this.config.concurrency) {
|
|
1600
1730
|
const batch = pendingItems.slice(i, i + this.config.concurrency);
|
|
1601
1731
|
|
|
1602
|
-
await Promise.all(
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
item
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1732
|
+
await Promise.all(
|
|
1733
|
+
batch.map(async (item) => {
|
|
1734
|
+
item.status = ITEM_STATUS.VERIFYING;
|
|
1735
|
+
const startTime = Date.now();
|
|
1736
|
+
|
|
1737
|
+
try {
|
|
1738
|
+
// Pass content rules context to verifier
|
|
1739
|
+
const result = await this.withRetry(() =>
|
|
1740
|
+
verifyFn(item, rulesContext),
|
|
1741
|
+
);
|
|
1742
|
+
const duration = Date.now() - startTime;
|
|
1743
|
+
|
|
1744
|
+
item.verification = result;
|
|
1745
|
+
item.status = ITEM_STATUS.VERIFIED;
|
|
1746
|
+
item.timing.verification = duration;
|
|
1747
|
+
this.state.stats.verified++;
|
|
1748
|
+
|
|
1749
|
+
this.analytics.trackVerification(item, duration);
|
|
1750
|
+
this.notifyItemComplete(item, "verified");
|
|
1751
|
+
} catch (err) {
|
|
1752
|
+
item.status = ITEM_STATUS.FAILED;
|
|
1753
|
+
item.errors.push(err.message);
|
|
1754
|
+
item.timing.verification = Date.now() - startTime;
|
|
1755
|
+
this.state.stats.failed++;
|
|
1756
|
+
|
|
1757
|
+
this.analytics.trackError(item, err, PHASES.VERIFY);
|
|
1758
|
+
this.notifyError(item, err);
|
|
1759
|
+
await this.webhook.notifyError({
|
|
1760
|
+
item: item.metadata.title,
|
|
1761
|
+
error: err.message,
|
|
1762
|
+
phase: PHASES.VERIFY,
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
}),
|
|
1766
|
+
);
|
|
1633
1767
|
|
|
1634
1768
|
await this.saveCheckpoint();
|
|
1635
1769
|
}
|
|
@@ -1656,12 +1790,15 @@ class ContentCoordinator {
|
|
|
1656
1790
|
}
|
|
1657
1791
|
|
|
1658
1792
|
const verification = item.verification || {};
|
|
1659
|
-
const score = (verification.redundancyScore ||
|
|
1660
|
-
const recommendation = (verification.recommendation ||
|
|
1793
|
+
const score = (verification.redundancyScore || "").toLowerCase();
|
|
1794
|
+
const recommendation = (verification.recommendation || "").toLowerCase();
|
|
1661
1795
|
|
|
1662
1796
|
// Check if ready for publishing
|
|
1663
|
-
const isLowRedundancy = [
|
|
1664
|
-
|
|
1797
|
+
const isLowRedundancy = [
|
|
1798
|
+
REDUNDANCY_THRESHOLDS.MINIMAL,
|
|
1799
|
+
REDUNDANCY_THRESHOLDS.LOW,
|
|
1800
|
+
].includes(score);
|
|
1801
|
+
const isProceedRecommended = recommendation.includes("proceed");
|
|
1665
1802
|
|
|
1666
1803
|
if (isLowRedundancy && isProceedRecommended) {
|
|
1667
1804
|
item.status = ITEM_STATUS.READY;
|
|
@@ -1693,23 +1830,26 @@ class ContentCoordinator {
|
|
|
1693
1830
|
const readyReport = this.generateReadyReport(timestamp);
|
|
1694
1831
|
const readyPath = path.join(
|
|
1695
1832
|
this.config.outputDir,
|
|
1696
|
-
`ready-for-publishing-${timestamp}.md
|
|
1833
|
+
`ready-for-publishing-${timestamp}.md`,
|
|
1697
1834
|
);
|
|
1698
1835
|
await fs.writeFile(readyPath, readyReport);
|
|
1699
1836
|
this.state.reports.readyForPublishing = readyPath;
|
|
1700
|
-
this.log(
|
|
1837
|
+
this.log("Generated report:", readyPath);
|
|
1701
1838
|
}
|
|
1702
1839
|
|
|
1703
1840
|
// Generate Needs Review report
|
|
1704
|
-
if (
|
|
1841
|
+
if (
|
|
1842
|
+
this.state.results.needsReview.length > 0 ||
|
|
1843
|
+
this.state.results.failed.length > 0
|
|
1844
|
+
) {
|
|
1705
1845
|
const reviewReport = this.generateNeedsReviewReport(timestamp);
|
|
1706
1846
|
const reviewPath = path.join(
|
|
1707
1847
|
this.config.outputDir,
|
|
1708
|
-
`needs-review-${timestamp}.md
|
|
1848
|
+
`needs-review-${timestamp}.md`,
|
|
1709
1849
|
);
|
|
1710
1850
|
await fs.writeFile(reviewPath, reviewReport);
|
|
1711
1851
|
this.state.reports.needsReview = reviewPath;
|
|
1712
|
-
this.log(
|
|
1852
|
+
this.log("Generated report:", reviewPath);
|
|
1713
1853
|
}
|
|
1714
1854
|
|
|
1715
1855
|
// Generate analytics report if enabled
|
|
@@ -1717,11 +1857,11 @@ class ContentCoordinator {
|
|
|
1717
1857
|
const analyticsReport = this.generateAnalyticsReport(timestamp);
|
|
1718
1858
|
const analyticsPath = path.join(
|
|
1719
1859
|
this.config.outputDir,
|
|
1720
|
-
`analytics-${timestamp}.md
|
|
1860
|
+
`analytics-${timestamp}.md`,
|
|
1721
1861
|
);
|
|
1722
1862
|
await fs.writeFile(analyticsPath, analyticsReport);
|
|
1723
1863
|
this.state.reports.analytics = analyticsPath;
|
|
1724
|
-
this.log(
|
|
1864
|
+
this.log("Generated analytics report:", analyticsPath);
|
|
1725
1865
|
}
|
|
1726
1866
|
|
|
1727
1867
|
this.analytics.endPhase(PHASES.REPORT);
|
|
@@ -1740,21 +1880,21 @@ class ContentCoordinator {
|
|
|
1740
1880
|
total: this.state.stats.totalItems,
|
|
1741
1881
|
ready: this.state.stats.ready,
|
|
1742
1882
|
needsReview: this.state.stats.needsReview,
|
|
1743
|
-
failed: this.state.stats.failed
|
|
1883
|
+
failed: this.state.stats.failed,
|
|
1744
1884
|
},
|
|
1745
1885
|
reports: this.state.reports,
|
|
1746
|
-
readyItems: this.state.results.ready.map(item => ({
|
|
1886
|
+
readyItems: this.state.results.ready.map((item) => ({
|
|
1747
1887
|
title: item.metadata.title,
|
|
1748
1888
|
score: item.verification?.redundancyScore,
|
|
1749
1889
|
recommendation: item.verification?.recommendation,
|
|
1750
|
-
platform: item.metadata.target_platform
|
|
1890
|
+
platform: item.metadata.target_platform,
|
|
1751
1891
|
})),
|
|
1752
|
-
needsReviewItems: this.state.results.needsReview.map(item => ({
|
|
1892
|
+
needsReviewItems: this.state.results.needsReview.map((item) => ({
|
|
1753
1893
|
title: item.metadata.title,
|
|
1754
1894
|
score: item.verification?.redundancyScore,
|
|
1755
|
-
reason: item.verification?.feedback ||
|
|
1895
|
+
reason: item.verification?.feedback || "Requires manual review",
|
|
1756
1896
|
})),
|
|
1757
|
-
contentRulesApplied: this.state.contentRulesLoaded
|
|
1897
|
+
contentRulesApplied: this.state.contentRulesLoaded,
|
|
1758
1898
|
};
|
|
1759
1899
|
}
|
|
1760
1900
|
|
|
@@ -1765,7 +1905,7 @@ class ContentCoordinator {
|
|
|
1765
1905
|
*/
|
|
1766
1906
|
async phasePublish(publishFn) {
|
|
1767
1907
|
if (this.config.dryRun) {
|
|
1768
|
-
this.log(
|
|
1908
|
+
this.log("Dry run mode - skipping publish phase");
|
|
1769
1909
|
this.setPhase(PHASES.COMPLETE);
|
|
1770
1910
|
return;
|
|
1771
1911
|
}
|
|
@@ -1774,99 +1914,110 @@ class ContentCoordinator {
|
|
|
1774
1914
|
this.analytics.startPhase(PHASES.PUBLISH);
|
|
1775
1915
|
|
|
1776
1916
|
const readyItems = this.state.results.ready.filter(
|
|
1777
|
-
item => item.status === ITEM_STATUS.READY
|
|
1917
|
+
(item) => item.status === ITEM_STATUS.READY,
|
|
1778
1918
|
);
|
|
1779
1919
|
|
|
1780
1920
|
// Get publishing context from content rules
|
|
1781
1921
|
const publishContext = this.contentRules.getPublishingContext();
|
|
1782
1922
|
|
|
1783
|
-
this.log(
|
|
1923
|
+
this.log(
|
|
1924
|
+
`Publishing ${readyItems.length} items with concurrency ${this.config.concurrency}`,
|
|
1925
|
+
);
|
|
1784
1926
|
|
|
1785
1927
|
// Process in batches based on concurrency
|
|
1786
1928
|
for (let i = 0; i < readyItems.length; i += this.config.concurrency) {
|
|
1787
1929
|
const batch = readyItems.slice(i, i + this.config.concurrency);
|
|
1788
1930
|
const batchStartIndex = i;
|
|
1789
1931
|
|
|
1790
|
-
await Promise.all(
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
item
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1932
|
+
await Promise.all(
|
|
1933
|
+
batch.map(async (item, batchIndex) => {
|
|
1934
|
+
item.status = ITEM_STATUS.PUBLISHING;
|
|
1935
|
+
const startTime = Date.now();
|
|
1936
|
+
const itemIndex = batchStartIndex + batchIndex;
|
|
1937
|
+
|
|
1938
|
+
try {
|
|
1939
|
+
// Get platform-specific publisher
|
|
1940
|
+
const platform =
|
|
1941
|
+
item.metadata.target_platform || this.config.defaultPlatform;
|
|
1942
|
+
const platformPublisher = this.publisher.getPublisher(platform);
|
|
1943
|
+
|
|
1944
|
+
// Get scheduling context for WordPress native scheduling
|
|
1945
|
+
const scheduleContext = this.scheduler.getSchedulingContext(
|
|
1946
|
+
item,
|
|
1947
|
+
itemIndex,
|
|
1948
|
+
readyItems.length,
|
|
1949
|
+
);
|
|
1950
|
+
|
|
1951
|
+
// Publish with platform context, content rules, and scheduling
|
|
1952
|
+
const result = await this.withRetry(() =>
|
|
1953
|
+
platformPublisher.publish(item, (item, platformContext) =>
|
|
1954
|
+
publishFn(item, {
|
|
1955
|
+
...platformContext,
|
|
1956
|
+
contentRules: publishContext,
|
|
1957
|
+
scheduling: scheduleContext,
|
|
1958
|
+
}),
|
|
1959
|
+
),
|
|
1960
|
+
);
|
|
1961
|
+
|
|
1962
|
+
const duration = Date.now() - startTime;
|
|
1963
|
+
|
|
1964
|
+
item.publishedUrl = result.url;
|
|
1965
|
+
item.publishedAt = new Date().toISOString();
|
|
1966
|
+
item.scheduledFor = scheduleContext.isScheduled
|
|
1967
|
+
? scheduleContext.publishDate
|
|
1968
|
+
: null;
|
|
1969
|
+
item.status = scheduleContext.isScheduled
|
|
1970
|
+
? ITEM_STATUS.SCHEDULED
|
|
1971
|
+
: ITEM_STATUS.PUBLISHED;
|
|
1972
|
+
item.timing.publishing = duration;
|
|
1973
|
+
this.state.results.published.push(item);
|
|
1974
|
+
this.state.stats.published++;
|
|
1975
|
+
|
|
1976
|
+
// Track in scheduler
|
|
1977
|
+
this.scheduler.trackScheduledPost(item, result, scheduleContext);
|
|
1978
|
+
|
|
1979
|
+
this.analytics.trackPublishing(item, duration);
|
|
1980
|
+
this.notifyItemComplete(
|
|
1981
|
+
item,
|
|
1982
|
+
scheduleContext.isScheduled ? "scheduled" : "published",
|
|
1983
|
+
);
|
|
1984
|
+
|
|
1985
|
+
// Update queue status
|
|
1986
|
+
await this.queue.updateItemStatus(item.id, "published", {
|
|
1987
|
+
publishedUrl: result.url,
|
|
1988
|
+
publishedAt: item.publishedAt,
|
|
1989
|
+
scheduledFor: item.scheduledFor,
|
|
1990
|
+
});
|
|
1991
|
+
|
|
1992
|
+
// Webhook notification
|
|
1993
|
+
await this.webhook.notifyPublished({
|
|
1994
|
+
title: item.metadata.title,
|
|
1995
|
+
url: result.url,
|
|
1996
|
+
platform,
|
|
1997
|
+
scheduled: scheduleContext.isScheduled,
|
|
1998
|
+
scheduledFor: scheduleContext.scheduledFor,
|
|
1999
|
+
});
|
|
2000
|
+
} catch (err) {
|
|
2001
|
+
item.status = ITEM_STATUS.FAILED;
|
|
2002
|
+
item.errors.push(err.message);
|
|
2003
|
+
item.timing.publishing = Date.now() - startTime;
|
|
2004
|
+
this.state.stats.failed++;
|
|
2005
|
+
|
|
2006
|
+
this.analytics.trackError(item, err, PHASES.PUBLISH);
|
|
2007
|
+
this.notifyError(item, err);
|
|
2008
|
+
|
|
2009
|
+
await this.queue.updateItemStatus(item.id, "failed", {
|
|
2010
|
+
error: err.message,
|
|
2011
|
+
});
|
|
2012
|
+
|
|
2013
|
+
await this.webhook.notifyError({
|
|
2014
|
+
item: item.metadata.title,
|
|
2015
|
+
error: err.message,
|
|
2016
|
+
phase: PHASES.PUBLISH,
|
|
2017
|
+
});
|
|
2018
|
+
}
|
|
2019
|
+
}),
|
|
2020
|
+
);
|
|
1870
2021
|
|
|
1871
2022
|
await this.saveCheckpoint();
|
|
1872
2023
|
}
|
|
@@ -1888,18 +2039,26 @@ class ContentCoordinator {
|
|
|
1888
2039
|
const lockAcquired = await this.cronHelper.acquireLock();
|
|
1889
2040
|
if (!lockAcquired) {
|
|
1890
2041
|
if (!this.config.quietMode) {
|
|
1891
|
-
console.log(
|
|
2042
|
+
console.log(
|
|
2043
|
+
"[ContentCoordinator] Another instance is running, exiting",
|
|
2044
|
+
);
|
|
1892
2045
|
}
|
|
1893
|
-
return { skipped: true, reason:
|
|
2046
|
+
return { skipped: true, reason: "lock_held" };
|
|
1894
2047
|
}
|
|
1895
2048
|
|
|
1896
2049
|
const intervalCheck = await this.cronHelper.checkRunInterval();
|
|
1897
2050
|
if (!intervalCheck.canRun) {
|
|
1898
2051
|
await this.cronHelper.releaseLock();
|
|
1899
2052
|
if (!this.config.quietMode) {
|
|
1900
|
-
console.log(
|
|
2053
|
+
console.log(
|
|
2054
|
+
`[ContentCoordinator] Minimum interval not met, next run at ${intervalCheck.nextRunTime}`,
|
|
2055
|
+
);
|
|
1901
2056
|
}
|
|
1902
|
-
return {
|
|
2057
|
+
return {
|
|
2058
|
+
skipped: true,
|
|
2059
|
+
reason: "interval_not_met",
|
|
2060
|
+
nextRunTime: intervalCheck.nextRunTime,
|
|
2061
|
+
};
|
|
1903
2062
|
}
|
|
1904
2063
|
}
|
|
1905
2064
|
|
|
@@ -1915,26 +2074,44 @@ class ContentCoordinator {
|
|
|
1915
2074
|
await this.phaseInitialize();
|
|
1916
2075
|
}
|
|
1917
2076
|
|
|
1918
|
-
if (
|
|
2077
|
+
if (
|
|
2078
|
+
this.state.phase === PHASES.INITIALIZE ||
|
|
2079
|
+
this.state.phase === PHASES.VERIFY
|
|
2080
|
+
) {
|
|
1919
2081
|
await this.phaseVerify(verify);
|
|
1920
2082
|
}
|
|
1921
2083
|
|
|
1922
|
-
if (
|
|
2084
|
+
if (
|
|
2085
|
+
[PHASES.INITIALIZE, PHASES.VERIFY, PHASES.CATEGORIZE].includes(
|
|
2086
|
+
this.state.phase,
|
|
2087
|
+
)
|
|
2088
|
+
) {
|
|
1923
2089
|
await this.phaseCategorize();
|
|
1924
2090
|
}
|
|
1925
2091
|
|
|
1926
|
-
if (
|
|
2092
|
+
if (
|
|
2093
|
+
[
|
|
2094
|
+
PHASES.INITIALIZE,
|
|
2095
|
+
PHASES.VERIFY,
|
|
2096
|
+
PHASES.CATEGORIZE,
|
|
2097
|
+
PHASES.REPORT,
|
|
2098
|
+
].includes(this.state.phase)
|
|
2099
|
+
) {
|
|
1927
2100
|
await this.phaseReport();
|
|
1928
2101
|
}
|
|
1929
2102
|
|
|
1930
2103
|
const notification = await this.phaseNotify();
|
|
1931
2104
|
|
|
1932
2105
|
// Request confirmation if not in force mode (skip in cron mode)
|
|
1933
|
-
if (
|
|
2106
|
+
if (
|
|
2107
|
+
!this.config.force &&
|
|
2108
|
+
!this.config.cronMode &&
|
|
2109
|
+
this.state.results.ready.length > 0
|
|
2110
|
+
) {
|
|
1934
2111
|
if (confirm) {
|
|
1935
2112
|
const confirmed = await confirm(notification);
|
|
1936
2113
|
if (!confirmed) {
|
|
1937
|
-
this.log(
|
|
2114
|
+
this.log("Publishing cancelled by user");
|
|
1938
2115
|
this.analytics.endWorkflow();
|
|
1939
2116
|
if (this.config.cronMode) {
|
|
1940
2117
|
await this.cronHelper.releaseLock();
|
|
@@ -1969,14 +2146,13 @@ class ContentCoordinator {
|
|
|
1969
2146
|
}
|
|
1970
2147
|
|
|
1971
2148
|
return results;
|
|
1972
|
-
|
|
1973
2149
|
} catch (err) {
|
|
1974
|
-
this.logError(
|
|
2150
|
+
this.logError("Workflow error:", err.message);
|
|
1975
2151
|
this.analytics.endWorkflow();
|
|
1976
2152
|
await this.saveCheckpoint();
|
|
1977
2153
|
await this.webhook.notifyError({
|
|
1978
2154
|
error: err.message,
|
|
1979
|
-
phase: this.state.phase
|
|
2155
|
+
phase: this.state.phase,
|
|
1980
2156
|
});
|
|
1981
2157
|
|
|
1982
2158
|
// Cron mode: release lock on error
|
|
@@ -1997,22 +2173,24 @@ class ContentCoordinator {
|
|
|
1997
2173
|
success: this.state.stats.failed === 0,
|
|
1998
2174
|
stats: this.state.stats,
|
|
1999
2175
|
reports: this.state.reports,
|
|
2000
|
-
published: this.state.results.published.map(item => ({
|
|
2176
|
+
published: this.state.results.published.map((item) => ({
|
|
2001
2177
|
title: item.metadata.title,
|
|
2002
2178
|
url: item.publishedUrl,
|
|
2003
|
-
platform: item.metadata.target_platform
|
|
2179
|
+
platform: item.metadata.target_platform,
|
|
2004
2180
|
})),
|
|
2005
|
-
needsReview: this.state.results.needsReview.map(item => ({
|
|
2181
|
+
needsReview: this.state.results.needsReview.map((item) => ({
|
|
2006
2182
|
title: item.metadata.title,
|
|
2007
|
-
reason: item.verification?.feedback ||
|
|
2183
|
+
reason: item.verification?.feedback || "Requires review",
|
|
2008
2184
|
})),
|
|
2009
|
-
failed: this.state.results.failed.map(item => ({
|
|
2185
|
+
failed: this.state.results.failed.map((item) => ({
|
|
2010
2186
|
title: item.metadata.title,
|
|
2011
|
-
errors: item.errors
|
|
2187
|
+
errors: item.errors,
|
|
2012
2188
|
})),
|
|
2013
2189
|
duration: this.calculateDuration(),
|
|
2014
|
-
analytics: this.config.enableAnalytics
|
|
2015
|
-
|
|
2190
|
+
analytics: this.config.enableAnalytics
|
|
2191
|
+
? this.analytics.getSummary()
|
|
2192
|
+
: null,
|
|
2193
|
+
contentRulesApplied: this.state.contentRulesLoaded,
|
|
2016
2194
|
};
|
|
2017
2195
|
}
|
|
2018
2196
|
|
|
@@ -2034,7 +2212,7 @@ Generated: ${this.formatTimestamp(timestamp)}
|
|
|
2034
2212
|
- Total Items: ${items.length}
|
|
2035
2213
|
- Total Word Count: ~${Math.round(totalWords / 1000)}K words
|
|
2036
2214
|
- Average Redundancy Score: ${this.calculateAverageScore(items)}
|
|
2037
|
-
- Content Rules Applied: ${this.state.contentRulesLoaded ?
|
|
2215
|
+
- Content Rules Applied: ${this.state.contentRulesLoaded ? "Yes" : "No"}
|
|
2038
2216
|
|
|
2039
2217
|
## Items
|
|
2040
2218
|
|
|
@@ -2044,12 +2222,12 @@ Generated: ${this.formatTimestamp(timestamp)}
|
|
|
2044
2222
|
const v = item.verification || {};
|
|
2045
2223
|
report += `### ${index + 1}. ${item.metadata.title}
|
|
2046
2224
|
- **File**: ${path.basename(item.filePath)}
|
|
2047
|
-
- **Redundancy Score**: ${v.redundancyScore ||
|
|
2048
|
-
- **Recommendation**: ${v.recommendation ||
|
|
2225
|
+
- **Redundancy Score**: ${v.redundancyScore || "N/A"}
|
|
2226
|
+
- **Recommendation**: ${v.recommendation || "N/A"}
|
|
2049
2227
|
- **Word Count**: ~${item.content?.split(/\s+/).length || 0} words
|
|
2050
|
-
- **Verifier Feedback**: ${v.feedback ||
|
|
2051
|
-
- **Target Platform**: ${item.metadata.target_platform ||
|
|
2052
|
-
- **Priority**: ${item.metadata.priority ||
|
|
2228
|
+
- **Verifier Feedback**: ${v.feedback || "No feedback provided"}
|
|
2229
|
+
- **Target Platform**: ${item.metadata.target_platform || "wordpress"}
|
|
2230
|
+
- **Priority**: ${item.metadata.priority || "normal"}
|
|
2053
2231
|
|
|
2054
2232
|
`;
|
|
2055
2233
|
});
|
|
@@ -2084,10 +2262,10 @@ Generated: ${this.formatTimestamp(timestamp)}
|
|
|
2084
2262
|
const v = item.verification || {};
|
|
2085
2263
|
report += `### ${index + 1}. ${item.metadata.title}
|
|
2086
2264
|
- **File**: ${path.basename(item.filePath)}
|
|
2087
|
-
- **Redundancy Score**: ${v.redundancyScore ||
|
|
2088
|
-
- **Recommendation**: ${v.recommendation ||
|
|
2265
|
+
- **Redundancy Score**: ${v.redundancyScore || "N/A"}
|
|
2266
|
+
- **Recommendation**: ${v.recommendation || "N/A"}
|
|
2089
2267
|
- **Issue**: ${this.getReviewReason(item)}
|
|
2090
|
-
- **Verifier Feedback**: ${v.feedback ||
|
|
2268
|
+
- **Verifier Feedback**: ${v.feedback || "No feedback provided"}
|
|
2091
2269
|
- **Suggested Actions**:
|
|
2092
2270
|
- Review and revise proprietary content
|
|
2093
2271
|
- Ensure content adds unique value
|
|
@@ -2105,7 +2283,7 @@ Generated: ${this.formatTimestamp(timestamp)}
|
|
|
2105
2283
|
report += `### ${index + 1}. ${item.metadata.title}
|
|
2106
2284
|
- **File**: ${path.basename(item.filePath)}
|
|
2107
2285
|
- **Errors**:
|
|
2108
|
-
${item.errors.map(e => ` - ${e}`).join(
|
|
2286
|
+
${item.errors.map((e) => ` - ${e}`).join("\n")}
|
|
2109
2287
|
- **Suggested Actions**:
|
|
2110
2288
|
- Review file format and content
|
|
2111
2289
|
- Fix any parsing errors
|
|
@@ -2182,15 +2360,17 @@ Generated: ${this.formatTimestamp(timestamp)}
|
|
|
2182
2360
|
*/
|
|
2183
2361
|
getReviewReason(item) {
|
|
2184
2362
|
const v = item.verification || {};
|
|
2185
|
-
const score = (v.redundancyScore ||
|
|
2186
|
-
const rec = (v.recommendation ||
|
|
2363
|
+
const score = (v.redundancyScore || "").toLowerCase();
|
|
2364
|
+
const rec = (v.recommendation || "").toLowerCase();
|
|
2187
2365
|
|
|
2188
|
-
if (score ===
|
|
2189
|
-
|
|
2190
|
-
if (
|
|
2191
|
-
|
|
2366
|
+
if (score === "high")
|
|
2367
|
+
return "High redundancy - content may duplicate existing knowledge";
|
|
2368
|
+
if (score === "medium")
|
|
2369
|
+
return "Medium redundancy - consider adding more unique insights";
|
|
2370
|
+
if (rec.includes("reject")) return "Content rejected by verifier";
|
|
2371
|
+
if (rec.includes("review")) return "Manual review recommended";
|
|
2192
2372
|
|
|
2193
|
-
return
|
|
2373
|
+
return "Content requires attention before publishing";
|
|
2194
2374
|
}
|
|
2195
2375
|
|
|
2196
2376
|
/**
|
|
@@ -2204,20 +2384,20 @@ Generated: ${this.formatTimestamp(timestamp)}
|
|
|
2204
2384
|
let count = 0;
|
|
2205
2385
|
|
|
2206
2386
|
for (const item of items) {
|
|
2207
|
-
const score = (item.verification?.redundancyScore ||
|
|
2387
|
+
const score = (item.verification?.redundancyScore || "").toLowerCase();
|
|
2208
2388
|
if (scores[score]) {
|
|
2209
2389
|
total += scores[score];
|
|
2210
2390
|
count++;
|
|
2211
2391
|
}
|
|
2212
2392
|
}
|
|
2213
2393
|
|
|
2214
|
-
if (count === 0) return
|
|
2394
|
+
if (count === 0) return "N/A";
|
|
2215
2395
|
|
|
2216
2396
|
const avg = total / count;
|
|
2217
|
-
if (avg <= 1.5) return
|
|
2218
|
-
if (avg <= 2.5) return
|
|
2219
|
-
if (avg <= 3.5) return
|
|
2220
|
-
return
|
|
2397
|
+
if (avg <= 1.5) return "Minimal";
|
|
2398
|
+
if (avg <= 2.5) return "Low";
|
|
2399
|
+
if (avg <= 3.5) return "Medium";
|
|
2400
|
+
return "High";
|
|
2221
2401
|
}
|
|
2222
2402
|
|
|
2223
2403
|
/**
|
|
@@ -2234,7 +2414,10 @@ Generated: ${this.formatTimestamp(timestamp)}
|
|
|
2234
2414
|
} catch (err) {
|
|
2235
2415
|
lastError = err;
|
|
2236
2416
|
if (attempt < this.config.retryAttempts) {
|
|
2237
|
-
this.log(
|
|
2417
|
+
this.log(
|
|
2418
|
+
`Retry attempt ${attempt}/${this.config.retryAttempts} after error:`,
|
|
2419
|
+
err.message,
|
|
2420
|
+
);
|
|
2238
2421
|
await this.sleep(this.config.retryDelay * attempt);
|
|
2239
2422
|
}
|
|
2240
2423
|
}
|
|
@@ -2282,7 +2465,7 @@ Generated: ${this.formatTimestamp(timestamp)}
|
|
|
2282
2465
|
this.state.errors.push({
|
|
2283
2466
|
item: item.metadata.title,
|
|
2284
2467
|
error: error.message,
|
|
2285
|
-
timestamp: new Date().toISOString()
|
|
2468
|
+
timestamp: new Date().toISOString(),
|
|
2286
2469
|
});
|
|
2287
2470
|
|
|
2288
2471
|
if (this.callbacks.onError) {
|
|
@@ -2296,10 +2479,11 @@ Generated: ${this.formatTimestamp(timestamp)}
|
|
|
2296
2479
|
*/
|
|
2297
2480
|
getTimestamp() {
|
|
2298
2481
|
const now = new Date();
|
|
2299
|
-
return now
|
|
2300
|
-
.
|
|
2301
|
-
.replace(
|
|
2302
|
-
.replace(
|
|
2482
|
+
return now
|
|
2483
|
+
.toISOString()
|
|
2484
|
+
.replace(/T/, "-")
|
|
2485
|
+
.replace(/:/g, "-")
|
|
2486
|
+
.replace(/\..+/, "");
|
|
2303
2487
|
}
|
|
2304
2488
|
|
|
2305
2489
|
/**
|
|
@@ -2308,7 +2492,9 @@ Generated: ${this.formatTimestamp(timestamp)}
|
|
|
2308
2492
|
* @returns {string} Human-readable timestamp
|
|
2309
2493
|
*/
|
|
2310
2494
|
formatTimestamp(timestamp) {
|
|
2311
|
-
return timestamp
|
|
2495
|
+
return timestamp
|
|
2496
|
+
.replace(/-/g, (m, i) => (i > 9 ? ":" : "-"))
|
|
2497
|
+
.replace("-", " ");
|
|
2312
2498
|
}
|
|
2313
2499
|
|
|
2314
2500
|
/**
|
|
@@ -2316,7 +2502,7 @@ Generated: ${this.formatTimestamp(timestamp)}
|
|
|
2316
2502
|
* @returns {string} Duration string
|
|
2317
2503
|
*/
|
|
2318
2504
|
calculateDuration() {
|
|
2319
|
-
if (!this.state.startedAt) return
|
|
2505
|
+
if (!this.state.startedAt) return "N/A";
|
|
2320
2506
|
|
|
2321
2507
|
const start = new Date(this.state.startedAt);
|
|
2322
2508
|
const end = new Date();
|
|
@@ -2337,7 +2523,7 @@ Generated: ${this.formatTimestamp(timestamp)}
|
|
|
2337
2523
|
* @returns {Promise<void>}
|
|
2338
2524
|
*/
|
|
2339
2525
|
sleep(ms) {
|
|
2340
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
2526
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2341
2527
|
}
|
|
2342
2528
|
|
|
2343
2529
|
/**
|
|
@@ -2346,7 +2532,7 @@ Generated: ${this.formatTimestamp(timestamp)}
|
|
|
2346
2532
|
*/
|
|
2347
2533
|
log(...args) {
|
|
2348
2534
|
if (this.config.verbose) {
|
|
2349
|
-
console.log(
|
|
2535
|
+
console.log("[ContentCoordinator]", ...args);
|
|
2350
2536
|
}
|
|
2351
2537
|
}
|
|
2352
2538
|
|
|
@@ -2355,7 +2541,7 @@ Generated: ${this.formatTimestamp(timestamp)}
|
|
|
2355
2541
|
* @param {...*} args - Error arguments
|
|
2356
2542
|
*/
|
|
2357
2543
|
logError(...args) {
|
|
2358
|
-
console.error(
|
|
2544
|
+
console.error("[ContentCoordinator Error]", ...args);
|
|
2359
2545
|
}
|
|
2360
2546
|
|
|
2361
2547
|
// ============================================
|
|
@@ -2416,7 +2602,7 @@ Generated: ${this.formatTimestamp(timestamp)}
|
|
|
2416
2602
|
generateCrontabEntry(options = {}) {
|
|
2417
2603
|
return this.cronHelper.generateCrontabEntry({
|
|
2418
2604
|
workDir: this.workDir || process.cwd(),
|
|
2419
|
-
...options
|
|
2605
|
+
...options,
|
|
2420
2606
|
});
|
|
2421
2607
|
}
|
|
2422
2608
|
|
|
@@ -2506,10 +2692,14 @@ ${entry}
|
|
|
2506
2692
|
|
|
2507
2693
|
## Suggested Schedules
|
|
2508
2694
|
|
|
2509
|
-
${schedules
|
|
2695
|
+
${schedules
|
|
2696
|
+
.map(
|
|
2697
|
+
(s) => `### ${s.name}
|
|
2510
2698
|
# ${s.description}
|
|
2511
2699
|
# Schedule: ${s.schedule}
|
|
2512
|
-
|
|
2700
|
+
`,
|
|
2701
|
+
)
|
|
2702
|
+
.join("\n")}
|
|
2513
2703
|
|
|
2514
2704
|
## Environment Variables
|
|
2515
2705
|
|
|
@@ -2520,9 +2710,9 @@ Make sure these are set in your crontab or script:
|
|
|
2520
2710
|
|
|
2521
2711
|
## Log Rotation (optional)
|
|
2522
2712
|
|
|
2523
|
-
Add to /etc/logrotate.d/content-coordinator:
|
|
2713
|
+
Add to /etc/logrotate.d/content-production-coordinator:
|
|
2524
2714
|
\`\`\`
|
|
2525
|
-
/var/log/content-coordinator.log {
|
|
2715
|
+
/var/log/content-production-coordinator.log {
|
|
2526
2716
|
weekly
|
|
2527
2717
|
rotate 4
|
|
2528
2718
|
compress
|
|
@@ -2537,7 +2727,7 @@ After adding, verify with:
|
|
|
2537
2727
|
crontab -l
|
|
2538
2728
|
|
|
2539
2729
|
Check logs at:
|
|
2540
|
-
tail -f /var/log/content-coordinator.log
|
|
2730
|
+
tail -f /var/log/content-production-coordinator.log
|
|
2541
2731
|
`;
|
|
2542
2732
|
}
|
|
2543
2733
|
}
|
|
@@ -2556,7 +2746,7 @@ export {
|
|
|
2556
2746
|
ITEM_STATUS,
|
|
2557
2747
|
REDUNDANCY_THRESHOLDS,
|
|
2558
2748
|
PLATFORMS,
|
|
2559
|
-
DEFAULT_CONFIG
|
|
2749
|
+
DEFAULT_CONFIG,
|
|
2560
2750
|
};
|
|
2561
2751
|
|
|
2562
2752
|
// Default export for convenience
|