codymaster 5.2.0 → 7.0.0
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/CHANGELOG.md +276 -0
- package/README.md +216 -333
- package/dist/agent/antigravity.js +152 -0
- package/dist/agent/backend.js +2 -0
- package/dist/agent/claude.js +196 -0
- package/dist/agent/codex.js +204 -0
- package/dist/agent/copilot.js +284 -0
- package/dist/agent/cursor.js +211 -0
- package/dist/agent/factory.js +30 -0
- package/dist/agent/gemini.js +142 -0
- package/dist/agent/opencode.js +205 -0
- package/dist/agent/spawn-helper.js +237 -0
- package/dist/agent/version.js +25 -0
- package/dist/browse/adapter-factory.js +69 -0
- package/dist/browse/adapters/agent-browser-adapter.js +305 -0
- package/dist/browse/adapters/playwright-adapter.js +309 -0
- package/dist/browse/adapters/types.js +6 -0
- package/dist/browse/error-collector.js +132 -0
- package/dist/browse/event-log.js +109 -0
- package/dist/browse/index.js +17 -0
- package/dist/browse-server.js +204 -120
- package/dist/cli/command-registry.js +12 -0
- package/dist/cli/commands/dashboard.js +76 -2
- package/dist/cli/commands/engineering.js +218 -4
- package/dist/cli/commands/install.js +160 -0
- package/dist/cli/commands/learn.js +181 -0
- package/dist/cli/commands/parallel.js +138 -0
- package/dist/cli/commands/quality.js +105 -0
- package/dist/cli/commands/stack.js +49 -0
- package/dist/cli/commands/update.js +159 -0
- package/dist/cli/update-check.js +94 -10
- package/dist/continuity.js +3 -1
- package/dist/dashboard.js +47 -6
- package/dist/data.js +35 -0
- package/dist/execution/tdd-gate.js +113 -0
- package/dist/executor/cancel.js +34 -0
- package/dist/executor/gc.js +74 -0
- package/dist/executor/index.js +14 -0
- package/dist/executor/runner.js +70 -0
- package/dist/executor/workdir.js +31 -0
- package/dist/handoff/contracts.js +22 -0
- package/dist/handoff/index.js +18 -0
- package/dist/handoff/io.js +121 -0
- package/dist/index.js +7 -3
- package/dist/indexer/stack-detect.js +219 -0
- package/dist/install/copy.js +98 -0
- package/dist/install/engine.js +42 -0
- package/dist/install/paths.js +70 -0
- package/dist/install/platforms/_simple.js +85 -0
- package/dist/install/platforms/antigravity.js +91 -0
- package/dist/install/platforms/claude-code.js +107 -0
- package/dist/install/platforms/cursor.js +77 -0
- package/dist/install/platforms/index.js +27 -0
- package/dist/install/platforms/simple.js +163 -0
- package/dist/install/profiles.js +75 -0
- package/dist/install/types.js +2 -0
- package/dist/learnings.js +208 -0
- package/dist/middleware/metrics.js +30 -0
- package/dist/middleware/security-headers.js +14 -0
- package/dist/realtime/event-bus.js +29 -0
- package/dist/realtime/ws-hub.js +91 -0
- package/dist/schemas/task-schema.js +48 -0
- package/dist/schemas/validate.js +18 -0
- package/dist/skills-lock.js +96 -0
- package/dist/sprint-pipeline.js +26 -0
- package/dist/storage/index.js +21 -0
- package/dist/storage/repos/activity-repo.js +46 -0
- package/dist/storage/repos/message-repo.js +39 -0
- package/dist/storage/repos/project-repo.js +56 -0
- package/dist/storage/repos/task-repo.js +142 -0
- package/dist/storage/services/project-service.js +49 -0
- package/dist/storage/services/task-service.js +97 -0
- package/dist/storage/sqlite.js +113 -0
- package/dist/tier-classify.js +131 -0
- package/dist/ui/onboarding.js +51 -15
- package/dist/utils/cli-utils.js +7 -2
- package/dist/utils/design-taste.js +108 -0
- package/dist/utils/output-compress.js +143 -0
- package/dist/vibecoding-index.js +126 -0
- package/package.json +19 -4
- package/public/dashboard/app.js +52 -1
- package/scripts/build-skills-lock.mjs +88 -0
- package/scripts/build-skills.mjs +187 -28
- package/scripts/compress-skill.mjs +73 -0
- package/scripts/deprecate-skill.mjs +72 -0
- package/scripts/install.sh +170 -0
- package/scripts/mcp-bridge.js +2 -2
- package/scripts/postinstall.js +54 -287
- package/scripts/update-changelog.sh +88 -0
- package/scripts/validate-skills.mjs +101 -4
- package/skills/_shared/SKILL_TEMPLATE.md +62 -0
- package/skills/cm-autopilot/scripts/autopilot.py +19 -2
- package/skills/cm-brainstorm-idea/SKILL.md +9 -0
- package/skills/cm-clean-code/SKILL.md +20 -0
- package/skills/cm-code-review/SKILL.md +21 -0
- package/skills/cm-codeintell/SKILL.md +9 -0
- package/skills/cm-conductor-worktrees/SKILL.archive.md +28 -0
- package/skills/cm-conductor-worktrees/SKILL.md +17 -19
- package/skills/cm-continuity/SKILL.md +9 -0
- package/skills/cm-dashboard/SKILL.archive.md +15 -0
- package/skills/cm-dashboard/SKILL.md +20 -9
- package/skills/cm-dashboard/ui/app.js +9 -1
- package/skills/cm-debugging/SKILL.md +9 -0
- package/skills/cm-design-studio/SKILL.archive.md +34 -0
- package/skills/cm-design-studio/SKILL.md +17 -25
- package/skills/cm-design-system/SKILL.md +1 -0
- package/skills/cm-engineering-meta/SKILL.archive.md +73 -0
- package/skills/cm-engineering-meta/SKILL.md +16 -63
- package/skills/cm-execution/SKILL.md +98 -0
- package/skills/cm-git-worktrees/SKILL.archive.md +157 -0
- package/skills/cm-git-worktrees/SKILL.md +15 -146
- package/skills/cm-identity-guard/SKILL.md +8 -0
- package/skills/cm-planning/SKILL.md +63 -92
- package/skills/cm-post-deploy-canary/SKILL.archive.md +22 -0
- package/skills/cm-post-deploy-canary/SKILL.md +17 -13
- package/skills/cm-qa-visual-cli/SKILL.archive.md +22 -0
- package/skills/cm-qa-visual-cli/SKILL.md +16 -12
- package/skills/cm-quality-gate/SKILL.md +38 -0
- package/skills/cm-safe-deploy/SKILL.md +9 -0
- package/skills/cm-second-opinion-cli/SKILL.archive.md +23 -0
- package/skills/cm-second-opinion-cli/SKILL.md +17 -14
- package/skills/cm-secret-shield/SKILL.archive.md +580 -0
- package/skills/cm-secret-shield/SKILL.md +15 -569
- package/skills/cm-security-gate/SKILL.archive.md +239 -0
- package/skills/cm-security-gate/SKILL.md +15 -228
- package/skills/cm-skill-health/SKILL.archive.md +83 -0
- package/skills/cm-skill-health/SKILL.md +16 -73
- package/skills/cm-skill-index/SKILL.md +8 -0
- package/skills/cm-skill-mastery/SKILL.archive.md +156 -0
- package/skills/cm-skill-mastery/SKILL.md +16 -146
- package/skills/cm-skill-search/SKILL.archive.md +49 -0
- package/skills/cm-skill-search/SKILL.md +17 -40
- package/skills/cm-skill-share/SKILL.archive.md +58 -0
- package/skills/cm-skill-share/SKILL.md +17 -49
- package/skills/cm-sprint-bus/SKILL.md +9 -0
- package/skills/cm-start/SKILL.md +17 -0
- package/skills/cm-tdd/SKILL.md +19 -0
- package/skills/cm-terminal/SKILL.md +15 -0
- package/skills/cm-test-gate/SKILL.archive.md +245 -0
- package/skills/cm-test-gate/SKILL.md +15 -234
- package/skills/cm-ui-preview/SKILL.archive.md +153 -0
- package/skills/cm-ui-preview/SKILL.md +16 -143
- package/skills/cm-ux-master/cli/uxmaster/commands/mcp.py +1 -1
- package/skills/cm-ux-master/mcp/mcp-config.json +1 -1
- package/skills/cm-ux-master/mcp/server.py +2 -2
- package/skills/profiles/design.txt +1 -1
- package/skills/profiles/full.txt +0 -10
- package/skills/profiles/growth.txt +8 -8
- package/skills/profiles/knowledge.txt +1 -1
- package/skills/profiles/top35.json +41 -0
- package/adapters/antigravity.js +0 -15
- package/adapters/claude-code.js +0 -17
- package/adapters/cursor.js +0 -16
- package/skills/cm-ads-tracker/SKILL.md +0 -401
- package/skills/cm-ads-tracker/evals/evals.json +0 -55
- package/skills/cm-ads-tracker/references/gtm-architecture.md +0 -321
- package/skills/cm-ads-tracker/references/industry-events.md +0 -294
- package/skills/cm-ads-tracker/references/platforms-api.md +0 -238
- package/skills/cm-ads-tracker/templates/capi-payload.md +0 -79
- package/skills/cm-ads-tracker/templates/datalayer-push.js +0 -104
- package/skills/cm-ads-tracker/templates/gtm-variables.js +0 -56
- package/skills/cm-auto-publisher/SKILL.md +0 -81
- package/skills/cm-booking-calendar/SKILL.md +0 -521
- package/skills/cm-booking-calendar/references/industry-patterns.md +0 -527
- package/skills/cm-booking-calendar/templates/booking-form.css +0 -626
- package/skills/cm-booking-calendar/templates/booking-form.html +0 -477
- package/skills/cm-booking-calendar/templates/calendar-engine.js +0 -419
- package/skills/cm-booking-calendar/templates/calendar-export.js +0 -395
- package/skills/cm-booking-calendar/templates/reminder-config.js +0 -629
- package/skills/cm-content-factory/.content-factory-state.json +0 -132
- package/skills/cm-content-factory/.git 2/logs/refs/heads/main +0 -1
- package/skills/cm-content-factory/.git 2/logs/refs/remotes/origin/main +0 -1
- package/skills/cm-content-factory/.git 2/objects/02/fb0956734b5f8ba3f918b7defd04a89cfe0076 +0 -0
- package/skills/cm-content-factory/.git 2/objects/08/1e129d75dc6feac6c02037272e6bd1a04e3324 +0 -0
- package/skills/cm-content-factory/.git 2/objects/0c/5393416f3c5e01c9a655a802bff0dd52f76f0a +0 -0
- package/skills/cm-content-factory/.git 2/objects/10/0b9be46978a946a77188f68be725098a122001 +0 -0
- package/skills/cm-content-factory/.git 2/objects/10/cf041167fc9843610eb3d90259ef3396315fdc +0 -0
- package/skills/cm-content-factory/.git 2/objects/12/5e19538dd6e1338ffe74f6c4c165b00435bf48 +0 -0
- package/skills/cm-content-factory/.git 2/objects/16/a9b9d0088d5c1347628b45a2620b479d8ad57c +0 -0
- package/skills/cm-content-factory/.git 2/objects/17/8c2a9ef93c33ae4eec9d58e82321f9229843a1 +0 -0
- package/skills/cm-content-factory/.git 2/objects/25/397ae41d09104d763bdcac2695209d85cdea89 +0 -0
- package/skills/cm-content-factory/.git 2/objects/2f/a836b7947f2d458e1f639788bf4bb0983a3305 +0 -0
- package/skills/cm-content-factory/.git 2/objects/3a/baaaf0a1c0909c0828335791557125fba911e0 +0 -0
- package/skills/cm-content-factory/.git 2/objects/42/2924221b81f5ce3c4e4daac9a64a24f9b01f9a +0 -0
- package/skills/cm-content-factory/.git 2/objects/42/ec0ce707447dc11446a34c9995fb8533801731 +0 -0
- package/skills/cm-content-factory/.git 2/objects/46/e43ce92866d56ce74b1d750db307cfe6154a15 +0 -0
- package/skills/cm-content-factory/.git 2/objects/48/5e41b633c63f55b8277bcc59f44f67681f671a +0 -0
- package/skills/cm-content-factory/.git 2/objects/49/49c596a3a89fa240642acd95dd3258e261eb09 +0 -0
- package/skills/cm-content-factory/.git 2/objects/50/9d42d8412ef8eaf7f7e138476bac2e4d10ce60 +0 -0
- package/skills/cm-content-factory/.git 2/objects/55/0c8c389d981b463ef849aeb792d8be3ccb6ec8 +0 -0
- package/skills/cm-content-factory/.git 2/objects/5d/82d3b18410cdda3ace3677436f0cb599dbe2d2 +0 -0
- package/skills/cm-content-factory/.git 2/objects/60/0617c58e871a38b33bf29e282d132bb3c381ad +0 -0
- package/skills/cm-content-factory/.git 2/objects/6a/8369a99c687b7245c92ffaf0e0f0dab9014504 +0 -0
- package/skills/cm-content-factory/.git 2/objects/79/bea435d40ab531c1aaf6be0432c6a5b7aaed21 +0 -0
- package/skills/cm-content-factory/.git 2/objects/7e/5ebd79251c2f14e4aceb86c74b6b6daae6b500 +0 -0
- package/skills/cm-content-factory/.git 2/objects/81/98a822a60178d6d5023ddb3e222cddf048742e +0 -0
- package/skills/cm-content-factory/.git 2/objects/86/0a0e1943dfe53411d2e499a1f16f46a96ef758 +0 -0
- package/skills/cm-content-factory/.git 2/objects/86/971fb55fdc081fdbae52376f0f13e57a4e9b04 +0 -0
- package/skills/cm-content-factory/.git 2/objects/88/b89dd609a0a03f8d4fe8bfde20d5b8fc1d326d +0 -0
- package/skills/cm-content-factory/.git 2/objects/90/8737edb6b7809e32cc01590b4e08ba42a9d40d +0 -0
- package/skills/cm-content-factory/.git 2/objects/93/d5a8a9a7d4fb7f11491cb596a6880528725118 +0 -0
- package/skills/cm-content-factory/.git 2/objects/98/46a2ab81d0c3b3eb00ef88fc56989aa7e9f316 +0 -0
- package/skills/cm-content-factory/.git 2/objects/9b/d8dd1e49cf274eaf9c555f3ab39dce7af5715e +0 -0
- package/skills/cm-content-factory/.git 2/objects/a1/13329fb0cec96ae78b222d33a24c3b5bc7fa1f +0 -0
- package/skills/cm-content-factory/.git 2/objects/a9/e6effe626e8a3aea3a8fc3364b492191c6e7d0 +0 -0
- package/skills/cm-content-factory/.git 2/objects/ad/6de7e48d9782cca9353d1ff0aa1aab7fe1df85 +0 -0
- package/skills/cm-content-factory/.git 2/objects/af/54ae316f771ff692e299ffcd8bf2f06b413b59 +0 -0
- package/skills/cm-content-factory/.git 2/objects/b0/4cb8b0b00dad633e731c1472161419e738d674 +0 -0
- package/skills/cm-content-factory/.git 2/objects/b3/094abb0b9ed46419b269e4a4e36a459690e3b0 +0 -0
- package/skills/cm-content-factory/.git 2/objects/b9/435c5d4baac2cfc5c83009ddd27b46b60db5f1 +0 -0
- package/skills/cm-content-factory/.git 2/objects/ba/5da17dbaec5ec2dcfdfd126aead518d1171d5c +0 -0
- package/skills/cm-content-factory/.git 2/objects/c0/bf58703aa258ba5dd63083bebaec8f223d844c +0 -0
- package/skills/cm-content-factory/.git 2/objects/c4/701a34edf1fc1bad58ccc57bd03f9426acb59a +0 -0
- package/skills/cm-content-factory/.git 2/objects/c7/5ccce9a4e5cc74d9b3174550cf6d993ca43638 +0 -0
- package/skills/cm-content-factory/.git 2/objects/c7/710d59b5a35b0f1f0a0399386643a0bd94c929 +0 -0
- package/skills/cm-content-factory/.git 2/objects/d1/fe58237112e953e5fec52da22cf38e08be3df9 +0 -5
- package/skills/cm-content-factory/.git 2/objects/d2/2bbe9fd2f74c95bc5583e803f5e435f1e2cd86 +0 -0
- package/skills/cm-content-factory/.git 2/objects/d7/e72852ea2bff74581dbf247d400120086229f4 +0 -0
- package/skills/cm-content-factory/.git 2/objects/d8/d4c3b5553e4fd72807e1d4b49ef07d9ef3ac35 +0 -0
- package/skills/cm-content-factory/.git 2/objects/dc/75050c2876f6a02ae2a53a3c886f395b622977 +0 -0
- package/skills/cm-content-factory/.git 2/objects/ee/e8546f95acec500187c08a28a8b9ee02db0dec +0 -0
- package/skills/cm-content-factory/.git 2/objects/ef/263c059208b416c2146434f10cb2b9fabcba16 +0 -0
- package/skills/cm-content-factory/.git 2/objects/f3/ae597e84d9a59b88acd21c99bde2eaf686d785 +0 -0
- package/skills/cm-content-factory/.git 2/objects/f3/f6f5673c821d3d8e76fa267a9e882e7a5387ea +0 -0
- package/skills/cm-content-factory/.git 2/objects/f9/6e6d0ad02624dd11d5848594d056caef7a5e8b +0 -0
- package/skills/cm-content-factory/.git 2/objects/ff/278988fc1edf0db3abcf18de795f4cc0b4f3e1 +0 -0
- package/skills/cm-content-factory/.git 2/refs/heads/main +0 -1
- package/skills/cm-content-factory/.git 2/refs/remotes/origin/main +0 -1
- package/skills/cm-content-factory/.pytest_cache 2/v/cache/nodeids +0 -76
- package/skills/cm-content-factory/.pytest_cache 2/v/cache/stepwise +0 -1
- package/skills/cm-content-factory/AGENTS.md +0 -61
- package/skills/cm-content-factory/CLAUDE.md +0 -63
- package/skills/cm-content-factory/CURSOR.md +0 -43
- package/skills/cm-content-factory/Content Factory.zip +0 -0
- package/skills/cm-content-factory/SKILL.md +0 -416
- package/skills/cm-content-factory/cf +0 -313
- package/skills/cm-content-factory/config.schema.json +0 -397
- package/skills/cm-content-factory/dashboard/app.js +0 -556
- package/skills/cm-content-factory/dashboard/index.html +0 -397
- package/skills/cm-content-factory/dashboard/style.css +0 -1211
- package/skills/cm-content-factory/examples/01-real-estate.config.json +0 -146
- package/skills/cm-content-factory/examples/02-personal-finance.config.json +0 -146
- package/skills/cm-content-factory/examples/03-health-wellness.config.json +0 -147
- package/skills/cm-content-factory/examples/04-saas-software.config.json +0 -147
- package/skills/cm-content-factory/examples/05-legal-services.config.json +0 -147
- package/skills/cm-content-factory/examples/06-insurance.config.json +0 -146
- package/skills/cm-content-factory/examples/07-ecommerce-dropship.config.json +0 -146
- package/skills/cm-content-factory/examples/08-online-education.config.json +0 -147
- package/skills/cm-content-factory/examples/09-crypto-defi.config.json +0 -147
- package/skills/cm-content-factory/examples/10-beauty-skincare.config.json +0 -147
- package/skills/cm-content-factory/examples/11-home-services.config.json +0 -146
- package/skills/cm-content-factory/examples/12-dental-clinic.config.json +0 -147
- package/skills/cm-content-factory/examples/13-pet-care.config.json +0 -147
- package/skills/cm-content-factory/examples/14-travel-hospitality.config.json +0 -147
- package/skills/cm-content-factory/examples/15-ai-automation.config.json +0 -147
- package/skills/cm-content-factory/examples/16-wedding-events.config.json +0 -147
- package/skills/cm-content-factory/examples/17-fitness-coaching.config.json +0 -148
- package/skills/cm-content-factory/examples/18-cybersecurity.config.json +0 -147
- package/skills/cm-content-factory/examples/19-food-restaurant.config.json +0 -148
- package/skills/cm-content-factory/examples/20-solar-energy.config.json +0 -147
- package/skills/cm-content-factory/examples/fitness-blog.config.json +0 -116
- package/skills/cm-content-factory/examples/tech-blog.config.json +0 -107
- package/skills/cm-content-factory/extensions/EXTENSION_GUIDE.md +0 -72
- package/skills/cm-content-factory/extensions/hooks.py +0 -126
- package/skills/cm-content-factory/extensions/openclaw_adapter.py +0 -132
- package/skills/cm-content-factory/landing/docs/content/changelog.md +0 -36
- package/skills/cm-content-factory/landing/docs/content/deployment.md +0 -46
- package/skills/cm-content-factory/landing/docs/content/execution-flow.md +0 -67
- package/skills/cm-content-factory/landing/docs/content/memory-system.md +0 -38
- package/skills/cm-content-factory/landing/docs/content/openspace.md +0 -27
- package/skills/cm-content-factory/landing/docs/content/use-cases.md +0 -26
- package/skills/cm-content-factory/landing/docs/content/v5-intro.md +0 -28
- package/skills/cm-content-factory/landing/docs/index.html +0 -240
- package/skills/cm-content-factory/landing/index.html +0 -680
- package/skills/cm-content-factory/landing/script.js +0 -143
- package/skills/cm-content-factory/landing/style.css +0 -1216
- package/skills/cm-content-factory/landing/translations.js +0 -508
- package/skills/cm-content-factory/logs/events.jsonl +0 -11
- package/skills/cm-content-factory/profiles/_template.profile.json +0 -231
- package/skills/cm-content-factory/profiles/finance.profile.json +0 -278
- package/skills/cm-content-factory/profiles/legal.profile.json +0 -263
- package/skills/cm-content-factory/profiles/medical-research.profile.json +0 -321
- package/skills/cm-content-factory/profiles/technology.profile.json +0 -275
- package/skills/cm-content-factory/scripts/agent_dispatcher.py +0 -266
- package/skills/cm-content-factory/scripts/audit.py +0 -106
- package/skills/cm-content-factory/scripts/dashboard_server.py +0 -225
- package/skills/cm-content-factory/scripts/deploy.py +0 -146
- package/skills/cm-content-factory/scripts/extract.py +0 -132
- package/skills/cm-content-factory/scripts/landing_generator.py +0 -459
- package/skills/cm-content-factory/scripts/memory.py +0 -521
- package/skills/cm-content-factory/scripts/monetize.py +0 -239
- package/skills/cm-content-factory/scripts/pipeline.py +0 -357
- package/skills/cm-content-factory/scripts/plan.py +0 -163
- package/skills/cm-content-factory/scripts/publish.py +0 -145
- package/skills/cm-content-factory/scripts/research.py +0 -337
- package/skills/cm-content-factory/scripts/scaffold.py +0 -464
- package/skills/cm-content-factory/scripts/scoreboard.py +0 -336
- package/skills/cm-content-factory/scripts/seo.py +0 -90
- package/skills/cm-content-factory/scripts/state_manager.py +0 -320
- package/skills/cm-content-factory/scripts/token_manager.py +0 -268
- package/skills/cm-content-factory/scripts/validate.py +0 -221
- package/skills/cm-content-factory/scripts/wizard.py +0 -329
- package/skills/cm-content-factory/scripts/write.py +0 -93
- package/skills/cm-content-factory/sites/docs-site/src/assets/houston.webp +0 -0
- package/skills/cm-content-factory/sites/docs-site/src/content/docs/architecture.md +0 -90
- package/skills/cm-content-factory/sites/docs-site/src/content/docs/data-flow.md +0 -54
- package/skills/cm-content-factory/sites/docs-site/src/content/docs/deployment.md +0 -38
- package/skills/cm-content-factory/sites/docs-site/src/content/docs/flows/index.md +0 -65
- package/skills/cm-content-factory/sites/docs-site/src/content/docs/flows/lc-content-lifecycle.md +0 -48
- package/skills/cm-content-factory/sites/docs-site/src/content/docs/flows/seq-write-mode.md +0 -39
- package/skills/cm-content-factory/sites/docs-site/src/content/docs/flows/uj-first-batch.md +0 -42
- package/skills/cm-content-factory/sites/docs-site/src/content/docs/flows/wf-content-pipeline.md +0 -51
- package/skills/cm-content-factory/sites/docs-site/src/content/docs/flows/wf-learning-cycle.md +0 -52
- package/skills/cm-content-factory/sites/docs-site/src/content/docs/getting-started/configuration.md +0 -86
- package/skills/cm-content-factory/sites/docs-site/src/content/docs/getting-started/installation.md +0 -80
- package/skills/cm-content-factory/sites/docs-site/src/content/docs/getting-started/intro.md +0 -58
- package/skills/cm-content-factory/sites/docs-site/src/content/docs/index.md +0 -102
- package/skills/cm-content-factory/sites/docs-site/src/content/docs/jtbd/index.md +0 -45
- package/skills/cm-content-factory/sites/docs-site/src/content/docs/jtbd/optimize-seo.md +0 -29
- package/skills/cm-content-factory/sites/docs-site/src/content/docs/jtbd/scale-content-production.md +0 -55
- package/skills/cm-content-factory/sites/docs-site/src/content/docs/jtbd/standardize-quality.md +0 -29
- package/skills/cm-content-factory/sites/docs-site/src/content/docs/personas/buyer-cmo-huong.md +0 -41
- package/skills/cm-content-factory/sites/docs-site/src/content/docs/personas/buyer-content-lead-khoa.md +0 -40
- package/skills/cm-content-factory/sites/docs-site/src/content/docs/personas/index.md +0 -56
- package/skills/cm-content-factory/sites/docs-site/src/content/docs/personas/user-content-manager-lan.md +0 -46
- package/skills/cm-content-factory/sites/docs-site/src/content/docs/personas/user-seo-minh.md +0 -45
- package/skills/cm-content-factory/sites/docs-site/src/content/docs/personas/user-writer-tu.md +0 -45
- package/skills/cm-content-factory/sites/docs-site/src/content/docs/sop/content-pipeline.md +0 -108
- package/skills/cm-content-factory/sites/docs-site/src/content/docs/sop/index.md +0 -22
- package/skills/cm-content-factory/sites/docs-site/src/content/docs/sop/memory-system.md +0 -52
- package/skills/cm-content-factory/sites/docs-site/src/content/docs/sop/seo-optimization.md +0 -58
- package/skills/cm-content-factory/sites/docs-site/src/content/docs/sop/troubleshooting-guide.md +0 -92
- package/skills/cm-content-factory/sites/docs-site/src/styles/custom.css +0 -575
- package/skills/cm-content-factory/tests/conftest.py +0 -66
- package/skills/cm-content-factory/tests/test_agent_dispatcher.py +0 -125
- package/skills/cm-content-factory/tests/test_memory.py +0 -128
- package/skills/cm-content-factory/tests/test_pipeline.py +0 -107
- package/skills/cm-content-factory/tests/test_research.py +0 -56
- package/skills/cm-content-factory/tests/test_state_manager.py +0 -131
- package/skills/cm-content-factory/tests/test_token_manager.py +0 -110
- package/skills/cm-content-factory/tests/test_wizard.py +0 -121
- package/skills/cm-cro-methodology/SKILL.md +0 -290
- package/skills/cm-cro-methodology/references/COPYWRITING.md +0 -178
- package/skills/cm-cro-methodology/references/OBJECTIONS.md +0 -135
- package/skills/cm-cro-methodology/references/PERSUASION.md +0 -158
- package/skills/cm-cro-methodology/references/RESEARCH.md +0 -220
- package/skills/cm-cro-methodology/references/funnel-analysis.md +0 -365
- package/skills/cm-cro-methodology/references/testing-methodology.md +0 -330
- package/skills/cm-google-form/SKILL.md +0 -266
- package/skills/cm-google-form/templates/apps-script.js +0 -55
- package/skills/cm-google-form/templates/form-markup.html +0 -110
- package/skills/cm-google-form/templates/form-submit.js +0 -201
- package/skills/cm-google-form/templates/toast.css +0 -152
- package/skills/cm-growth-hacking/SKILL.md +0 -282
- package/skills/cm-growth-hacking/bottom-sheet-engine.md +0 -261
- package/skills/cm-growth-hacking/calendar-integration.md +0 -264
- package/skills/cm-growth-hacking/references/engagement-patterns.md +0 -346
- package/skills/cm-growth-hacking/templates/bottom-sheet.css +0 -528
- package/skills/cm-growth-hacking/templates/bottom-sheet.js +0 -269
- package/skills/cm-growth-hacking/templates/calendar-cta.js +0 -213
- package/skills/cm-growth-hacking/templates/tracking-events.js +0 -211
- package/skills/cm-growth-hacking/templates/trigger-manager.js +0 -254
- package/skills/cm-growth-hacking/tracking-events.md +0 -246
- package/skills/cm-growth-hacking/trigger-system.md +0 -342
- package/skills/cm-jtbd/SKILL.md +0 -98
- package/skills/cm-notebooklm/SKILL.md +0 -156
- package/skills/cm-notebooklm/references/command_reference.md +0 -94
- package/skills/cm-notebooklm/references/workflows.md +0 -60
- package/skills/cm-notebooklm/resources/knowledge_sources.md +0 -106
- package/skills/cm-notebooklm/scripts/brain-sync.sh +0 -453
- package/skills/cm-notebooklm/scripts/graduate_wisdom.py +0 -101
- package/skills/cm-readit/SKILL.md +0 -289
- package/skills/cm-readit/audio-player.md +0 -206
- package/skills/cm-readit/examples/blog-reader.js +0 -352
- package/skills/cm-readit/examples/voice-cro.js +0 -390
- package/skills/cm-readit/tts-engine.md +0 -262
- package/skills/cm-readit/ui-patterns.md +0 -362
- package/skills/cm-readit/voice-cro.md +0 -223
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Plan Phase — Config-driven topic planning.
|
|
4
|
-
|
|
5
|
-
Generates topic queue from knowledge-base using configured article types.
|
|
6
|
-
Delegates to existing topic_planner.py or generates from config.
|
|
7
|
-
|
|
8
|
-
Usage:
|
|
9
|
-
python3 plan.py --config content-factory.config.json
|
|
10
|
-
python3 plan.py --config content-factory.config.json --dry-run
|
|
11
|
-
python3 plan.py --config content-factory.config.json --group CVG
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
import json
|
|
15
|
-
import sys
|
|
16
|
-
import os
|
|
17
|
-
import subprocess
|
|
18
|
-
import argparse
|
|
19
|
-
from pathlib import Path
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def load_config(config_path: str) -> dict:
|
|
23
|
-
with open(config_path, "r", encoding="utf-8") as f:
|
|
24
|
-
return json.load(f)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def run_planner(project_root: Path, config: dict, dry_run: bool, group: str = None):
|
|
28
|
-
"""Run topic planning using existing project script."""
|
|
29
|
-
planner = project_root / "scripts" / "topic_planner.py"
|
|
30
|
-
if not planner.exists():
|
|
31
|
-
print("❌ scripts/topic_planner.py not found.")
|
|
32
|
-
print(" Creating basic topic plan from knowledge-base...")
|
|
33
|
-
return generate_basic_plan(project_root, config, dry_run, group)
|
|
34
|
-
|
|
35
|
-
cmd = ["python3", str(planner)]
|
|
36
|
-
if group:
|
|
37
|
-
cmd.extend(["--group", group])
|
|
38
|
-
|
|
39
|
-
print(f" 📋 Running topic planner...")
|
|
40
|
-
print(f" Article types: {len(config['content']['article_types'])}")
|
|
41
|
-
print(f" Output: {config['output'].get('queue_dir', 'topics-queue/')}")
|
|
42
|
-
|
|
43
|
-
if dry_run:
|
|
44
|
-
cmd.append("--dry-run")
|
|
45
|
-
|
|
46
|
-
result = subprocess.run(cmd, cwd=str(project_root))
|
|
47
|
-
return result.returncode == 0
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def generate_basic_plan(project_root: Path, config: dict, dry_run: bool, group: str = None):
|
|
51
|
-
"""Generate topic plan directly from config when no planner script exists."""
|
|
52
|
-
kb_dir = project_root / config["output"].get("knowledge_dir", "knowledge-base/")
|
|
53
|
-
queue_dir = project_root / config["output"].get("queue_dir", "topics-queue/")
|
|
54
|
-
index_file = kb_dir / "index.json"
|
|
55
|
-
|
|
56
|
-
if not index_file.exists():
|
|
57
|
-
print(f"❌ Knowledge-base index not found: {index_file}")
|
|
58
|
-
print(" Run extract phase first.")
|
|
59
|
-
return False
|
|
60
|
-
|
|
61
|
-
with open(index_file, "r", encoding="utf-8") as f:
|
|
62
|
-
index = json.load(f)
|
|
63
|
-
|
|
64
|
-
article_types = config["content"]["article_types"]
|
|
65
|
-
topics = []
|
|
66
|
-
|
|
67
|
-
entries = index.get("entries", [])
|
|
68
|
-
if group:
|
|
69
|
-
entries = [e for e in entries if e.get("group_code") == group]
|
|
70
|
-
|
|
71
|
-
for entry in entries:
|
|
72
|
-
for at in article_types:
|
|
73
|
-
topic_name = entry.get("name", entry.get("disease_name", "Unknown"))
|
|
74
|
-
title = at["title_template"].replace("{topic_name}", topic_name)
|
|
75
|
-
slug = _slugify(title)
|
|
76
|
-
|
|
77
|
-
topics.append({
|
|
78
|
-
"title": title,
|
|
79
|
-
"slug": slug,
|
|
80
|
-
"article_type": at["id"],
|
|
81
|
-
"category": at["category"],
|
|
82
|
-
"seo_intent": at.get("seo_intent", "informational"),
|
|
83
|
-
"tags": at.get("tags_base", []),
|
|
84
|
-
"source_entry": entry.get("name", ""),
|
|
85
|
-
"group_code": entry.get("group_code", ""),
|
|
86
|
-
"status": "pending"
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
if dry_run:
|
|
90
|
-
print(f" [DRY RUN] Would generate {len(topics)} topics")
|
|
91
|
-
for t in topics[:5]:
|
|
92
|
-
print(f" → {t['title'][:60]}...")
|
|
93
|
-
return True
|
|
94
|
-
|
|
95
|
-
queue_dir.mkdir(parents=True, exist_ok=True)
|
|
96
|
-
from datetime import datetime
|
|
97
|
-
batch_file = queue_dir / f"batch-{datetime.now().strftime('%Y%m%d-%H%M')}.json"
|
|
98
|
-
|
|
99
|
-
batch = {
|
|
100
|
-
"generated_at": datetime.now().isoformat(),
|
|
101
|
-
"total_topics": len(topics),
|
|
102
|
-
"config_niche": config["niche"],
|
|
103
|
-
"topics": topics
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
with open(batch_file, "w", encoding="utf-8") as f:
|
|
107
|
-
json.dump(batch, f, ensure_ascii=False, indent=2)
|
|
108
|
-
|
|
109
|
-
print(f" ✅ Generated {len(topics)} topics → {batch_file}")
|
|
110
|
-
return True
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def _slugify(text: str, max_len: int = 80) -> str:
|
|
114
|
-
"""Convert text to URL slug."""
|
|
115
|
-
import re
|
|
116
|
-
import unicodedata
|
|
117
|
-
|
|
118
|
-
# Vietnamese character map
|
|
119
|
-
char_map = {
|
|
120
|
-
'à': 'a', 'á': 'a', 'ả': 'a', 'ã': 'a', 'ạ': 'a',
|
|
121
|
-
'ă': 'a', 'ắ': 'a', 'ằ': 'a', 'ẳ': 'a', 'ẵ': 'a', 'ặ': 'a',
|
|
122
|
-
'â': 'a', 'ấ': 'a', 'ầ': 'a', 'ẩ': 'a', 'ẫ': 'a', 'ậ': 'a',
|
|
123
|
-
'đ': 'd',
|
|
124
|
-
'è': 'e', 'é': 'e', 'ẻ': 'e', 'ẽ': 'e', 'ẹ': 'e',
|
|
125
|
-
'ê': 'e', 'ế': 'e', 'ề': 'e', 'ể': 'e', 'ễ': 'e', 'ệ': 'e',
|
|
126
|
-
'ì': 'i', 'í': 'i', 'ỉ': 'i', 'ĩ': 'i', 'ị': 'i',
|
|
127
|
-
'ò': 'o', 'ó': 'o', 'ỏ': 'o', 'õ': 'o', 'ọ': 'o',
|
|
128
|
-
'ô': 'o', 'ố': 'o', 'ồ': 'o', 'ổ': 'o', 'ỗ': 'o', 'ộ': 'o',
|
|
129
|
-
'ơ': 'o', 'ớ': 'o', 'ờ': 'o', 'ở': 'o', 'ỡ': 'o', 'ợ': 'o',
|
|
130
|
-
'ù': 'u', 'ú': 'u', 'ủ': 'u', 'ũ': 'u', 'ụ': 'u',
|
|
131
|
-
'ư': 'u', 'ứ': 'u', 'ừ': 'u', 'ử': 'u', 'ữ': 'u', 'ự': 'u',
|
|
132
|
-
'ỳ': 'y', 'ý': 'y', 'ỷ': 'y', 'ỹ': 'y', 'ỵ': 'y',
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
text = text.lower()
|
|
136
|
-
result = []
|
|
137
|
-
for c in text:
|
|
138
|
-
result.append(char_map.get(c, c))
|
|
139
|
-
text = ''.join(result)
|
|
140
|
-
|
|
141
|
-
text = re.sub(r'[^a-z0-9\s-]', '', text)
|
|
142
|
-
text = re.sub(r'[\s-]+', '-', text).strip('-')
|
|
143
|
-
return text[:max_len]
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
def main():
|
|
147
|
-
parser = argparse.ArgumentParser(description="Plan Phase — Topic planning")
|
|
148
|
-
parser.add_argument("--config", required=True, help="Path to config JSON")
|
|
149
|
-
parser.add_argument("--dry-run", action="store_true")
|
|
150
|
-
parser.add_argument("--group", help="Filter by group code")
|
|
151
|
-
args = parser.parse_args()
|
|
152
|
-
|
|
153
|
-
config = load_config(args.config)
|
|
154
|
-
project_root = Path(args.config).resolve().parent
|
|
155
|
-
|
|
156
|
-
print(f"📋 PLAN Phase — {len(config['content']['article_types'])} article types")
|
|
157
|
-
|
|
158
|
-
success = run_planner(project_root, config, args.dry_run, args.group)
|
|
159
|
-
sys.exit(0 if success else 1)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if __name__ == "__main__":
|
|
163
|
-
main()
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Publish Phase — Config-driven build + deploy.
|
|
4
|
-
|
|
5
|
-
Runs build command, validates content, commits, and pushes to git.
|
|
6
|
-
|
|
7
|
-
Usage:
|
|
8
|
-
python3 publish.py --config content-factory.config.json --build # Build only
|
|
9
|
-
python3 publish.py --config content-factory.config.json --push # Git push only
|
|
10
|
-
python3 publish.py --config content-factory.config.json # Build + push
|
|
11
|
-
python3 publish.py --config content-factory.config.json --dry-run
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
import json
|
|
15
|
-
import sys
|
|
16
|
-
import subprocess
|
|
17
|
-
import argparse
|
|
18
|
-
from pathlib import Path
|
|
19
|
-
from datetime import datetime
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def load_config(config_path: str) -> dict:
|
|
23
|
-
with open(config_path, "r", encoding="utf-8") as f:
|
|
24
|
-
return json.load(f)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def run_build(project_root: Path, config: dict, dry_run: bool) -> bool:
|
|
28
|
-
"""Run the build command from config."""
|
|
29
|
-
build_cmd = config.get("pipeline", {}).get("build_command", "")
|
|
30
|
-
if not build_cmd:
|
|
31
|
-
print(" ⚠️ No build_command in config.pipeline")
|
|
32
|
-
return True
|
|
33
|
-
|
|
34
|
-
if dry_run:
|
|
35
|
-
print(f" [DRY RUN] Would run: {build_cmd}")
|
|
36
|
-
return True
|
|
37
|
-
|
|
38
|
-
print(f" 🔨 Building: {build_cmd}")
|
|
39
|
-
result = subprocess.run(build_cmd, shell=True, cwd=str(project_root))
|
|
40
|
-
if result.returncode == 0:
|
|
41
|
-
print(" ✅ Build complete")
|
|
42
|
-
else:
|
|
43
|
-
print(" ❌ Build failed")
|
|
44
|
-
return result.returncode == 0
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def run_git_push(project_root: Path, config: dict, dry_run: bool) -> bool:
|
|
48
|
-
"""Git add, commit, and push content."""
|
|
49
|
-
content_dir = config["output"]["content_dir"]
|
|
50
|
-
branch = config.get("pipeline", {}).get("git_branch", "main")
|
|
51
|
-
|
|
52
|
-
# Check for changes
|
|
53
|
-
result = subprocess.run(
|
|
54
|
-
["git", "status", "--porcelain", content_dir],
|
|
55
|
-
capture_output=True, text=True, cwd=str(project_root)
|
|
56
|
-
)
|
|
57
|
-
changed_lines = [l for l in result.stdout.strip().split('\n') if l.strip()]
|
|
58
|
-
num_changes = len(changed_lines)
|
|
59
|
-
|
|
60
|
-
if num_changes == 0:
|
|
61
|
-
print(" ✅ No new content to publish")
|
|
62
|
-
return True
|
|
63
|
-
|
|
64
|
-
print(f" 📄 Found {num_changes} new/modified files")
|
|
65
|
-
|
|
66
|
-
if dry_run:
|
|
67
|
-
print(" [DRY RUN] Would commit:")
|
|
68
|
-
for line in changed_lines[:10]:
|
|
69
|
-
print(f" {line}")
|
|
70
|
-
return True
|
|
71
|
-
|
|
72
|
-
# Stage
|
|
73
|
-
subprocess.run(["git", "add", content_dir], cwd=str(project_root))
|
|
74
|
-
|
|
75
|
-
# Also stage knowledge-base and topics-queue if they exist
|
|
76
|
-
kb_dir = config["output"].get("knowledge_dir", "knowledge-base/")
|
|
77
|
-
queue_dir = config["output"].get("queue_dir", "topics-queue/")
|
|
78
|
-
for extra in [kb_dir, queue_dir]:
|
|
79
|
-
if (project_root / extra).exists():
|
|
80
|
-
subprocess.run(["git", "add", extra], cwd=str(project_root))
|
|
81
|
-
|
|
82
|
-
# Commit
|
|
83
|
-
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
84
|
-
niche = config.get("niche", "content")
|
|
85
|
-
msg = f"auto: publish {num_changes} {niche} articles ({timestamp})"
|
|
86
|
-
|
|
87
|
-
subprocess.run(["git", "commit", "-m", msg], cwd=str(project_root))
|
|
88
|
-
|
|
89
|
-
# Push
|
|
90
|
-
push_result = subprocess.run(
|
|
91
|
-
["git", "push", "origin", branch],
|
|
92
|
-
capture_output=True, text=True, cwd=str(project_root)
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
if push_result.returncode != 0:
|
|
96
|
-
# Try alternate branch names
|
|
97
|
-
alt = "master" if branch == "main" else "main"
|
|
98
|
-
push_result = subprocess.run(
|
|
99
|
-
["git", "push", "origin", alt],
|
|
100
|
-
capture_output=True, text=True, cwd=str(project_root)
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
if push_result.returncode == 0:
|
|
104
|
-
print(f" ✅ Published {num_changes} articles to {branch}")
|
|
105
|
-
else:
|
|
106
|
-
print(f" ⚠️ Push failed: {push_result.stderr[:200]}")
|
|
107
|
-
print(" Content is committed locally. Push manually.")
|
|
108
|
-
|
|
109
|
-
return True
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def main():
|
|
113
|
-
parser = argparse.ArgumentParser(description="Publish Phase — Build + Deploy")
|
|
114
|
-
parser.add_argument("--config", required=True, help="Path to config JSON")
|
|
115
|
-
parser.add_argument("--build", action="store_true", help="Build only")
|
|
116
|
-
parser.add_argument("--push", action="store_true", help="Git push only")
|
|
117
|
-
parser.add_argument("--dry-run", action="store_true")
|
|
118
|
-
parser.add_argument("--group", help="(unused, for pipeline compat)")
|
|
119
|
-
args = parser.parse_args()
|
|
120
|
-
|
|
121
|
-
config = load_config(args.config)
|
|
122
|
-
project_root = Path(args.config).resolve().parent
|
|
123
|
-
|
|
124
|
-
print(f"🚀 PUBLISH Phase")
|
|
125
|
-
|
|
126
|
-
build_only = args.build and not args.push
|
|
127
|
-
push_only = args.push and not args.build
|
|
128
|
-
do_both = not args.build and not args.push
|
|
129
|
-
|
|
130
|
-
success = True
|
|
131
|
-
if build_only or do_both:
|
|
132
|
-
success = run_build(project_root, config, args.dry_run) and success
|
|
133
|
-
|
|
134
|
-
if push_only or do_both:
|
|
135
|
-
if config.get("pipeline", {}).get("auto_publish", False) or push_only:
|
|
136
|
-
success = run_git_push(project_root, config, args.dry_run) and success
|
|
137
|
-
else:
|
|
138
|
-
print(" ⬜ auto_publish is disabled in config")
|
|
139
|
-
print(" Use --push to force, or set pipeline.auto_publish: true")
|
|
140
|
-
|
|
141
|
-
sys.exit(0 if success else 1)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if __name__ == "__main__":
|
|
145
|
-
main()
|
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Research Engine — Auto-research any topic for the content factory.
|
|
4
|
-
|
|
5
|
-
Capabilities:
|
|
6
|
-
1. Web search for topic keywords
|
|
7
|
-
2. Competitor content analysis (scrape top SERP results)
|
|
8
|
-
3. Knowledge synthesis → structured JSON
|
|
9
|
-
4. Auto-save to memory/semantic/niche_knowledge.json
|
|
10
|
-
|
|
11
|
-
Usage:
|
|
12
|
-
python3 research.py --config content-factory.config.json --topic "fitness recovery"
|
|
13
|
-
python3 research.py --config content-factory.config.json --topic "bấm huyệt đau lưng" --depth deep
|
|
14
|
-
python3 research.py --config content-factory.config.json --competitor https://example.com
|
|
15
|
-
python3 research.py --config content-factory.config.json --gap-scan
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
import json
|
|
19
|
-
import sys
|
|
20
|
-
import re
|
|
21
|
-
import subprocess
|
|
22
|
-
import argparse
|
|
23
|
-
from pathlib import Path
|
|
24
|
-
from datetime import datetime
|
|
25
|
-
|
|
26
|
-
# Import memory engine if available
|
|
27
|
-
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
28
|
-
sys.path.insert(0, str(SCRIPT_DIR))
|
|
29
|
-
try:
|
|
30
|
-
from memory import MemoryEngine
|
|
31
|
-
except ImportError:
|
|
32
|
-
MemoryEngine = None
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class ResearchEngine:
|
|
36
|
-
"""Auto-research engine for discovering and synthesizing knowledge."""
|
|
37
|
-
|
|
38
|
-
def __init__(self, config_path: str):
|
|
39
|
-
with open(config_path, "r", encoding="utf-8") as f:
|
|
40
|
-
self.config = json.load(f)
|
|
41
|
-
|
|
42
|
-
self.config_path = config_path
|
|
43
|
-
self.project_root = Path(config_path).resolve().parent
|
|
44
|
-
self.research_cfg = self.config.get("research", {})
|
|
45
|
-
self.memory = MemoryEngine(config_path) if MemoryEngine else None
|
|
46
|
-
|
|
47
|
-
def research_topic(self, topic: str, depth: str = "standard") -> dict:
|
|
48
|
-
"""Research a topic using web search and synthesis.
|
|
49
|
-
|
|
50
|
-
Args:
|
|
51
|
-
topic: The topic to research
|
|
52
|
-
depth: "quick" (3 sources), "standard" (5), "deep" (10)
|
|
53
|
-
|
|
54
|
-
Returns:
|
|
55
|
-
Structured knowledge dict
|
|
56
|
-
"""
|
|
57
|
-
max_sources = {"quick": 3, "standard": 5, "deep": 10}.get(depth, 5)
|
|
58
|
-
max_sources = min(max_sources, self.research_cfg.get("max_sources_per_topic", 10))
|
|
59
|
-
|
|
60
|
-
print(f" 🔍 Researching: {topic} (depth: {depth}, max sources: {max_sources})")
|
|
61
|
-
|
|
62
|
-
knowledge = {
|
|
63
|
-
"topic": topic,
|
|
64
|
-
"researched_at": datetime.now().isoformat(),
|
|
65
|
-
"depth": depth,
|
|
66
|
-
"sources": [],
|
|
67
|
-
"key_points": [],
|
|
68
|
-
"subtopics": [],
|
|
69
|
-
"competitor_insights": [],
|
|
70
|
-
"content_angles": [],
|
|
71
|
-
"keywords": [],
|
|
72
|
-
"questions_people_ask": []
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
# Step 1: Generate search queries
|
|
76
|
-
queries = self._generate_queries(topic)
|
|
77
|
-
print(f" 📋 Generated {len(queries)} search queries")
|
|
78
|
-
|
|
79
|
-
# Step 2: Search and extract
|
|
80
|
-
for query in queries[:max_sources]:
|
|
81
|
-
result = self._search_web(query)
|
|
82
|
-
if result:
|
|
83
|
-
knowledge["sources"].append(result)
|
|
84
|
-
|
|
85
|
-
# Step 3: Extract key points from sources
|
|
86
|
-
knowledge["key_points"] = self._extract_key_points(knowledge["sources"])
|
|
87
|
-
|
|
88
|
-
# Step 4: Generate content angles
|
|
89
|
-
knowledge["content_angles"] = self._suggest_content_angles(topic, knowledge["key_points"])
|
|
90
|
-
|
|
91
|
-
# Step 5: Save to memory
|
|
92
|
-
if self.memory:
|
|
93
|
-
self.memory.add_niche_knowledge(topic, {
|
|
94
|
-
"topics": {topic: knowledge},
|
|
95
|
-
"sources": [s.get("url", "") for s in knowledge["sources"]]
|
|
96
|
-
})
|
|
97
|
-
print(f" 💾 Saved to memory/semantic/niche_knowledge.json")
|
|
98
|
-
|
|
99
|
-
# Save as standalone research file
|
|
100
|
-
research_dir = self.project_root / "memory" / "research"
|
|
101
|
-
research_dir.mkdir(parents=True, exist_ok=True)
|
|
102
|
-
slug = re.sub(r'[^a-z0-9]+', '-', topic.lower())[:50]
|
|
103
|
-
research_file = research_dir / f"{slug}.json"
|
|
104
|
-
with open(research_file, "w", encoding="utf-8") as f:
|
|
105
|
-
json.dump(knowledge, f, ensure_ascii=False, indent=2)
|
|
106
|
-
print(f" 📄 Research saved: {research_file.relative_to(self.project_root)}")
|
|
107
|
-
|
|
108
|
-
return knowledge
|
|
109
|
-
|
|
110
|
-
def analyze_competitor(self, url: str) -> dict:
|
|
111
|
-
"""Analyze a competitor's content structure."""
|
|
112
|
-
print(f" 🔎 Analyzing competitor: {url}")
|
|
113
|
-
|
|
114
|
-
analysis = {
|
|
115
|
-
"url": url,
|
|
116
|
-
"analyzed_at": datetime.now().isoformat(),
|
|
117
|
-
"content_structure": {},
|
|
118
|
-
"topics_covered": [],
|
|
119
|
-
"missing_from_us": [],
|
|
120
|
-
"their_strengths": [],
|
|
121
|
-
"our_advantages": []
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
# Try to extract page content
|
|
125
|
-
content = self._fetch_url(url)
|
|
126
|
-
if content:
|
|
127
|
-
analysis["content_structure"] = self._analyze_structure(content)
|
|
128
|
-
analysis["topics_covered"] = self._extract_topics(content)
|
|
129
|
-
|
|
130
|
-
# Compare with our content
|
|
131
|
-
our_content = self._get_our_content_inventory()
|
|
132
|
-
their_topics = set(t.lower() for t in analysis["topics_covered"])
|
|
133
|
-
our_topics = set(t.lower() for t in our_content)
|
|
134
|
-
analysis["missing_from_us"] = list(their_topics - our_topics)
|
|
135
|
-
|
|
136
|
-
return analysis
|
|
137
|
-
|
|
138
|
-
def scan_content_gaps(self) -> list:
|
|
139
|
-
"""Scan for content gaps — topics we should cover but don't."""
|
|
140
|
-
print(f" 🕳️ Scanning for content gaps...")
|
|
141
|
-
|
|
142
|
-
# Get our current content inventory
|
|
143
|
-
our_topics = self._get_our_content_inventory()
|
|
144
|
-
print(f" 📄 Our inventory: {len(our_topics)} articles")
|
|
145
|
-
|
|
146
|
-
# Get competitor URLs from config
|
|
147
|
-
competitor_urls = self.research_cfg.get("competitor_urls", [])
|
|
148
|
-
|
|
149
|
-
gaps = []
|
|
150
|
-
for url in competitor_urls:
|
|
151
|
-
analysis = self.analyze_competitor(url)
|
|
152
|
-
for missing in analysis.get("missing_from_us", []):
|
|
153
|
-
gaps.append({
|
|
154
|
-
"topic": missing,
|
|
155
|
-
"found_at": url,
|
|
156
|
-
"priority": "medium",
|
|
157
|
-
"estimated_difficulty": "medium"
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
# Also check knowledge base for unwritten topics
|
|
161
|
-
kb_dir = self.project_root / self.config["output"].get("knowledge_dir", "knowledge-base/")
|
|
162
|
-
if kb_dir.exists():
|
|
163
|
-
for group_dir in kb_dir.iterdir():
|
|
164
|
-
if not group_dir.is_dir():
|
|
165
|
-
continue
|
|
166
|
-
for json_file in group_dir.glob("*.json"):
|
|
167
|
-
with open(json_file) as f:
|
|
168
|
-
data = json.load(f)
|
|
169
|
-
disease_name = data.get("disease_name", json_file.stem)
|
|
170
|
-
if disease_name.lower() not in [t.lower() for t in our_topics]:
|
|
171
|
-
gaps.append({
|
|
172
|
-
"topic": disease_name,
|
|
173
|
-
"found_at": "knowledge-base",
|
|
174
|
-
"priority": "high",
|
|
175
|
-
"estimated_difficulty": "low"
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
print(f" 🕳️ Found {len(gaps)} content gaps")
|
|
179
|
-
|
|
180
|
-
# Save gap analysis
|
|
181
|
-
gap_file = self.project_root / "content-gaps.json"
|
|
182
|
-
with open(gap_file, "w", encoding="utf-8") as f:
|
|
183
|
-
json.dump({
|
|
184
|
-
"scanned_at": datetime.now().isoformat(),
|
|
185
|
-
"total_gaps": len(gaps),
|
|
186
|
-
"gaps": gaps
|
|
187
|
-
}, f, ensure_ascii=False, indent=2)
|
|
188
|
-
|
|
189
|
-
return gaps
|
|
190
|
-
|
|
191
|
-
# ──────────────────────────────────────────────
|
|
192
|
-
# Internal methods
|
|
193
|
-
# ──────────────────────────────────────────────
|
|
194
|
-
|
|
195
|
-
def _generate_queries(self, topic: str) -> list:
|
|
196
|
-
"""Generate search queries from topic."""
|
|
197
|
-
lang = self.config.get("brand", {}).get("language", "vi")
|
|
198
|
-
niche = self.config.get("niche", "")
|
|
199
|
-
|
|
200
|
-
queries = [topic]
|
|
201
|
-
|
|
202
|
-
if lang == "vi":
|
|
203
|
-
queries.extend([
|
|
204
|
-
f"{topic} là gì",
|
|
205
|
-
f"cách điều trị {topic}",
|
|
206
|
-
f"{topic} triệu chứng nguyên nhân",
|
|
207
|
-
f"{topic} bấm huyệt" if "medspa" in niche else f"{topic} hướng dẫn",
|
|
208
|
-
])
|
|
209
|
-
else:
|
|
210
|
-
queries.extend([
|
|
211
|
-
f"what is {topic}",
|
|
212
|
-
f"{topic} complete guide",
|
|
213
|
-
f"{topic} symptoms causes treatment",
|
|
214
|
-
f"best {topic} tips",
|
|
215
|
-
])
|
|
216
|
-
|
|
217
|
-
return queries
|
|
218
|
-
|
|
219
|
-
def _search_web(self, query: str) -> dict:
|
|
220
|
-
"""Search web for a query. Returns structured result."""
|
|
221
|
-
# Use curl for basic URL fetching, or delegate to browser tool
|
|
222
|
-
# This is a lightweight fallback — full research uses the AI agent's search
|
|
223
|
-
return {
|
|
224
|
-
"query": query,
|
|
225
|
-
"searched_at": datetime.now().isoformat(),
|
|
226
|
-
"method": "pending_ai_search",
|
|
227
|
-
"note": "Full search requires AI agent context. Use /research workflow."
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
def _fetch_url(self, url: str) -> str:
|
|
231
|
-
"""Fetch URL content."""
|
|
232
|
-
try:
|
|
233
|
-
result = subprocess.run(
|
|
234
|
-
["curl", "-s", "-L", "--max-time", "10", url],
|
|
235
|
-
capture_output=True, text=True, timeout=15
|
|
236
|
-
)
|
|
237
|
-
return result.stdout[:50000] if result.returncode == 0 else ""
|
|
238
|
-
except Exception:
|
|
239
|
-
return ""
|
|
240
|
-
|
|
241
|
-
def _analyze_structure(self, html: str) -> dict:
|
|
242
|
-
"""Extract content structure from HTML."""
|
|
243
|
-
h1 = re.findall(r'<h1[^>]*>(.*?)</h1>', html, re.IGNORECASE | re.DOTALL)
|
|
244
|
-
h2 = re.findall(r'<h2[^>]*>(.*?)</h2>', html, re.IGNORECASE | re.DOTALL)
|
|
245
|
-
h3 = re.findall(r'<h3[^>]*>(.*?)</h3>', html, re.IGNORECASE | re.DOTALL)
|
|
246
|
-
|
|
247
|
-
# Clean HTML tags from headings
|
|
248
|
-
clean = lambda items: [re.sub(r'<[^>]+>', '', i).strip() for i in items]
|
|
249
|
-
|
|
250
|
-
return {
|
|
251
|
-
"h1": clean(h1),
|
|
252
|
-
"h2": clean(h2)[:20],
|
|
253
|
-
"h3": clean(h3)[:30],
|
|
254
|
-
"word_count_estimate": len(html.split()) // 3 # rough estimate
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
def _extract_topics(self, html: str) -> list:
|
|
258
|
-
"""Extract topic names from HTML content."""
|
|
259
|
-
h2 = re.findall(r'<h2[^>]*>(.*?)</h2>', html, re.IGNORECASE | re.DOTALL)
|
|
260
|
-
return [re.sub(r'<[^>]+>', '', h).strip() for h in h2[:30]]
|
|
261
|
-
|
|
262
|
-
def _extract_key_points(self, sources: list) -> list:
|
|
263
|
-
"""Extract key points from research sources."""
|
|
264
|
-
return [f"Source: {s.get('query', 'unknown')}" for s in sources if s]
|
|
265
|
-
|
|
266
|
-
def _suggest_content_angles(self, topic: str, key_points: list) -> list:
|
|
267
|
-
"""Suggest content angles based on research."""
|
|
268
|
-
article_types = self.config.get("content", {}).get("article_types", [])
|
|
269
|
-
return [
|
|
270
|
-
{
|
|
271
|
-
"type": at["id"],
|
|
272
|
-
"title": at["title_template"].replace("{topic_name}", topic),
|
|
273
|
-
"angle": at.get("seo_intent", "informational")
|
|
274
|
-
}
|
|
275
|
-
for at in article_types
|
|
276
|
-
]
|
|
277
|
-
|
|
278
|
-
def _get_our_content_inventory(self) -> list:
|
|
279
|
-
"""Get list of our published content titles."""
|
|
280
|
-
content_dir = self.project_root / self.config["output"]["content_dir"]
|
|
281
|
-
titles = []
|
|
282
|
-
|
|
283
|
-
if content_dir.exists():
|
|
284
|
-
for md in content_dir.glob("*.md"):
|
|
285
|
-
with open(md, "r", encoding="utf-8") as f:
|
|
286
|
-
content = f.read(500)
|
|
287
|
-
match = re.search(r'title:\s*["\']?(.+?)["\']?\s*$', content, re.MULTILINE)
|
|
288
|
-
if match:
|
|
289
|
-
titles.append(match.group(1).strip())
|
|
290
|
-
else:
|
|
291
|
-
titles.append(md.stem)
|
|
292
|
-
|
|
293
|
-
return titles
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
def main():
|
|
297
|
-
parser = argparse.ArgumentParser(description="Research Engine — Auto-research topics")
|
|
298
|
-
parser.add_argument("--config", required=True, help="Path to config JSON")
|
|
299
|
-
parser.add_argument("--topic", help="Topic to research")
|
|
300
|
-
parser.add_argument("--depth", default="standard", choices=["quick", "standard", "deep"])
|
|
301
|
-
parser.add_argument("--competitor", help="Competitor URL to analyze")
|
|
302
|
-
parser.add_argument("--gap-scan", action="store_true", help="Scan for content gaps")
|
|
303
|
-
args = parser.parse_args()
|
|
304
|
-
|
|
305
|
-
engine = ResearchEngine(args.config)
|
|
306
|
-
|
|
307
|
-
if args.topic:
|
|
308
|
-
print(f"🔬 RESEARCH Mode — Topic: {args.topic}")
|
|
309
|
-
result = engine.research_topic(args.topic, args.depth)
|
|
310
|
-
print(f"\n 📊 Results:")
|
|
311
|
-
print(f" Sources: {len(result['sources'])}")
|
|
312
|
-
print(f" Key points: {len(result['key_points'])}")
|
|
313
|
-
print(f" Content angles: {len(result['content_angles'])}")
|
|
314
|
-
for angle in result["content_angles"]:
|
|
315
|
-
print(f" → {angle['title'][:60]}...")
|
|
316
|
-
|
|
317
|
-
elif args.competitor:
|
|
318
|
-
print(f"🔎 COMPETITOR Analysis")
|
|
319
|
-
result = engine.analyze_competitor(args.competitor)
|
|
320
|
-
print(f"\n Topics covered: {len(result.get('topics_covered', []))}")
|
|
321
|
-
print(f" Missing from us: {len(result.get('missing_from_us', []))}")
|
|
322
|
-
|
|
323
|
-
elif args.gap_scan:
|
|
324
|
-
print(f"🕳️ CONTENT GAP Scan")
|
|
325
|
-
gaps = engine.scan_content_gaps()
|
|
326
|
-
high = [g for g in gaps if g["priority"] == "high"]
|
|
327
|
-
print(f"\n Total gaps: {len(gaps)}")
|
|
328
|
-
print(f" High priority: {len(high)}")
|
|
329
|
-
for g in high[:10]:
|
|
330
|
-
print(f" → {g['topic']} (from: {g['found_at']})")
|
|
331
|
-
|
|
332
|
-
else:
|
|
333
|
-
parser.print_help()
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
if __name__ == "__main__":
|
|
337
|
-
main()
|