@npow/ghostwriter 0.1.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.
Files changed (113) hide show
  1. package/dist/commands/connect.d.ts +30 -0
  2. package/dist/commands/connect.d.ts.map +1 -0
  3. package/dist/commands/connect.js +442 -0
  4. package/dist/commands/connect.js.map +1 -0
  5. package/dist/commands/create/index.d.ts +6 -0
  6. package/dist/commands/create/index.d.ts.map +1 -0
  7. package/dist/commands/create/index.js +89 -0
  8. package/dist/commands/create/index.js.map +1 -0
  9. package/dist/commands/create/prompts/example-article.d.ts +3 -0
  10. package/dist/commands/create/prompts/example-article.d.ts.map +1 -0
  11. package/dist/commands/create/prompts/example-article.js +32 -0
  12. package/dist/commands/create/prompts/example-article.js.map +1 -0
  13. package/dist/commands/create/prompts/intent-parsing.d.ts +3 -0
  14. package/dist/commands/create/prompts/intent-parsing.d.ts.map +1 -0
  15. package/dist/commands/create/prompts/intent-parsing.js +45 -0
  16. package/dist/commands/create/prompts/intent-parsing.js.map +1 -0
  17. package/dist/commands/create/prompts/page-content.d.ts +3 -0
  18. package/dist/commands/create/prompts/page-content.d.ts.map +1 -0
  19. package/dist/commands/create/prompts/page-content.js +56 -0
  20. package/dist/commands/create/prompts/page-content.js.map +1 -0
  21. package/dist/commands/create/prompts/source-discovery.d.ts +3 -0
  22. package/dist/commands/create/prompts/source-discovery.d.ts.map +1 -0
  23. package/dist/commands/create/prompts/source-discovery.js +46 -0
  24. package/dist/commands/create/prompts/source-discovery.js.map +1 -0
  25. package/dist/commands/create/prompts/voice-generation.d.ts +3 -0
  26. package/dist/commands/create/prompts/voice-generation.d.ts.map +1 -0
  27. package/dist/commands/create/prompts/voice-generation.js +43 -0
  28. package/dist/commands/create/prompts/voice-generation.js.map +1 -0
  29. package/dist/commands/create/steps/assemble-config.d.ts +5 -0
  30. package/dist/commands/create/steps/assemble-config.d.ts.map +1 -0
  31. package/dist/commands/create/steps/assemble-config.js +82 -0
  32. package/dist/commands/create/steps/assemble-config.js.map +1 -0
  33. package/dist/commands/create/steps/discover-sources.d.ts +5 -0
  34. package/dist/commands/create/steps/discover-sources.d.ts.map +1 -0
  35. package/dist/commands/create/steps/discover-sources.js +53 -0
  36. package/dist/commands/create/steps/discover-sources.js.map +1 -0
  37. package/dist/commands/create/steps/fingerprint-style.d.ts +5 -0
  38. package/dist/commands/create/steps/fingerprint-style.d.ts.map +1 -0
  39. package/dist/commands/create/steps/fingerprint-style.js +46 -0
  40. package/dist/commands/create/steps/fingerprint-style.js.map +1 -0
  41. package/dist/commands/create/steps/generate-example.d.ts +4 -0
  42. package/dist/commands/create/steps/generate-example.d.ts.map +1 -0
  43. package/dist/commands/create/steps/generate-example.js +23 -0
  44. package/dist/commands/create/steps/generate-example.js.map +1 -0
  45. package/dist/commands/create/steps/generate-voice.d.ts +4 -0
  46. package/dist/commands/create/steps/generate-voice.d.ts.map +1 -0
  47. package/dist/commands/create/steps/generate-voice.js +32 -0
  48. package/dist/commands/create/steps/generate-voice.js.map +1 -0
  49. package/dist/commands/create/steps/parse-intent.d.ts +4 -0
  50. package/dist/commands/create/steps/parse-intent.d.ts.map +1 -0
  51. package/dist/commands/create/steps/parse-intent.js +16 -0
  52. package/dist/commands/create/steps/parse-intent.js.map +1 -0
  53. package/dist/commands/create/steps/resolve-connection.d.ts +5 -0
  54. package/dist/commands/create/steps/resolve-connection.d.ts.map +1 -0
  55. package/dist/commands/create/steps/resolve-connection.js +100 -0
  56. package/dist/commands/create/steps/resolve-connection.js.map +1 -0
  57. package/dist/commands/create/steps/resolve-schedule.d.ts +13 -0
  58. package/dist/commands/create/steps/resolve-schedule.d.ts.map +1 -0
  59. package/dist/commands/create/steps/resolve-schedule.js +70 -0
  60. package/dist/commands/create/steps/resolve-schedule.js.map +1 -0
  61. package/dist/commands/create/steps/setup-site.d.ts +5 -0
  62. package/dist/commands/create/steps/setup-site.d.ts.map +1 -0
  63. package/dist/commands/create/steps/setup-site.js +44 -0
  64. package/dist/commands/create/steps/setup-site.js.map +1 -0
  65. package/dist/commands/create/steps/validate-and-summary.d.ts +4 -0
  66. package/dist/commands/create/steps/validate-and-summary.d.ts.map +1 -0
  67. package/dist/commands/create/steps/validate-and-summary.js +60 -0
  68. package/dist/commands/create/steps/validate-and-summary.js.map +1 -0
  69. package/dist/commands/create/steps/validate-site-access.d.ts +4 -0
  70. package/dist/commands/create/steps/validate-site-access.d.ts.map +1 -0
  71. package/dist/commands/create/steps/validate-site-access.js +25 -0
  72. package/dist/commands/create/steps/validate-site-access.js.map +1 -0
  73. package/dist/commands/create/types.d.ts +72 -0
  74. package/dist/commands/create/types.d.ts.map +1 -0
  75. package/dist/commands/create/types.js +2 -0
  76. package/dist/commands/create/types.js.map +1 -0
  77. package/dist/commands/fingerprint.d.ts +4 -0
  78. package/dist/commands/fingerprint.d.ts.map +1 -0
  79. package/dist/commands/fingerprint.js +31 -0
  80. package/dist/commands/fingerprint.js.map +1 -0
  81. package/dist/commands/init.d.ts +2 -0
  82. package/dist/commands/init.d.ts.map +1 -0
  83. package/dist/commands/init.js +117 -0
  84. package/dist/commands/init.js.map +1 -0
  85. package/dist/commands/list.d.ts +2 -0
  86. package/dist/commands/list.d.ts.map +1 -0
  87. package/dist/commands/list.js +36 -0
  88. package/dist/commands/list.js.map +1 -0
  89. package/dist/commands/logs.d.ts +6 -0
  90. package/dist/commands/logs.d.ts.map +1 -0
  91. package/dist/commands/logs.js +49 -0
  92. package/dist/commands/logs.js.map +1 -0
  93. package/dist/commands/patterns.d.ts +6 -0
  94. package/dist/commands/patterns.d.ts.map +1 -0
  95. package/dist/commands/patterns.js +51 -0
  96. package/dist/commands/patterns.js.map +1 -0
  97. package/dist/commands/run.d.ts +6 -0
  98. package/dist/commands/run.d.ts.map +1 -0
  99. package/dist/commands/run.js +139 -0
  100. package/dist/commands/run.js.map +1 -0
  101. package/dist/commands/status.d.ts +2 -0
  102. package/dist/commands/status.d.ts.map +1 -0
  103. package/dist/commands/status.js +66 -0
  104. package/dist/commands/status.js.map +1 -0
  105. package/dist/commands/validate.d.ts +2 -0
  106. package/dist/commands/validate.d.ts.map +1 -0
  107. package/dist/commands/validate.js +83 -0
  108. package/dist/commands/validate.js.map +1 -0
  109. package/dist/index.d.ts +3 -0
  110. package/dist/index.d.ts.map +1 -0
  111. package/dist/index.js +76 -0
  112. package/dist/index.js.map +1 -0
  113. package/package.json +38 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"example-article.js","sourceRoot":"","sources":["../../../../src/commands/create/prompts/example-article.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,sBAAsB,GAAG;;;;;;;;;;;;;kEAa4B,CAAC;AAEnE,MAAM,UAAU,yBAAyB,CACvC,WAAmB,EACnB,WAAmB,EACnB,UAAkB,EAClB,SAAiB,EACjB,YAAoB,EACpB,UAAoB,EACpB,cAAwB,EACxB,cAAwB,EACxB,IAAY;IAEZ,OAAO,kBAAkB,WAAW,SAAS,WAAW;;eAE3C,UAAU;;SAEhB,SAAS;EAChB,YAAY;;gCAEkB,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM;wBACvC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM;QACnD,IAAI;;;EAGV,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;;qDAEG,CAAC;AACtD,CAAC"}
