@vncsleal/quillby 0.3.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 (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +140 -0
  3. package/bin/quillby-mcp +12 -0
  4. package/config/context.json +1 -0
  5. package/config/memory.json +1 -0
  6. package/config/rss_sources.txt +2 -0
  7. package/dist/agent.d.ts +21 -0
  8. package/dist/agent.js +61 -0
  9. package/dist/agent.js.map +1 -0
  10. package/dist/agents/compose.d.ts +2 -0
  11. package/dist/agents/compose.js +40 -0
  12. package/dist/agents/compose.js.map +1 -0
  13. package/dist/agents/discover.d.ts +15 -0
  14. package/dist/agents/discover.js +39 -0
  15. package/dist/agents/discover.js.map +1 -0
  16. package/dist/agents/harvest.d.ts +31 -0
  17. package/dist/agents/harvest.js +100 -0
  18. package/dist/agents/harvest.js.map +1 -0
  19. package/dist/agents/onboard.d.ts +14 -0
  20. package/dist/agents/onboard.js +88 -0
  21. package/dist/agents/onboard.js.map +1 -0
  22. package/dist/agents/seeds.d.ts +34 -0
  23. package/dist/agents/seeds.js +64 -0
  24. package/dist/agents/seeds.js.map +1 -0
  25. package/dist/cli.d.ts +44 -0
  26. package/dist/cli.js +233 -0
  27. package/dist/cli.js.map +1 -0
  28. package/dist/config.d.ts +22 -0
  29. package/dist/config.js +44 -0
  30. package/dist/config.js.map +1 -0
  31. package/dist/extractors/content.d.ts +13 -0
  32. package/dist/extractors/content.js +93 -0
  33. package/dist/extractors/content.js.map +1 -0
  34. package/dist/extractors/reddit.d.ts +7 -0
  35. package/dist/extractors/reddit.js +69 -0
  36. package/dist/extractors/reddit.js.map +1 -0
  37. package/dist/extractors/rss.d.ts +4 -0
  38. package/dist/extractors/rss.js +58 -0
  39. package/dist/extractors/rss.js.map +1 -0
  40. package/dist/index.d.ts +1 -0
  41. package/dist/index.js +139 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/init.d.ts +1 -0
  44. package/dist/init.js +192 -0
  45. package/dist/init.js.map +1 -0
  46. package/dist/llm.d.ts +2 -0
  47. package/dist/llm.js +11 -0
  48. package/dist/llm.js.map +1 -0
  49. package/dist/mcp/server.d.ts +1 -0
  50. package/dist/mcp/server.js +1278 -0
  51. package/dist/mcp/server.js.map +1 -0
  52. package/dist/output/feedback.d.ts +51 -0
  53. package/dist/output/feedback.js +78 -0
  54. package/dist/output/feedback.js.map +1 -0
  55. package/dist/output/structures.d.ts +5 -0
  56. package/dist/output/structures.js +121 -0
  57. package/dist/output/structures.js.map +1 -0
  58. package/dist/types.d.ts +305 -0
  59. package/dist/types.js +74 -0
  60. package/dist/types.js.map +1 -0
  61. package/package.json +63 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vinicius Leal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # Quillby
2
+
3
+ Quillby gives Claude a daily content briefing. It scans articles across your topics, finds what's relevant to your audience, and helps you write posts that sound like you — not generic AI.
4
+
5
+ No extra accounts. No API keys. Everything runs on your computer, inside Claude.
6
+
7
+ ---
8
+
9
+ ## What you need
10
+
11
+ - **[Claude Desktop](https://claude.ai/download)** — the free desktop app from Anthropic (free tier works)
12
+ - **[Node.js 20+](https://nodejs.org)** — a free one-time install (click the large **LTS** button on their site)
13
+
14
+ ---
15
+
16
+ ## Installation
17
+
18
+ ### macOS / Linux
19
+
20
+ Paste this into your terminal. It installs Quillby and connects it to Claude Desktop automatically:
21
+
22
+ ```bash
23
+ curl -fsSL https://raw.githubusercontent.com/vncsleal/quillby/main/install.sh | bash
24
+ ```
25
+
26
+ ### Windows
27
+
28
+ Open PowerShell and run:
29
+
30
+ ```powershell
31
+ irm https://raw.githubusercontent.com/vncsleal/quillby/main/install.ps1 | iex
32
+ ```
33
+
34
+ Both scripts handle everything: install Quillby, inject the Claude Desktop config with absolute paths, and print next steps.
35
+
36
+ Then **fully quit Claude Desktop** (right-click the Dock/taskbar icon → Quit), reopen it, and in a new chat type:
37
+
38
+ > Set me up with Quillby
39
+
40
+ Claude will ask a few questions about your work, your audience, and what you publish. Answer naturally — that's how Quillby learns your voice.
41
+
42
+ ### Manual install (any platform)
43
+
44
+ 1. Install the package:
45
+ ```
46
+ npm install -g @vncsleal/quillby
47
+ ```
48
+
49
+ 2. Open your Claude Desktop config file:
50
+ - **Mac:** `~/Library/Application Support/Claude/claude_desktop_config.json`
51
+ - **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
52
+
53
+ 3. Add the following inside the `mcpServers` block:
54
+ ```json
55
+ {
56
+ "mcpServers": {
57
+ "quillby": {
58
+ "command": "quillby-mcp"
59
+ }
60
+ }
61
+ }
62
+ ```
63
+
64
+ 4. Fully quit and reopen Claude Desktop, then say: *Set me up with Quillby*
65
+
66
+ ---
67
+
68
+ ## Every day
69
+
70
+ Once set up, just talk to Claude like normal.
71
+
72
+ **Get today's content ideas:**
73
+
74
+ > "Give me my Quillby daily brief"
75
+
76
+ Claude scans today's articles across your topics, picks the most relevant ones for your audience, and gives you a set of ready-to-use ideas — each with a specific angle and hook.
77
+
78
+ **Write a post from any idea:**
79
+
80
+ > "Write a LinkedIn post from idea 3"
81
+
82
+ Claude writes it in your voice, based on your profile.
83
+
84
+ **Save it:**
85
+
86
+ > "Save this draft"
87
+
88
+ Quillby stores it in the `output/` folder inside your Quillby directory.
89
+
90
+ ---
91
+
92
+ ## Teaching Quillby your voice
93
+
94
+ The more examples Quillby has, the more accurately it writes like you.
95
+
96
+ When Claude writes a post you're happy with, say:
97
+
98
+ > "Add this post to my Quillby voice examples"
99
+
100
+ Quillby saves it. Every future post draws on those examples.
101
+
102
+ To check what Quillby knows about your style:
103
+
104
+ > "Show me my Quillby voice memory"
105
+
106
+ ---
107
+
108
+ ## Tips
109
+
110
+ **Updating your focus:**
111
+ > "Update my Quillby profile — I'm focusing on [topic] now"
112
+
113
+ **Adding sources:**
114
+ > "Find good news sources for my Quillby topics and add them"
115
+
116
+ **Being specific gets better results.** "Write a 150-word conversational LinkedIn post from idea 2" works much better than "write a post."
117
+
118
+ **Your content stays on your computer.** Your profile, voice examples, drafts, and content ideas are saved locally in the `output/` folder. Nothing is sent to any external service.
119
+
120
+ ---
121
+
122
+ ## Troubleshooting
123
+
124
+ **Quillby doesn't appear in Claude** — Make sure you fully quit and reopened Claude Desktop after saving the config. Check the path in the config matches exactly what the terminal printed (no extra spaces or missing characters).
125
+
126
+ **"No context saved" error** — Run the onboarding first: *"Run the quillby_onboarding prompt"*
127
+
128
+ **"No feeds configured" error** — Ask Claude to find sources: *"Find RSS feeds for my topics and add them to Quillby"*
129
+
130
+ ---
131
+
132
+ ## For developers
133
+
134
+ HTTP transport, environment variables, scheduled harvest, the full tool reference, and integration configs for VS Code and Cursor: see [docs/MCP.md](docs/MCP.md).
135
+
136
+ ---
137
+
138
+ ## License
139
+
140
+ MIT
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import { fileURLToPath } from 'node:url';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { realpathSync } from 'node:fs';
5
+ import { spawnSync } from 'node:child_process';
6
+
7
+ const __filename = realpathSync(fileURLToPath(import.meta.url));
8
+ const __dirname = dirname(__filename);
9
+ const server = resolve(__dirname, '..', 'dist', 'mcp', 'server.js');
10
+
11
+ const result = spawnSync(process.execPath, [server, ...process.argv.slice(2)], { stdio: 'inherit' });
12
+ process.exit(result.status ?? 1);
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1 @@
1
+ {"voiceExamples":[]}
@@ -0,0 +1,2 @@
1
+ # Quillby RSS Sources
2
+ # Add your RSS/Atom feed URLs below, one per line.
@@ -0,0 +1,21 @@
1
+ export type ToolDef = {
2
+ name: string;
3
+ description: string;
4
+ parameters: Record<string, unknown>;
5
+ execute: (args: unknown) => Promise<string>;
6
+ };
7
+ export type AgentOptions = {
8
+ model: string;
9
+ instructions: string;
10
+ userMessage: string;
11
+ tools?: ToolDef[];
12
+ maxTurns?: number;
13
+ temperature?: number;
14
+ jsonOutput?: boolean;
15
+ };
16
+ /**
17
+ * Runs a genuine agent loop: calls the LLM, executes any tool calls it makes,
18
+ * feeds results back, and repeats until the model produces a final answer
19
+ * (no more tool calls) or maxTurns is reached.
20
+ */
21
+ export declare function runAgent(opts: AgentOptions): Promise<string>;
package/dist/agent.js ADDED
@@ -0,0 +1,61 @@
1
+ import OpenAI from "openai";
2
+ // ─── Core agent loop ──────────────────────────────────────────────────────────
3
+ /**
4
+ * Runs a genuine agent loop: calls the LLM, executes any tool calls it makes,
5
+ * feeds results back, and repeats until the model produces a final answer
6
+ * (no more tool calls) or maxTurns is reached.
7
+ */
8
+ export async function runAgent(opts) {
9
+ const client = new OpenAI();
10
+ const { model, instructions, userMessage, tools = [], maxTurns = 25, temperature = 0.2, jsonOutput = false, } = opts;
11
+ const messages = [
12
+ { role: "system", content: instructions },
13
+ { role: "user", content: userMessage },
14
+ ];
15
+ const openaiTools = tools.map((t) => ({
16
+ type: "function",
17
+ function: {
18
+ name: t.name,
19
+ description: t.description,
20
+ parameters: t.parameters,
21
+ },
22
+ }));
23
+ for (let turn = 0; turn < maxTurns; turn++) {
24
+ const response = await client.chat.completions.create({
25
+ model,
26
+ messages,
27
+ tools: openaiTools.length > 0 ? openaiTools : undefined,
28
+ tool_choice: openaiTools.length > 0 ? "auto" : undefined,
29
+ temperature,
30
+ response_format: jsonOutput ? { type: "json_object" } : undefined,
31
+ });
32
+ const msg = response.choices[0].message;
33
+ messages.push(msg);
34
+ // No tool calls → final answer
35
+ if (!msg.tool_calls || msg.tool_calls.length === 0) {
36
+ return msg.content ?? "";
37
+ }
38
+ // Execute all tool calls (in parallel for tools that can run concurrently)
39
+ const toolResults = await Promise.all(msg.tool_calls.map(async (call) => {
40
+ const tool = tools.find((t) => t.name === call.function.name);
41
+ let content;
42
+ try {
43
+ const args = JSON.parse(call.function.arguments);
44
+ content = tool
45
+ ? await tool.execute(args)
46
+ : `Error: unknown tool "${call.function.name}"`;
47
+ }
48
+ catch (e) {
49
+ content = `Error executing tool: ${e instanceof Error ? e.message : String(e)}`;
50
+ }
51
+ return {
52
+ role: "tool",
53
+ tool_call_id: call.id,
54
+ content,
55
+ };
56
+ }));
57
+ messages.push(...toolResults);
58
+ }
59
+ throw new Error(`Agent loop exceeded ${maxTurns} turns without completing`);
60
+ }
61
+ //# sourceMappingURL=agent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent.js","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare const PLATFORM_GUIDES: Record<string, string>;
2
+ export declare const SUPPORTED_PLATFORMS: string[];
@@ -0,0 +1,40 @@
1
+ // Platform format guides — no LLM calls; host model writes the posts.
2
+ // ─── Anti-AI-mannerism rules (apply to every platform) ───────────────────────
3
+ //
4
+ // These are banned patterns. Violating any of them makes the draft feel AI-generated.
5
+ //
6
+ // BANNED constructs:
7
+ // - "It's not X, it's Y" contrasts — they are clichéd and preachy
8
+ // - Em-dash clusters: avoid 3+ dashes in a single post
9
+ // - Bullet-list summaries masquerading as prose
10
+ // - Filler openers: "In today's world", "In an era of", "Let's talk about",
11
+ // "This is a reminder that", "Here's the thing:", "The truth is:"
12
+ // - AI-corporate adjectives: "game-changer", "transformative", "innovative",
13
+ // "powerful", "exciting", "impactful", "leverage", "unlock", "dive into"
14
+ // - Redundant emoji stacking (two emojis in a row, or emoji + dash + text)
15
+ // - Rhetorical question openers that give away the answer: "Have you ever noticed..."
16
+ // - Passive closing motivationals: "Remember: X matters", "Don't forget to X"
17
+ // - Formulaic hooks: "X years in design taught me...", "Nobody talks about..."
18
+ // - Numbered listicles dressed as organic thought
19
+ // - Over-capitalized Concepts That Sound Like Product Names
20
+ //
21
+ // VOICE AMPLIFICATION — oversteer, not understeer:
22
+ // - Read the user's voiceExamples field. Identify the strongest stylistic quirks
23
+ // and push them harder than feels comfortable. That discomfort is the sweet spot.
24
+ // - If the user is direct, make it blunt. If they're warm, make it personal.
25
+ // If they're provocative, make it confrontational.
26
+ // - Use vocabulary the user actually uses. Avoid vocabulary they never use.
27
+ // - Write incomplete sentences if the user writes that way. Match rhythm.
28
+ // - Never smooth out the rough edges — the rough edges ARE the voice.
29
+ // ─── Platform guides ──────────────────────────────────────────────────────────
30
+ export const PLATFORM_GUIDES = {
31
+ linkedin: "LinkedIn post: 150–300 words. 3–5 short paragraphs — vary length, not all punchy. No em-dash clusters (1 max per post). No bullet lists. No filler openers. First line earns the click; make it specific, not clever. End with a real question or a hard stop — not a motivational kicker. Max 3 hashtags, lowercase, placed at the end. Write from a specific POV and hold it through the whole post. Read the user's voice examples and amplify the strongest quirks until it sounds unmistakably like them.",
32
+ x: "X/Twitter thread or single tweet. Single tweet: ≤280 chars, no padding, zero fluff — sounds like a hot take texted to a friend, not a content strategy. Thread (4–8 posts): each tweet works standalone, the thread earns a re-read. No corporate vocab. No emoji stacking. Strongest observation last. Write with the user's exact word choices, sentence length, and rhetorical habits. If the voice is abrasive in the examples, keep it abrasive.",
33
+ instagram: "Instagram caption: 125–150 words max (above-the-fold is 2 lines — make them count). Hook: specific, visual, or provocative — never a question. Short paragraphs, hard line breaks. CTA must be concrete and brief. 5–10 hashtags at the end, lowercase. Emojis: 0–2 total, only when they replace words, never as decoration. No AI-filler phrases (\"game-changer\", \"powerful\", \"in today's world\"). Tone must feel like a DM from the user, not a branded post. Amplify the voice — read voiceExamples and dial it up 20%.",
34
+ threads: "Threads: ≤500 chars standalone or 3–6 post thread. No hashtags. Raw and opinionated — think shower thought, not caption. One idea only, no wrapping up. Strong first line, abrupt last line. Feels like a voice note, not a post. Sentence fragments allowed. Use the user's vocabulary, not yours. If the user is provocative, be more provocative than feels safe.",
35
+ blog: "Blog post: 700–1400 words. One main argument, no listicle padding. Each section earns its existence. Personal examples or specific observations over generic claims. Lead with the sharpest version of the thesis, not a warm-up. Prose, not bullet points. End with consequence, not a summary. Maintain the user's exact register throughout — do not drift into editorial neutrality.",
36
+ newsletter: "Newsletter section: 300–500 words. The reader already opted in — skip the sell. One thing, told well, with a specific observation the reader won't get elsewhere. No filler. No listicle. Conversational but not performatively casual. End with a provocation or a concrete action — not a \"thanks for reading\". Use the user's voice examples as the tonal floor, then go further.",
37
+ medium: "Medium article: 800–1500 words. Story-driven, first-person, no passive voice. Subheadings that are sentences, not topics. The reader should feel like they're in the room with the writer. Specific anecdotes over general observations. Do not end with lessons learned — end with what changed, or what still hasn't. Voice must match the user's examples: take the distinctive patterns and amplify them across the whole piece.",
38
+ };
39
+ export const SUPPORTED_PLATFORMS = Object.keys(PLATFORM_GUIDES);
40
+ //# sourceMappingURL=compose.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compose.js","sourceRoot":"","sources":["../../src/agents/compose.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE,gFAAgF;AAChF,EAAE;AACF,sFAAsF;AACtF,EAAE;AACF,qBAAqB;AACrB,oEAAoE;AACpE,yDAAyD;AACzD,kDAAkD;AAClD,8EAA8E;AAC9E,sEAAsE;AACtE,+EAA+E;AAC/E,6EAA6E;AAC7E,6EAA6E;AAC7E,wFAAwF;AACxF,gFAAgF;AAChF,iFAAiF;AACjF,oDAAoD;AACpD,8DAA8D;AAC9D,EAAE;AACF,mDAAmD;AACnD,mFAAmF;AACnF,sFAAsF;AACtF,+EAA+E;AAC/E,uDAAuD;AACvD,8EAA8E;AAC9E,4EAA4E;AAC5E,wEAAwE;AAExE,iFAAiF;AAEjF,MAAM,CAAC,MAAM,eAAe,GAA2B;IACrD,QAAQ,EACN,gfAAgf;IAClf,CAAC,EAAE,ubAAub;IAC1b,SAAS,EACP,mgBAAmgB;IACrgB,OAAO,EACL,sWAAsW;IACxW,IAAI,EAAE,0XAA0X;IAChY,UAAU,EACR,wXAAwX;IAC1X,MAAM,EACJ,saAAsa;CACza,CAAC;AAEF,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Load current RSS sources from file. Returns empty array if file missing.
3
+ */
4
+ export declare function loadSources(): string[];
5
+ /**
6
+ * Append new feed URLs to the sources file, deduplicating against existing entries.
7
+ */
8
+ export declare function appendSources(newUrls: string[]): {
9
+ added: number;
10
+ skipped: number;
11
+ };
12
+ /**
13
+ * Replace the entire sources file with a new list.
14
+ */
15
+ export declare function replaceSources(urls: string[]): void;
@@ -0,0 +1,39 @@
1
+ import * as fs from "fs";
2
+ import { CONFIG, readTextFile } from "../config.js";
3
+ const SOURCES_FILE = CONFIG.FILES.SOURCES;
4
+ /**
5
+ * Load current RSS sources from file. Returns empty array if file missing.
6
+ */
7
+ export function loadSources() {
8
+ try {
9
+ return readTextFile(CONFIG.FILES.SOURCES)
10
+ .split("\n")
11
+ .map((s) => s.trim())
12
+ .filter((s) => s && !s.startsWith("#"));
13
+ }
14
+ catch {
15
+ return [];
16
+ }
17
+ }
18
+ /**
19
+ * Append new feed URLs to the sources file, deduplicating against existing entries.
20
+ */
21
+ export function appendSources(newUrls) {
22
+ const existing = new Set(loadSources());
23
+ const toAdd = newUrls.filter((u) => u.trim() && !existing.has(u.trim()));
24
+ if (toAdd.length === 0)
25
+ return { added: 0, skipped: newUrls.length };
26
+ const header = !fs.existsSync(SOURCES_FILE)
27
+ ? "# Quillby RSS Sources\n\n"
28
+ : "";
29
+ fs.appendFileSync(SOURCES_FILE, header + toAdd.join("\n") + "\n");
30
+ return { added: toAdd.length, skipped: newUrls.length - toAdd.length };
31
+ }
32
+ /**
33
+ * Replace the entire sources file with a new list.
34
+ */
35
+ export function replaceSources(urls) {
36
+ const unique = [...new Set(urls.map((u) => u.trim()).filter(Boolean))];
37
+ fs.writeFileSync(SOURCES_FILE, "# Quillby RSS Sources\n\n" + unique.join("\n") + "\n");
38
+ }
39
+ //# sourceMappingURL=discover.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discover.js","sourceRoot":"","sources":["../../src/agents/discover.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEpD,MAAM,YAAY,GAAG,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC;AAE1C;;GAEG;AACH,MAAM,UAAU,WAAW;IACzB,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC;aACtC,KAAK,CAAC,IAAI,CAAC;aACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;aACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,aAAa,CAAC,OAAiB;IAC7C,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC;IACxC,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IAEzE,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC;IAErE,MAAM,MAAM,GAAG,CAAC,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC;QACzC,CAAC,CAAC,2BAA2B;QAC7B,CAAC,CAAC,EAAE,CAAC;IAEP,EAAE,CAAC,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;IAElE,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;AACzE,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,IAAc;IAC3C,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IACvE,EAAE,CAAC,aAAa,CAAC,YAAY,EAAE,2BAA2B,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;AACzF,CAAC"}
@@ -0,0 +1,31 @@
1
+ import type { EnrichedArticle } from "../types.js";
2
+ /**
3
+ * Fetch articles from all configured sources (RSS, Reddit) and enrich content.
4
+ * RSS covers: Google News, Feedly, Medium tags, any Atom/RSS feed URL.
5
+ * Reddit covers: any subreddit community via reddit://r/<name>.
6
+ * Returns raw enriched articles for the host model to analyze.
7
+ * No LLM calls — pure I/O.
8
+ *
9
+ * slim=true: skip content fetching, return only title/source/link/snippet.
10
+ * Use this first to scan headlines cheaply, then call enrichArticle on demand.
11
+ */
12
+ export declare function fetchArticles(sources: string[], log?: (msg: string) => void, slim?: boolean): Promise<{
13
+ articles: EnrichedArticle[];
14
+ seenUrls: Set<string>;
15
+ }>;
16
+ /**
17
+ * Keyword pre-filter: score articles by how many user topic words appear
18
+ * in the title + snippet. Returns articles sorted descending by score,
19
+ * with score=0 articles kept at the end so nothing is dropped.
20
+ * This runs before sending to the LLM to reduce context window waste.
21
+ */
22
+ export declare function preScoreArticles(articles: {
23
+ title: string;
24
+ snippet: string;
25
+ link: string;
26
+ }[], topics: string[]): Array<{
27
+ title: string;
28
+ snippet: string;
29
+ link: string;
30
+ _preScore: number;
31
+ }>;
@@ -0,0 +1,100 @@
1
+ import { fetchFeeds, getSeenUrls } from "../extractors/rss.js";
2
+ import { fetchReddit } from "../extractors/reddit.js";
3
+ import { enrichArticle } from "../extractors/content.js";
4
+ import { mapWithConcurrency } from "../llm.js";
5
+ import { CONFIG } from "../config.js";
6
+ /**
7
+ * Route sources to the right extractor based on URL prefix:
8
+ * reddit://r/<subreddit>[/<sort>] (sort defaults to "hot")
9
+ * anything else → RSS (covers Google News, Feedly, Medium tags, etc.)
10
+ */
11
+ async function fetchAllSources(sources, log) {
12
+ const rssSources = [];
13
+ const redditSources = [];
14
+ for (const src of sources) {
15
+ if (src.startsWith("reddit://"))
16
+ redditSources.push(src);
17
+ else
18
+ rssSources.push(src);
19
+ }
20
+ const results = [];
21
+ if (rssSources.length > 0) {
22
+ const rssItems = await fetchFeeds(rssSources, log);
23
+ results.push(...rssItems);
24
+ }
25
+ for (const src of redditSources) {
26
+ // reddit://r/marketing → subreddit = "marketing", sort = "hot"
27
+ // reddit://r/marketing/top → subreddit = "marketing", sort = "top"
28
+ const path = src.replace("reddit://r/", "").trim();
29
+ const [subreddit, sort = "hot"] = path.split("/");
30
+ if (subreddit) {
31
+ const items = await fetchReddit(subreddit, sort, log);
32
+ results.push(...items);
33
+ }
34
+ }
35
+ return results;
36
+ }
37
+ /**
38
+ * Fetch articles from all configured sources (RSS, Reddit) and enrich content.
39
+ * RSS covers: Google News, Feedly, Medium tags, any Atom/RSS feed URL.
40
+ * Reddit covers: any subreddit community via reddit://r/<name>.
41
+ * Returns raw enriched articles for the host model to analyze.
42
+ * No LLM calls — pure I/O.
43
+ *
44
+ * slim=true: skip content fetching, return only title/source/link/snippet.
45
+ * Use this first to scan headlines cheaply, then call enrichArticle on demand.
46
+ */
47
+ export async function fetchArticles(sources, log = () => { }, slim = false) {
48
+ log(`Fetching from ${sources.length} source(s)...`);
49
+ const items = await fetchAllSources(sources, log);
50
+ const seenUrls = getSeenUrls();
51
+ items.forEach((item) => seenUrls.add(item.link));
52
+ if (items.length === 0) {
53
+ log("No new items found.");
54
+ return { articles: [], seenUrls };
55
+ }
56
+ if (slim) {
57
+ log(`Found ${items.length} items. Slim mode — skipping content fetch.`);
58
+ return {
59
+ articles: items.map((item) => ({ ...item, enrichedContent: "" })),
60
+ seenUrls,
61
+ };
62
+ }
63
+ log(`Found ${items.length} new items. Enriching content...`);
64
+ const articles = await mapWithConcurrency(items, async (item, index) => {
65
+ const enrichedContent = await enrichArticle(item.link, item.title);
66
+ if (index > 0 && index % 10 === 0) {
67
+ log(`Enriched ${index}/${items.length} articles...`);
68
+ }
69
+ return {
70
+ id: item.id,
71
+ source: item.source,
72
+ title: item.title,
73
+ link: item.link,
74
+ snippet: item.snippet,
75
+ enrichedContent,
76
+ publishedAt: item.publishedAt,
77
+ };
78
+ }, CONFIG.RSS.CONCURRENCY);
79
+ log(`Fetch complete. ${articles.length} articles ready for analysis.`);
80
+ return { articles, seenUrls };
81
+ }
82
+ /**
83
+ * Keyword pre-filter: score articles by how many user topic words appear
84
+ * in the title + snippet. Returns articles sorted descending by score,
85
+ * with score=0 articles kept at the end so nothing is dropped.
86
+ * This runs before sending to the LLM to reduce context window waste.
87
+ */
88
+ export function preScoreArticles(articles, topics) {
89
+ const keywords = topics.map((t) => t.toLowerCase().trim()).filter(Boolean);
90
+ if (keywords.length === 0)
91
+ return articles.map((a) => ({ ...a, _preScore: 0 }));
92
+ return articles
93
+ .map((a) => {
94
+ const haystack = `${a.title} ${a.snippet}`.toLowerCase();
95
+ const score = keywords.reduce((acc, kw) => acc + (haystack.includes(kw) ? 1 : 0), 0);
96
+ return { ...a, _preScore: score };
97
+ })
98
+ .sort((a, b) => b._preScore - a._preScore);
99
+ }
100
+ //# sourceMappingURL=harvest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harvest.js","sourceRoot":"","sources":["../../src/agents/harvest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import { type UserContext, type UserMemory } from "../types.js";
2
+ export declare function contextExists(): boolean;
3
+ export declare function loadContext(): UserContext | null;
4
+ export declare function saveContext(ctx: UserContext): void;
5
+ export declare function memoryExists(): boolean;
6
+ export declare function loadMemory(): UserMemory;
7
+ export declare function saveMemory(mem: UserMemory): void;
8
+ export declare function appendVoiceExample(text: string): void;
9
+ /**
10
+ * Render the user context as a concise text block for LLM system prompts.
11
+ */
12
+ export declare function contextToPromptText(ctx: UserContext, memory?: UserMemory): string;
13
+ /** The onboarding prompt text — used as an MCP prompt. */
14
+ export declare const ONBOARDING_PROMPT = "You are helping a new Quillby user set up their content intelligence profile.\n\nAsk the following questions conversationally \u2014 you don't need to number them or ask them all at once. Use natural follow-up based on their answers.\n\nQuestions to cover:\n1. What is your name and professional role?\n2. What industry or niche are you in?\n3. What topics are you most passionate about writing on? (aim for 3\u20138 topics)\n4. How would you describe your writing voice and style? (e.g., \"direct, no-fluff, analytical\" or \"warm, story-driven, practitioner-focused\")\n5. Who is your target audience?\n6. What are your content goals? (e.g., thought leadership, personal brand, lead generation, community building)\n7. Are there any topics you want to avoid in your content?\n8. Which platforms do you publish on? (LinkedIn, X/Twitter, blog, newsletter, Medium, etc.)\nOnce you have their answers, call the `quillby_set_context` tool with the structured data. After saving, let them know their profile is ready and suggest:\n- Running `quillby_add_feeds` to add relevant RSS sources.\n- Using `quillby_remember` to add example posts that define their voice \u2014 these accumulate in memory and improve every post Quillby generates.";
@@ -0,0 +1,88 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { CONFIG, ensureDir } from "../config.js";
4
+ import { UserContextSchema, UserMemorySchema } from "../types.js";
5
+ const CONTEXT_FILE = CONFIG.FILES.CONTEXT;
6
+ const MEMORY_FILE = CONFIG.FILES.MEMORY;
7
+ export function contextExists() {
8
+ return fs.existsSync(CONTEXT_FILE);
9
+ }
10
+ export function loadContext() {
11
+ if (!fs.existsSync(CONTEXT_FILE))
12
+ return null;
13
+ try {
14
+ const raw = JSON.parse(fs.readFileSync(CONTEXT_FILE, "utf-8"));
15
+ return UserContextSchema.parse(raw);
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }
21
+ export function saveContext(ctx) {
22
+ ensureDir(path.dirname(CONTEXT_FILE));
23
+ const validated = UserContextSchema.parse(ctx);
24
+ fs.writeFileSync(CONTEXT_FILE, JSON.stringify(validated, null, 2));
25
+ }
26
+ export function memoryExists() {
27
+ return fs.existsSync(MEMORY_FILE);
28
+ }
29
+ export function loadMemory() {
30
+ if (!fs.existsSync(MEMORY_FILE))
31
+ return UserMemorySchema.parse({});
32
+ try {
33
+ const raw = JSON.parse(fs.readFileSync(MEMORY_FILE, "utf-8"));
34
+ return UserMemorySchema.parse(raw);
35
+ }
36
+ catch {
37
+ return UserMemorySchema.parse({});
38
+ }
39
+ }
40
+ export function saveMemory(mem) {
41
+ ensureDir(path.dirname(MEMORY_FILE));
42
+ fs.writeFileSync(MEMORY_FILE, JSON.stringify(UserMemorySchema.parse(mem), null, 2));
43
+ }
44
+ export function appendVoiceExample(text) {
45
+ const mem = loadMemory();
46
+ const examples = [text, ...mem.voiceExamples].slice(0, 10);
47
+ saveMemory({ ...mem, voiceExamples: examples });
48
+ }
49
+ /**
50
+ * Render the user context as a concise text block for LLM system prompts.
51
+ */
52
+ export function contextToPromptText(ctx, memory) {
53
+ const lines = [
54
+ ctx.name ? `Name: ${ctx.name}` : null,
55
+ `Role: ${ctx.role}`,
56
+ `Industry: ${ctx.industry}`,
57
+ `Topics: ${ctx.topics.join(", ")}`,
58
+ `Voice: ${ctx.voice}`,
59
+ `Audience: ${ctx.audienceDescription}`,
60
+ `Goals: ${ctx.contentGoals.join(", ")}`,
61
+ ctx.excludeTopics?.length ? `Avoid: ${ctx.excludeTopics.join(", ")}` : null,
62
+ `Platforms: ${ctx.platforms.join(", ")}`,
63
+ ]
64
+ .filter(Boolean)
65
+ .join("\n");
66
+ const examples = memory?.voiceExamples?.length
67
+ ? `\n\nVoice examples:\n${memory.voiceExamples.map((e, i) => `[${i + 1}] ${e}`).join("\n\n")}`
68
+ : "";
69
+ return lines + examples;
70
+ }
71
+ /** The onboarding prompt text — used as an MCP prompt. */
72
+ export const ONBOARDING_PROMPT = `You are helping a new Quillby user set up their content intelligence profile.
73
+
74
+ Ask the following questions conversationally — you don't need to number them or ask them all at once. Use natural follow-up based on their answers.
75
+
76
+ Questions to cover:
77
+ 1. What is your name and professional role?
78
+ 2. What industry or niche are you in?
79
+ 3. What topics are you most passionate about writing on? (aim for 3–8 topics)
80
+ 4. How would you describe your writing voice and style? (e.g., "direct, no-fluff, analytical" or "warm, story-driven, practitioner-focused")
81
+ 5. Who is your target audience?
82
+ 6. What are your content goals? (e.g., thought leadership, personal brand, lead generation, community building)
83
+ 7. Are there any topics you want to avoid in your content?
84
+ 8. Which platforms do you publish on? (LinkedIn, X/Twitter, blog, newsletter, Medium, etc.)
85
+ Once you have their answers, call the \`quillby_set_context\` tool with the structured data. After saving, let them know their profile is ready and suggest:
86
+ - Running \`quillby_add_feeds\` to add relevant RSS sources.
87
+ - Using \`quillby_remember\` to add example posts that define their voice — these accumulate in memory and improve every post Quillby generates.`;
88
+ //# sourceMappingURL=onboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"onboard.js","sourceRoot":"","sources":["../../src/agents/onboard.ts"],"names":[],"mappings":"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"}