@@ -0,0 +1,3 @@
1
+ export declare const INTENT_PARSING_SYSTEM = "You are a configuration parser for an autonomous blogging platform.\nGiven a natural language description of a blog channel, extract structured information.\n\nReturn ONLY valid JSON matching this schema (no markdown, no explanation):\n\n{\n \"channelId\": \"kebab-case-id\",\n \"channelName\": \"Human Readable Name\",\n \"contentType\": \"article|listicle|recap|analysis|tutorial|recipe|review|roundup\",\n \"topic\": {\n \"domain\": \"topic-domain\",\n \"focus\": \"One sentence describing the content focus\",\n \"keywords\": [\"keyword1\", \"keyword2\", \"keyword3\"],\n \"constraints\": \"Optional safety constraints or content restrictions\"\n },\n \"toneDescription\": \"Description of the desired tone/style\",\n \"styleReferences\": [\"URL or writer/publication name for style reference\"],\n \"publishPlatform\": \"wordpress-com\",\n \"siteUrl\": \"site URL if mentioned\",\n \"connectionId\": \"connection ID if mentioned\",\n \"schedule\": {\n \"frequency\": \"daily|weekly|biweekly|monthly\",\n \"dayOfWeek\": \"optional day like Monday, Saturday\",\n \"time\": \"optional time like 9am, 10:00\",\n \"timezone\": \"optional timezone like America/New_York\"\n },\n \"targetWordCount\": null\n}\n\nRules:\n- channelId: derive from the topic, 2-4 words, kebab-case (e.g. \"weekly-stock-recap\")\n- contentType: pick the best match from the enum values\n- If user mentions a writer by name (e.g. \"like Matt Levine\"), put their name in styleReferences\n- If user provides a URL, put it in styleReferences\n- publishPlatform: \"wordpress-com\" for WordPress.com sites\n- siteUrl: extract the full domain if mentioned (e.g. \"marketpulse.wordpress.com\")\n- schedule: infer from frequency words. Default to weekly if a recap/roundup, daily otherwise\n- constraints: add \"Never give financial advice\" for finance topics, \"Recipes must include measurements\" for recipe topics, etc.\n- targetWordCount: null unless explicitly mentioned. The system will default to 1500.";
2
+ export declare function buildIntentParsingPrompt(description: string): string;
3
+ //# sourceMappingURL=intent-parsing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"intent-parsing.d.ts","sourceRoot":"","sources":["../../../../src/commands/create/prompts/intent-parsing.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,qBAAqB,q+DAsCoD,CAAC;AAEvF,wBAAgB,wBAAwB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAIpE"}
@@ -0,0 +1,45 @@
1
+ export const INTENT_PARSING_SYSTEM = `You are a configuration parser for an autonomous blogging platform.
2
+ Given a natural language description of a blog channel, extract structured information.
3
+
4
+ Return ONLY valid JSON matching this schema (no markdown, no explanation):
5
+
6
+ {
7
+ "channelId": "kebab-case-id",
8
+ "channelName": "Human Readable Name",
9
+ "contentType": "article|listicle|recap|analysis|tutorial|recipe|review|roundup",
10
+ "topic": {
11
+ "domain": "topic-domain",
12
+ "focus": "One sentence describing the content focus",
13
+ "keywords": ["keyword1", "keyword2", "keyword3"],
14
+ "constraints": "Optional safety constraints or content restrictions"
15
+ },
16
+ "toneDescription": "Description of the desired tone/style",
17
+ "styleReferences": ["URL or writer/publication name for style reference"],
18
+ "publishPlatform": "wordpress-com",
19
+ "siteUrl": "site URL if mentioned",
20
+ "connectionId": "connection ID if mentioned",
21
+ "schedule": {
22
+ "frequency": "daily|weekly|biweekly|monthly",
23
+ "dayOfWeek": "optional day like Monday, Saturday",
24
+ "time": "optional time like 9am, 10:00",
25
+ "timezone": "optional timezone like America/New_York"
26
+ },
27
+ "targetWordCount": null
28
+ }
29
+
30
+ Rules:
31
+ - channelId: derive from the topic, 2-4 words, kebab-case (e.g. "weekly-stock-recap")
32
+ - contentType: pick the best match from the enum values
33
+ - If user mentions a writer by name (e.g. "like Matt Levine"), put their name in styleReferences
34
+ - If user provides a URL, put it in styleReferences
35
+ - publishPlatform: "wordpress-com" for WordPress.com sites
36
+ - siteUrl: extract the full domain if mentioned (e.g. "marketpulse.wordpress.com")
37
+ - schedule: infer from frequency words. Default to weekly if a recap/roundup, daily otherwise
38
+ - constraints: add "Never give financial advice" for finance topics, "Recipes must include measurements" for recipe topics, etc.
39
+ - targetWordCount: null unless explicitly mentioned. The system will default to 1500.`;
40
+ export function buildIntentParsingPrompt(description) {
41
+ return `Parse this channel description into structured configuration:
42
+
43
+ "${description}"`;
44
+ }
45
+ //# sourceMappingURL=intent-parsing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"intent-parsing.js","sourceRoot":"","sources":["../../../../src/commands/create/prompts/intent-parsing.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,qBAAqB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;sFAsCiD,CAAC;AAEvF,MAAM,UAAU,wBAAwB,CAAC,WAAmB;IAC1D,OAAO;;GAEN,WAAW,GAAG,CAAC;AAClB,CAAC"}
@@ -0,0 +1,3 @@
1
+ export declare const PAGE_CONTENT_SYSTEM = "You are a web content writer for a blog.\nGenerate HTML content for site pages (About, Contact, etc.).\nWrite in the voice/tone described. Keep it concise and authentic.\n\nReturn ONLY valid JSON matching this schema (no markdown, no explanation):\n\n{\n \"categories\": [\n { \"name\": \"Category Name\", \"slug\": \"category-slug\", \"description\": \"Brief description\" }\n ],\n \"tags\": [\n { \"name\": \"Tag Name\", \"slug\": \"tag-slug\" }\n ],\n \"pages\": [\n {\n \"title\": \"Page Title\",\n \"slug\": \"page-slug\",\n \"content\": \"<p>HTML content here</p>\",\n \"status\": \"publish\"\n }\n ],\n \"siteIdentity\": {\n \"title\": \"Site Title\",\n \"tagline\": \"Site tagline\"\n },\n \"menus\": [\n {\n \"name\": \"Main Navigation\",\n \"location\": \"primary\",\n \"items\": [\n { \"title\": \"Home\", \"type\": \"custom\", \"url\": \"/\" },\n { \"title\": \"About\", \"type\": \"page\", \"objectSlug\": \"about\" }\n ]\n }\n ]\n}\n\nRules:\n- Generate 3-5 relevant categories based on the topic\n- Generate 5-8 relevant tags\n- Always create an \"About\" page and a \"Contact\" page\n- About page should be written in the blog's voice/tone, 150-250 words\n- Contact page should include a simple contact message\n- Site title should be the channel name\n- Tagline should be catchy and relevant (under 10 words)\n- Primary menu should link to Home, About, and the main categories";
2
+ export declare function buildPageContentPrompt(channelName: string, topicDomain: string, topicFocus: string, toneDescription: string, voicePersona: string): string;
3
+ //# sourceMappingURL=page-content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page-content.d.ts","sourceRoot":"","sources":["../../../../src/commands/create/prompts/page-content.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,mBAAmB,88CA6CmC,CAAC;AAEpE,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAClB,eAAe,EAAE,MAAM,EACvB,YAAY,EAAE,MAAM,GACnB,MAAM,CAQR"}
@@ -0,0 +1,56 @@
1
+ export const PAGE_CONTENT_SYSTEM = `You are a web content writer for a blog.
2
+ Generate HTML content for site pages (About, Contact, etc.).
3
+ Write in the voice/tone described. Keep it concise and authentic.
4
+
5
+ Return ONLY valid JSON matching this schema (no markdown, no explanation):
6
+
7
+ {
8
+ "categories": [
9
+ { "name": "Category Name", "slug": "category-slug", "description": "Brief description" }
10
+ ],
11
+ "tags": [
12
+ { "name": "Tag Name", "slug": "tag-slug" }
13
+ ],
14
+ "pages": [
15
+ {
16
+ "title": "Page Title",
17
+ "slug": "page-slug",
18
+ "content": "<p>HTML content here</p>",
19
+ "status": "publish"
20
+ }
21
+ ],
22
+ "siteIdentity": {
23
+ "title": "Site Title",
24
+ "tagline": "Site tagline"
25
+ },
26
+ "menus": [
27
+ {
28
+ "name": "Main Navigation",
29
+ "location": "primary",
30
+ "items": [
31
+ { "title": "Home", "type": "custom", "url": "/" },
32
+ { "title": "About", "type": "page", "objectSlug": "about" }
33
+ ]
34
+ }
35
+ ]
36
+ }
37
+
38
+ Rules:
39
+ - Generate 3-5 relevant categories based on the topic
40
+ - Generate 5-8 relevant tags
41
+ - Always create an "About" page and a "Contact" page
42
+ - About page should be written in the blog's voice/tone, 150-250 words
43
+ - Contact page should include a simple contact message
44
+ - Site title should be the channel name
45
+ - Tagline should be catchy and relevant (under 10 words)
46
+ - Primary menu should link to Home, About, and the main categories`;
47
+ export function buildPageContentPrompt(channelName, topicDomain, topicFocus, toneDescription, voicePersona) {
48
+ return `Generate site setup content for: ${channelName}
49
+
50
+ Topic: ${topicDomain} — ${topicFocus}
51
+ Tone: ${toneDescription}
52
+ Voice persona: ${voicePersona}
53
+
54
+ Create categories, tags, About/Contact pages, site identity, and navigation menu.`;
55
+ }
56
+ //# sourceMappingURL=page-content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page-content.js","sourceRoot":"","sources":["../../../../src/commands/create/prompts/page-content.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,mBAAmB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mEA6CgC,CAAC;AAEpE,MAAM,UAAU,sBAAsB,CACpC,WAAmB,EACnB,WAAmB,EACnB,UAAkB,EAClB,eAAuB,EACvB,YAAoB;IAEpB,OAAO,oCAAoC,WAAW;;SAE/C,WAAW,MAAM,UAAU;QAC5B,eAAe;iBACN,YAAY;;kFAEqD,CAAC;AACnF,CAAC"}
@@ -0,0 +1,3 @@
1
+ export declare const SOURCE_DISCOVERY_SYSTEM = "You are a data source researcher for an AI blogging platform.\nGiven a topic domain and keywords, suggest real RSS feeds and API data sources\nthat would provide fresh content for writing about this topic.\n\nReturn ONLY valid JSON matching this schema (no markdown, no explanation):\n\n{\n \"sources\": [\n {\n \"type\": \"rss\",\n \"url\": \"https://example.com/feed\",\n \"name\": \"Source Name\",\n \"description\": \"What this source provides\"\n },\n {\n \"type\": \"api\",\n \"url\": \"https://api.example.com/v1/endpoint\",\n \"name\": \"API Name\",\n \"description\": \"What data this API provides\",\n \"requiresApiKey\": true,\n \"apiKeyEnvVar\": \"EXAMPLE_API_KEY\"\n }\n ]\n}\n\nRules:\n- Suggest 3-6 sources total, prioritizing RSS feeds (they're free and easy)\n- Only suggest REAL, working RSS feed URLs that you are confident exist\n- Common reliable RSS feeds include:\n - Major news: Reuters, AP News, BBC, NPR\n - Tech: Hacker News, TechCrunch, The Verge, Ars Technica\n - Finance: Yahoo Finance, Bloomberg, MarketWatch, CNBC\n - Science: Nature, Science Daily, Phys.org\n - Food/recipes: Serious Eats, Bon Appetit, Food52\n- For RSS, use the actual feed URL (usually /feed, /rss, /feed.xml)\n- For APIs, flag if they require API keys and what env var to set\n- Prefer diverse sources (don't just suggest 5 feeds from the same publisher)";
2
+ export declare function buildSourceDiscoveryPrompt(domain: string, keywords: string[], focus: string): string;
3
+ //# sourceMappingURL=source-discovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source-discovery.d.ts","sourceRoot":"","sources":["../../../../src/commands/create/prompts/source-discovery.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,uBAAuB,u5CAoC0C,CAAC;AAE/E,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAAE,EAClB,KAAK,EAAE,MAAM,GACZ,MAAM,CAOR"}
@@ -0,0 +1,46 @@
1
+ export const SOURCE_DISCOVERY_SYSTEM = `You are a data source researcher for an AI blogging platform.
2
+ Given a topic domain and keywords, suggest real RSS feeds and API data sources
3
+ that would provide fresh content for writing about this topic.
4
+
5
+ Return ONLY valid JSON matching this schema (no markdown, no explanation):
6
+
7
+ {
8
+ "sources": [
9
+ {
10
+ "type": "rss",
11
+ "url": "https://example.com/feed",
12
+ "name": "Source Name",
13
+ "description": "What this source provides"
14
+ },
15
+ {
16
+ "type": "api",
17
+ "url": "https://api.example.com/v1/endpoint",
18
+ "name": "API Name",
19
+ "description": "What data this API provides",
20
+ "requiresApiKey": true,
21
+ "apiKeyEnvVar": "EXAMPLE_API_KEY"
22
+ }
23
+ ]
24
+ }
25
+
26
+ Rules:
27
+ - Suggest 3-6 sources total, prioritizing RSS feeds (they're free and easy)
28
+ - Only suggest REAL, working RSS feed URLs that you are confident exist
29
+ - Common reliable RSS feeds include:
30
+ - Major news: Reuters, AP News, BBC, NPR
31
+ - Tech: Hacker News, TechCrunch, The Verge, Ars Technica
32
+ - Finance: Yahoo Finance, Bloomberg, MarketWatch, CNBC
33
+ - Science: Nature, Science Daily, Phys.org
34
+ - Food/recipes: Serious Eats, Bon Appetit, Food52
35
+ - For RSS, use the actual feed URL (usually /feed, /rss, /feed.xml)
36
+ - For APIs, flag if they require API keys and what env var to set
37
+ - Prefer diverse sources (don't just suggest 5 feeds from the same publisher)`;
38
+ export function buildSourceDiscoveryPrompt(domain, keywords, focus) {
39
+ return `Find data sources for a blog about: ${domain}
40
+
41
+ Focus: ${focus}
42
+ Keywords: ${keywords.join(", ")}
43
+
44
+ Suggest real RSS feeds and APIs that would provide relevant content.`;
45
+ }
46
+ //# sourceMappingURL=source-discovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source-discovery.js","sourceRoot":"","sources":["../../../../src/commands/create/prompts/source-discovery.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,uBAAuB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;8EAoCuC,CAAC;AAE/E,MAAM,UAAU,0BAA0B,CACxC,MAAc,EACd,QAAkB,EAClB,KAAa;IAEb,OAAO,uCAAuC,MAAM;;SAE7C,KAAK;YACF,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;;qEAEsC,CAAC;AACtE,CAAC"}
@@ -0,0 +1,3 @@
1
+ export declare const VOICE_GENERATION_SYSTEM = "You are a character designer for an AI blogging platform.\nGiven a style profile (writing metrics), a tone description, and a topic domain,\ncreate a compelling writer persona.\n\nReturn ONLY valid JSON matching this schema (no markdown, no explanation):\n\n{\n \"name\": \"Character Name (first name + optional descriptor like 'the Market Guy')\",\n \"persona\": \"2-3 sentence description of who this person is and how they write\",\n \"age\": 30,\n \"backstory\": \"2-3 sentences about their background that explains their expertise\",\n \"opinions\": [\"Strong opinion 1\", \"Strong opinion 2\", \"Strong opinion 3\"],\n \"verbalTics\": [\"Catchphrase 1\", \"Catchphrase 2\"],\n \"vocabulary\": {\n \"preferred\": [\"word1\", \"word2\", \"word3\"],\n \"forbidden\": [\"forbidden1\", \"forbidden2\"]\n },\n \"tone\": \"conversational|professional|academic|casual|authoritative|humorous|warm\"\n}\n\nRules:\n- The persona should feel like a real person, not a generic AI\n- Opinions should be specific and relevant to the topic domain\n- Verbal tics should be natural conversational phrases, not clich\u00E9s\n- Preferred vocabulary should match the domain expertise\n- Forbidden words MUST include all AI-typical phrases from the blacklist provided\n- Tone must be one of: conversational, professional, academic, casual, authoritative, humorous, warm\n- Pick a tone that matches the tone description most closely\n- Age should be believable for someone with the described expertise";
2
+ export declare function buildVoiceGenerationPrompt(toneDescription: string, topicDomain: string, styleProfileText: string, forbiddenPhrases: string[]): string;
3
+ //# sourceMappingURL=voice-generation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voice-generation.d.ts","sourceRoot":"","sources":["../../../../src/commands/create/prompts/voice-generation.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,uBAAuB,g+CA4BgC,CAAC;AAErE,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,MAAM,EACvB,WAAW,EAAE,MAAM,EACnB,gBAAgB,EAAE,MAAM,EACxB,gBAAgB,EAAE,MAAM,EAAE,GACzB,MAAM,CAYR"}
@@ -0,0 +1,43 @@
1
+ export const VOICE_GENERATION_SYSTEM = `You are a character designer for an AI blogging platform.
2
+ Given a style profile (writing metrics), a tone description, and a topic domain,
3
+ create a compelling writer persona.
4
+
5
+ Return ONLY valid JSON matching this schema (no markdown, no explanation):
6
+
7
+ {
8
+ "name": "Character Name (first name + optional descriptor like 'the Market Guy')",
9
+ "persona": "2-3 sentence description of who this person is and how they write",
10
+ "age": 30,
11
+ "backstory": "2-3 sentences about their background that explains their expertise",
12
+ "opinions": ["Strong opinion 1", "Strong opinion 2", "Strong opinion 3"],
13
+ "verbalTics": ["Catchphrase 1", "Catchphrase 2"],
14
+ "vocabulary": {
15
+ "preferred": ["word1", "word2", "word3"],
16
+ "forbidden": ["forbidden1", "forbidden2"]
17
+ },
18
+ "tone": "conversational|professional|academic|casual|authoritative|humorous|warm"
19
+ }
20
+
21
+ Rules:
22
+ - The persona should feel like a real person, not a generic AI
23
+ - Opinions should be specific and relevant to the topic domain
24
+ - Verbal tics should be natural conversational phrases, not clichés
25
+ - Preferred vocabulary should match the domain expertise
26
+ - Forbidden words MUST include all AI-typical phrases from the blacklist provided
27
+ - Tone must be one of: conversational, professional, academic, casual, authoritative, humorous, warm
28
+ - Pick a tone that matches the tone description most closely
29
+ - Age should be believable for someone with the described expertise`;
30
+ export function buildVoiceGenerationPrompt(toneDescription, topicDomain, styleProfileText, forbiddenPhrases) {
31
+ return `Create a writer persona for a ${topicDomain} blog.
32
+
33
+ Desired tone: ${toneDescription}
34
+
35
+ Style profile of reference writing:
36
+ ${styleProfileText}
37
+
38
+ The "forbidden" vocabulary list MUST include at least these AI-typical phrases:
39
+ ${forbiddenPhrases.map((p) => `- "${p}"`).join("\n")}
40
+
41
+ Generate a character that would naturally write in this style about ${topicDomain}.`;
42
+ }
43
+ //# sourceMappingURL=voice-generation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voice-generation.js","sourceRoot":"","sources":["../../../../src/commands/create/prompts/voice-generation.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,uBAAuB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;oEA4B6B,CAAC;AAErE,MAAM,UAAU,0BAA0B,CACxC,eAAuB,EACvB,WAAmB,EACnB,gBAAwB,EACxB,gBAA0B;IAE1B,OAAO,iCAAiC,WAAW;;gBAErC,eAAe;;;EAG7B,gBAAgB;;;EAGhB,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;;sEAEkB,WAAW,GAAG,CAAC;AACrF,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { Ora } from "ora";
2
+ import { type ChannelConfig } from "@ghostwriter/core";
3
+ import type { CreateContext } from "../types.js";
4
+ export declare function assembleConfig(ctx: CreateContext, spinner: Ora): Promise<ChannelConfig>;
5
+ //# sourceMappingURL=assemble-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assemble-config.d.ts","sourceRoot":"","sources":["../../../../src/commands/create/steps/assemble-config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE/B,OAAO,EAAuC,KAAK,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAC5F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,wBAAsB,cAAc,CAClC,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,GAAG,GACX,OAAO,CAAC,aAAa,CAAC,CAsFxB"}
@@ -0,0 +1,82 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { stringify as yamlStringify } from "yaml";
4
+ import { getChannelsDir, ChannelConfigSchema } from "@ghostwriter/core";
5
+ export async function assembleConfig(ctx, spinner) {
6
+ spinner.start("Assembling channel config...");
7
+ const intent = ctx.intent;
8
+ const voice = ctx.voice;
9
+ const schedule = ctx.schedule;
10
+ const siteUrl = intent.siteUrl
11
+ ? `https://${intent.siteUrl.replace(/^https?:\/\//, "")}`
12
+ : ctx.connection?.url ?? "https://example.wordpress.com";
13
+ const config = ChannelConfigSchema.parse({
14
+ id: intent.channelId,
15
+ name: intent.channelName,
16
+ contentType: intent.contentType,
17
+ topic: {
18
+ domain: intent.topic.domain,
19
+ focus: intent.topic.focus,
20
+ keywords: intent.topic.keywords,
21
+ constraints: intent.topic.constraints,
22
+ },
23
+ dataSources: ctx.dataSources,
24
+ voice: {
25
+ name: voice.name,
26
+ persona: voice.persona,
27
+ age: voice.age,
28
+ backstory: voice.backstory,
29
+ opinions: voice.opinions,
30
+ verbalTics: voice.verbalTics,
31
+ exampleContent: ["./examples/sample-1.md"],
32
+ vocabulary: voice.vocabulary,
33
+ tone: voice.tone,
34
+ },
35
+ publishTargets: [
36
+ {
37
+ platform: "wordpress",
38
+ id: ctx.connection?.id,
39
+ url: siteUrl,
40
+ },
41
+ ],
42
+ schedule: {
43
+ cron: schedule.cron,
44
+ timezone: schedule.timezone,
45
+ enabled: true,
46
+ },
47
+ qualityGate: {
48
+ minScores: {
49
+ structure: 7,
50
+ readability: 7,
51
+ voiceMatch: 7,
52
+ factualAccuracy: 7,
53
+ sourceCoverage: 7,
54
+ hookStrength: 7,
55
+ engagementPotential: 7,
56
+ naturalness: 7,
57
+ perplexityVariance: 7,
58
+ },
59
+ maxRevisions: 3,
60
+ },
61
+ targetWordCount: intent.targetWordCount ?? 1500,
62
+ batchApi: false,
63
+ });
64
+ if (ctx.options.dryRun) {
65
+ spinner.succeed("Config assembled (dry run — not writing files)");
66
+ return config;
67
+ }
68
+ // Write files
69
+ const channelsDir = getChannelsDir();
70
+ const channelDir = join(channelsDir, intent.channelId);
71
+ const examplesDir = join(channelDir, "examples");
72
+ await mkdir(examplesDir, { recursive: true });
73
+ const yamlContent = yamlStringify(config, {
74
+ lineWidth: 120,
75
+ defaultStringType: "QUOTE_DOUBLE",
76
+ defaultKeyType: "PLAIN",
77
+ });
78
+ await writeFile(join(channelDir, "config.yml"), yamlContent, "utf-8");
79
+ spinner.succeed(`Config written to channels/${intent.channelId}/config.yml`);
80
+ return config;
81
+ }
82
+ //# sourceMappingURL=assemble-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assemble-config.js","sourceRoot":"","sources":["../../../../src/commands/create/steps/assemble-config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import type { Ora } from "ora";
2
+ import type { DataSource } from "@ghostwriter/core";
3
+ import type { CreateContext } from "../types.js";
4
+ export declare function discoverSources(ctx: CreateContext, spinner: Ora): Promise<DataSource[]>;
5
+ //# sourceMappingURL=discover-sources.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discover-sources.d.ts","sourceRoot":"","sources":["../../../../src/commands/create/steps/discover-sources.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAC/B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAGpD,OAAO,KAAK,EAAE,aAAa,EAAqB,MAAM,aAAa,CAAC;AAMpE,wBAAsB,eAAe,CACnC,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,GAAG,GACX,OAAO,CAAC,UAAU,EAAE,CAAC,CAiEvB"}
@@ -0,0 +1,53 @@
1
+ import { callLlmJson } from "@ghostwriter/content-pipeline";
2
+ import { validateRssFeed } from "@ghostwriter/data-ingestion";
3
+ import { SOURCE_DISCOVERY_SYSTEM, buildSourceDiscoveryPrompt, } from "../prompts/source-discovery.js";
4
+ export async function discoverSources(ctx, spinner) {
5
+ spinner.start("Discovering data sources...");
6
+ const intent = ctx.intent;
7
+ const { data, cost } = await callLlmJson("sonnet", SOURCE_DISCOVERY_SYSTEM, buildSourceDiscoveryPrompt(intent.topic.domain, intent.topic.keywords, intent.topic.focus), { temperature: 0.3 });
8
+ ctx.totalCost += cost;
9
+ // Validate RSS feeds
10
+ const validSources = [];
11
+ let validated = 0;
12
+ let failed = 0;
13
+ for (const source of data.sources) {
14
+ if (source.type === "rss") {
15
+ spinner.text = `Validating RSS: ${source.name}...`;
16
+ const result = await validateRssFeed(source.url);
17
+ if (result.valid) {
18
+ validSources.push({
19
+ type: "rss",
20
+ url: source.url,
21
+ maxItems: 10,
22
+ });
23
+ validated++;
24
+ }
25
+ else {
26
+ failed++;
27
+ }
28
+ }
29
+ else if (source.type === "api") {
30
+ // API sources are included but may need API keys
31
+ validSources.push({
32
+ type: "api",
33
+ provider: source.name.toLowerCase().replace(/\s+/g, "-"),
34
+ endpoint: source.url,
35
+ headers: source.apiKeyEnvVar
36
+ ? { Authorization: `Bearer \${${source.apiKeyEnvVar}}` }
37
+ : undefined,
38
+ });
39
+ }
40
+ }
41
+ if (validSources.length === 0) {
42
+ // Fallback: create a generic RSS source from the domain
43
+ spinner.warn("No sources validated — adding a placeholder RSS source");
44
+ validSources.push({
45
+ type: "rss",
46
+ url: `https://news.google.com/rss/search?q=${encodeURIComponent(intent.topic.keywords[0] ?? intent.topic.domain)}`,
47
+ maxItems: 10,
48
+ });
49
+ }
50
+ spinner.succeed(`Found ${validSources.length} source(s) (${validated} RSS validated, ${failed} failed)`);
51
+ return validSources;
52
+ }
53
+ //# sourceMappingURL=discover-sources.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discover-sources.js","sourceRoot":"","sources":["../../../../src/commands/create/steps/discover-sources.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAE9D,OAAO,EACL,uBAAuB,EACvB,0BAA0B,GAC3B,MAAM,gCAAgC,CAAC;AAExC,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,GAAkB,EAClB,OAAY;IAEZ,OAAO,CAAC,KAAK,CAAC,6BAA6B,CAAC,CAAC;IAE7C,MAAM,MAAM,GAAG,GAAG,CAAC,MAAO,CAAC;IAE3B,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,MAAM,WAAW,CACtC,QAAQ,EACR,uBAAuB,EACvB,0BAA0B,CACxB,MAAM,CAAC,KAAK,CAAC,MAAM,EACnB,MAAM,CAAC,KAAK,CAAC,QAAQ,EACrB,MAAM,CAAC,KAAK,CAAC,KAAK,CACnB,EACD,EAAE,WAAW,EAAE,GAAG,EAAE,CACrB,CAAC;IAEF,GAAG,CAAC,SAAS,IAAI,IAAI,CAAC;IAEtB,qBAAqB;IACrB,MAAM,YAAY,GAAiB,EAAE,CAAC;IACtC,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,IAAI,MAAM,GAAG,CAAC,CAAC;IAEf,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QAClC,IAAI,MAAM,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;YAC1B,OAAO,CAAC,IAAI,GAAG,mBAAmB,MAAM,CAAC,IAAI,KAAK,CAAC;YACnD,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACjD,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;gBACjB,YAAY,CAAC,IAAI,CAAC;oBAChB,IAAI,EAAE,KAAK;oBACX,GAAG,EAAE,MAAM,CAAC,GAAG;oBACf,QAAQ,EAAE,EAAE;iBACb,CAAC,CAAC;gBACH,SAAS,EAAE,CAAC;YACd,CAAC;iBAAM,CAAC;gBACN,MAAM,EAAE,CAAC;YACX,CAAC;QACH,CAAC;aAAM,IAAI,MAAM,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;YACjC,iDAAiD;YACjD,YAAY,CAAC,IAAI,CAAC;gBAChB,IAAI,EAAE,KAAK;gBACX,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;gBACxD,QAAQ,EAAE,MAAM,CAAC,GAAG;gBACpB,OAAO,EAAE,MAAM,CAAC,YAAY;oBAC1B,CAAC,CAAC,EAAE,aAAa,EAAE,aAAa,MAAM,CAAC,YAAY,GAAG,EAAE;oBACxD,CAAC,CAAC,SAAS;aACd,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9B,wDAAwD;QACxD,OAAO,CAAC,IAAI,CAAC,wDAAwD,CAAC,CAAC;QACvE,YAAY,CAAC,IAAI,CAAC;YAChB,IAAI,EAAE,KAAK;YACX,GAAG,EAAE,wCAAwC,kBAAkB,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE;YAClH,QAAQ,EAAE,EAAE;SACb,CAAC,CAAC;IACL,CAAC;IAED,OAAO,CAAC,OAAO,CACb,SAAS,YAAY,CAAC,MAAM,eAAe,SAAS,mBAAmB,MAAM,UAAU,CACxF,CAAC;IAEF,OAAO,YAAY,CAAC;AACtB,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { Ora } from "ora";
2
+ import { type StyleProfile } from "@ghostwriter/style-fingerprint";
3
+ import type { CreateContext } from "../types.js";
4
+ export declare function fingerprintStyle(ctx: CreateContext, spinner: Ora): Promise<StyleProfile | undefined>;
5
+ //# sourceMappingURL=fingerprint-style.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fingerprint-style.d.ts","sourceRoot":"","sources":["../../../../src/commands/create/steps/fingerprint-style.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAC/B,OAAO,EAIL,KAAK,YAAY,EAClB,MAAM,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,GAAG,GACX,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAsCnC"}
@@ -0,0 +1,46 @@
1
+ import { analyzeUrl, mergeStyleProfiles, formatStyleProfile, } from "@ghostwriter/style-fingerprint";
2
+ export async function fingerprintStyle(ctx, spinner) {
3
+ const refs = ctx.intent?.styleReferences ?? [];
4
+ if (refs.length === 0) {
5
+ spinner.info("No style references — will generate voice from description only");
6
+ return undefined;
7
+ }
8
+ spinner.start(`Fingerprinting style from ${refs.length} reference(s)...`);
9
+ const profiles = [];
10
+ for (const ref of refs) {
11
+ // Check if it's a URL
12
+ if (isUrl(ref)) {
13
+ try {
14
+ const profile = await analyzeUrl(ref);
15
+ profiles.push(profile);
16
+ spinner.text = `Fingerprinted: ${ref} (${profile.sampleCount} samples)`;
17
+ }
18
+ catch (err) {
19
+ spinner.text = `Could not analyze ${ref}: ${err instanceof Error ? err.message : String(err)}`;
20
+ }
21
+ }
22
+ else {
23
+ // It's a name reference (e.g. "Matt Levine") — skip URL resolution
24
+ // The voice generation step will use the name as context
25
+ spinner.text = `Style reference "${ref}" is a name — will use for voice generation`;
26
+ }
27
+ }
28
+ if (profiles.length === 0) {
29
+ spinner.info("No URLs could be fingerprinted — will rely on description for voice");
30
+ return undefined;
31
+ }
32
+ const merged = profiles.length === 1 ? profiles[0] : mergeStyleProfiles(profiles);
33
+ const summary = formatStyleProfile(merged, "compact");
34
+ spinner.succeed(`Style fingerprint: ${summary}`);
35
+ return merged;
36
+ }
37
+ function isUrl(str) {
38
+ try {
39
+ const url = new URL(str.startsWith("http") ? str : `https://${str}`);
40
+ return url.hostname.includes(".");
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
46
+ //# sourceMappingURL=fingerprint-style.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fingerprint-style.js","sourceRoot":"","sources":["../../../../src/commands/create/steps/fingerprint-style.ts"],"names":[],"mappings":"AACA,OAAO,EACL,UAAU,EACV,kBAAkB,EAClB,kBAAkB,GAEnB,MAAM,gCAAgC,CAAC;AAGxC,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,GAAkB,EAClB,OAAY;IAEZ,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,EAAE,eAAe,IAAI,EAAE,CAAC;IAC/C,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO,CAAC,IAAI,CAAC,iEAAiE,CAAC,CAAC;QAChF,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,OAAO,CAAC,KAAK,CAAC,6BAA6B,IAAI,CAAC,MAAM,kBAAkB,CAAC,CAAC;IAE1E,MAAM,QAAQ,GAAmB,EAAE,CAAC;IAEpC,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,sBAAsB;QACtB,IAAI,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;YACf,IAAI,CAAC;gBACH,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,GAAG,CAAC,CAAC;gBACtC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBACvB,OAAO,CAAC,IAAI,GAAG,kBAAkB,GAAG,KAAK,OAAO,CAAC,WAAW,WAAW,CAAC;YAC1E,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,OAAO,CAAC,IAAI,GAAG,qBAAqB,GAAG,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;YACjG,CAAC;QACH,CAAC;aAAM,CAAC;YACN,mEAAmE;YACnE,yDAAyD;YACzD,OAAO,CAAC,IAAI,GAAG,oBAAoB,GAAG,6CAA6C,CAAC;QACtF,CAAC;IACH,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,CAAC,IAAI,CAAC,qEAAqE,CAAC,CAAC;QACpF,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IAClF,MAAM,OAAO,GAAG,kBAAkB,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACtD,OAAO,CAAC,OAAO,CAAC,sBAAsB,OAAO,EAAE,CAAC,CAAC;IAEjD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,KAAK,CAAC,GAAW;IACxB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,GAAG,EAAE,CAAC,CAAC;QACrE,OAAO,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;IACpC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { Ora } from "ora";
2
+ import type { CreateContext } from "../types.js";
3
+ export declare function generateExample(ctx: CreateContext, spinner: Ora): Promise<void>;
4
+ //# sourceMappingURL=generate-example.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-example.d.ts","sourceRoot":"","sources":["../../../../src/commands/create/steps/generate-example.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAG/B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAMjD,wBAAsB,eAAe,CACnC,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,GAAG,GACX,OAAO,CAAC,IAAI,CAAC,CA8Cf"}
@@ -0,0 +1,23 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { callLlm } from "@ghostwriter/content-pipeline";
4
+ import { getChannelsDir } from "@ghostwriter/core";
5
+ import { EXAMPLE_ARTICLE_SYSTEM, buildExampleArticlePrompt, } from "../prompts/example-article.js";
6
+ export async function generateExample(ctx, spinner) {
7
+ spinner.start("Generating sample article...");
8
+ const intent = ctx.intent;
9
+ const voice = ctx.voice;
10
+ const result = await callLlm("sonnet", EXAMPLE_ARTICLE_SYSTEM, buildExampleArticlePrompt(intent.channelName, intent.contentType, intent.topic.focus, voice.name, voice.persona, voice.verbalTics ?? [], voice.vocabulary.preferred, voice.vocabulary.forbidden.slice(0, 30), // Don't overwhelm the prompt
11
+ voice.tone), { temperature: 0.8, maxTokens: 2048 });
12
+ ctx.totalCost += result.cost;
13
+ if (ctx.options.dryRun) {
14
+ spinner.succeed(`Sample article generated (${result.content.split(/\s+/).length} words, dry run — not writing)`);
15
+ return;
16
+ }
17
+ const channelsDir = getChannelsDir();
18
+ const examplePath = join(channelsDir, intent.channelId, "examples", "sample-1.md");
19
+ await writeFile(examplePath, result.content, "utf-8");
20
+ const wordCount = result.content.split(/\s+/).length;
21
+ spinner.succeed(`Sample article: ${wordCount} words → channels/${intent.channelId}/examples/sample-1.md`);
22
+ }
23
+ //# sourceMappingURL=generate-example.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-example.js","sourceRoot":"","sources":["../../../../src/commands/create/steps/generate-example.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,OAAO,EACL,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,+BAA+B,CAAC;AAEvC,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,GAAkB,EAClB,OAAY;IAEZ,OAAO,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAAC;IAE9C,MAAM,MAAM,GAAG,GAAG,CAAC,MAAO,CAAC;IAC3B,MAAM,KAAK,GAAG,GAAG,CAAC,KAAM,CAAC;IAEzB,MAAM,MAAM,GAAG,MAAM,OAAO,CAC1B,QAAQ,EACR,sBAAsB,EACtB,yBAAyB,CACvB,MAAM,CAAC,WAAW,EAClB,MAAM,CAAC,WAAW,EAClB,MAAM,CAAC,KAAK,CAAC,KAAK,EAClB,KAAK,CAAC,IAAI,EACV,KAAK,CAAC,OAAO,EACb,KAAK,CAAC,UAAU,IAAI,EAAE,EACtB,KAAK,CAAC,UAAU,CAAC,SAAS,EAC1B,KAAK,CAAC,UAAU,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,6BAA6B;IACtE,KAAK,CAAC,IAAI,CACX,EACD,EAAE,WAAW,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,CACtC,CAAC;IAEF,GAAG,CAAC,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC;IAE7B,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;QACvB,OAAO,CAAC,OAAO,CACb,6BAA6B,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,gCAAgC,CAChG,CAAC;QACF,OAAO;IACT,CAAC;IAED,MAAM,WAAW,GAAG,cAAc,EAAE,CAAC;IACrC,MAAM,WAAW,GAAG,IAAI,CACtB,WAAW,EACX,MAAM,CAAC,SAAS,EAChB,UAAU,EACV,aAAa,CACd,CAAC;IAEF,MAAM,SAAS,CAAC,WAAW,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAEtD,MAAM,SAAS,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC;IACrD,OAAO,CAAC,OAAO,CACb,mBAAmB,SAAS,qBAAqB,MAAM,CAAC,SAAS,uBAAuB,CACzF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { Ora } from "ora";
2
+ import type { CreateContext, GeneratedVoice } from "../types.js";
3
+ export declare function generateVoice(ctx: CreateContext, spinner: Ora): Promise<GeneratedVoice>;
4
+ //# sourceMappingURL=generate-voice.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-voice.d.ts","sourceRoot":"","sources":["../../../../src/commands/create/steps/generate-voice.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAI/B,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAMjE,wBAAsB,aAAa,CACjC,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,GAAG,GACX,OAAO,CAAC,cAAc,CAAC,CAiDzB"}
@@ -0,0 +1,32 @@
1
+ import { callLlmJson } from "@ghostwriter/content-pipeline";
2
+ import { AI_PHRASE_BLACKLIST } from "@ghostwriter/core";
3
+ import { formatStyleProfile } from "@ghostwriter/style-fingerprint";
4
+ import { VOICE_GENERATION_SYSTEM, buildVoiceGenerationPrompt, } from "../prompts/voice-generation.js";
5
+ export async function generateVoice(ctx, spinner) {
6
+ spinner.start("Generating voice persona...");
7
+ const intent = ctx.intent;
8
+ // Build style profile text for the prompt
9
+ let styleProfileText = "No style profile available — use the tone description.";
10
+ if (ctx.styleProfile) {
11
+ styleProfileText = formatStyleProfile(ctx.styleProfile, "prompt");
12
+ }
13
+ // Include style reference names as additional context
14
+ const nameRefs = intent.styleReferences.filter((ref) => !ref.startsWith("http"));
15
+ if (nameRefs.length > 0) {
16
+ styleProfileText += `\n\nThe writing style should be inspired by: ${nameRefs.join(", ")}`;
17
+ }
18
+ // Use a subset of the blacklist for the prompt (top 20 most common)
19
+ const forbiddenSample = AI_PHRASE_BLACKLIST.slice(0, 20);
20
+ const { data, cost } = await callLlmJson("sonnet", VOICE_GENERATION_SYSTEM, buildVoiceGenerationPrompt(intent.toneDescription, intent.topic.domain, styleProfileText, forbiddenSample), { temperature: 0.7 });
21
+ ctx.totalCost += cost;
22
+ // Ensure forbidden list includes the full blacklist
23
+ const existingForbidden = new Set(data.vocabulary.forbidden.map((w) => w.toLowerCase()));
24
+ for (const phrase of AI_PHRASE_BLACKLIST) {
25
+ if (!existingForbidden.has(phrase.toLowerCase())) {
26
+ data.vocabulary.forbidden.push(phrase);
27
+ }
28
+ }
29
+ spinner.succeed(`Voice: ${data.name} — ${data.tone} tone`);
30
+ return data;
31
+ }
32
+ //# sourceMappingURL=generate-voice.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-voice.js","sourceRoot":"","sources":["../../../../src/commands/create/steps/generate-voice.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAC5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAEpE,OAAO,EACL,uBAAuB,EACvB,0BAA0B,GAC3B,MAAM,gCAAgC,CAAC;AAExC,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,GAAkB,EAClB,OAAY;IAEZ,OAAO,CAAC,KAAK,CAAC,6BAA6B,CAAC,CAAC;IAE7C,MAAM,MAAM,GAAG,GAAG,CAAC,MAAO,CAAC;IAE3B,0CAA0C;IAC1C,IAAI,gBAAgB,GAAG,wDAAwD,CAAC;IAChF,IAAI,GAAG,CAAC,YAAY,EAAE,CAAC;QACrB,gBAAgB,GAAG,kBAAkB,CAAC,GAAG,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;IACpE,CAAC;IAED,sDAAsD;IACtD,MAAM,QAAQ,GAAG,MAAM,CAAC,eAAe,CAAC,MAAM,CAC5C,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,CACjC,CAAC;IACF,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,gBAAgB,IAAI,gDAAgD,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;IAC5F,CAAC;IAED,oEAAoE;IACpE,MAAM,eAAe,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAEzD,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,MAAM,WAAW,CACtC,QAAQ,EACR,uBAAuB,EACvB,0BAA0B,CACxB,MAAM,CAAC,eAAe,EACtB,MAAM,CAAC,KAAK,CAAC,MAAM,EACnB,gBAAgB,EAChB,eAAe,CAChB,EACD,EAAE,WAAW,EAAE,GAAG,EAAE,CACrB,CAAC;IAEF,GAAG,CAAC,SAAS,IAAI,IAAI,CAAC;IAEtB,oDAAoD;IACpD,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAC/B,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CACtD,CAAC;IACF,KAAK,MAAM,MAAM,IAAI,mBAAmB,EAAE,CAAC;QACzC,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;YACjD,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACzC,CAAC;IACH,CAAC;IAED,OAAO,CAAC,OAAO,CAAC,UAAU,IAAI,CAAC,IAAI,MAAM,IAAI,CAAC,IAAI,OAAO,CAAC,CAAC;IAE3D,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { Ora } from "ora";
2
+ import type { CreateContext, ParsedIntent } from "../types.js";
3
+ export declare function parseIntent(ctx: CreateContext, spinner: Ora): Promise<ParsedIntent>;
4
+ //# sourceMappingURL=parse-intent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-intent.d.ts","sourceRoot":"","sources":["../../../../src/commands/create/steps/parse-intent.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE/B,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAM/D,wBAAsB,WAAW,CAC/B,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,GAAG,GACX,OAAO,CAAC,YAAY,CAAC,CAwBvB"}
@@ -0,0 +1,16 @@
1
+ import { callLlmJson } from "@ghostwriter/content-pipeline";
2
+ import { INTENT_PARSING_SYSTEM, buildIntentParsingPrompt, } from "../prompts/intent-parsing.js";
3
+ export async function parseIntent(ctx, spinner) {
4
+ spinner.start("Parsing intent from description...");
5
+ const { data, cost } = await callLlmJson("sonnet", INTENT_PARSING_SYSTEM, buildIntentParsingPrompt(ctx.rawDescription), { temperature: 0.3 });
6
+ ctx.totalCost += cost;
7
+ // Ensure channelId is valid kebab-case
8
+ data.channelId = data.channelId
9
+ .toLowerCase()
10
+ .replace(/[^a-z0-9-]/g, "-")
11
+ .replace(/-+/g, "-")
12
+ .replace(/^-|-$/g, "");
13
+ spinner.succeed(`Parsed: "${data.channelName}" (${data.contentType}) — ${data.topic.domain}`);
14
+ return data;
15
+ }
16
+ //# sourceMappingURL=parse-intent.js.map