@clubnet/seedclub 0.2.8 → 0.2.9

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.
@@ -0,0 +1,169 @@
1
+ /**
2
+ * /extract — the main entry point for the extraction system.
3
+ *
4
+ * Usage:
5
+ * /extract <url> — Capture + extract a URL into the 9-dimension schema
6
+ * /extract — Extract from content already in the conversation
7
+ * /extract clubtone — Turn the last extraction into content directions
8
+ *
9
+ * Flow:
10
+ * 1. User shares a URL or pastes content
11
+ * 2. /extract captures it (L1) and runs 9-dimension extraction (L2)
12
+ * 3. /extract clubtone takes the extraction and generates content seeds
13
+ */
14
+
15
+ import type { ExtensionAPI } from "@mariozechner/Seed Club-coding-agent";
16
+ import { capture } from "../extraction/capture.js";
17
+ import { buildExtractionPrompt, buildContentSeedPrompt } from "../extraction/schema.js";
18
+
19
+ export function registerExtractCommand(pi: ExtensionAPI) {
20
+ // ── /extract command ──────────────────────────────────────────────
21
+
22
+ pi.registerCommand("extract", {
23
+ description: "Extract structured knowledge from a URL or conversation content",
24
+ handler: async (args, ctx) => {
25
+ const arg = args?.trim() || "";
26
+
27
+ // /extract clubtone — content seed pass
28
+ if (arg === "clubtone" || arg === "club" || arg === "tone" || arg === "seeds" || arg === "content") {
29
+ pi.sendUserMessage(
30
+ `Look at the most recent extraction in our conversation. Now turn it into content directions using this approach:\n\n` +
31
+ `Generate 3-5 content directions I could take. For each:\n` +
32
+ `1. **Hook** — opening line that stops a scroll\n` +
33
+ `2. **Core argument** — what I'd actually say, grounded in the source\n` +
34
+ `3. **Format** — tweet thread, short post, essay, newsletter, conversation starter\n` +
35
+ `4. **Source grounding** — which claims, patterns, quotes feed this\n` +
36
+ `5. **Voice note** — how I'd explain this to a friend in 2 sentences\n\n` +
37
+ `Don't be generic. These should feel like things only someone who deeply read the source would write. My voice, not the source's voice.`,
38
+ );
39
+ return;
40
+ }
41
+
42
+ // /extract <url> — capture + extract
43
+ const url = extractUrl(arg);
44
+ if (url) {
45
+ ctx.ui.notify(`Capturing ${url}...`, "info");
46
+
47
+ const result = await capture(url, pi);
48
+ if (!result.complete || !result.content) {
49
+ ctx.ui.notify(`Failed to capture ${url}${result.note ? `: ${result.note}` : ""}`, "error");
50
+ return;
51
+ }
52
+
53
+ const title = extractTitle(result.content) || new URL(url).hostname;
54
+ ctx.ui.notify(`Captured (${result.fetchMethod}). Running extraction...`, "info");
55
+
56
+ // Truncate if massive — keep first 40K chars for extraction
57
+ const content = result.content.length > 40000
58
+ ? result.content.slice(0, 40000) + "\n\n[Content truncated at 40K chars]"
59
+ : result.content;
60
+
61
+ const prompt = buildExtractionPrompt(content, url, title);
62
+ pi.sendUserMessage(
63
+ prompt +
64
+ "
65
+
66
+ After completing the extraction, call seed_save_extraction with the structured results so they are saved to persistent memory. Include all 9 dimensions."
67
+ );
68
+ return;
69
+ }
70
+
71
+ // /extract (no args) — extract from conversation context
72
+ if (!arg) {
73
+ pi.sendUserMessage(
74
+ `Look at the content I've shared in this conversation (URLs, pasted text, documents, etc.) and run a full 9-dimension extraction:\n\n` +
75
+ `1. **Summary** — 2-3 sentences\n` +
76
+ `2. **Claims & Arguments** — every claim including implicit ones, with evidence and confidence\n` +
77
+ `3. **Entities** — people, orgs, concepts, products, technologies\n` +
78
+ `4. **Temporal** — only if time-sensitive, otherwise skip\n` +
79
+ `5. **Quotes** — direct quotes worth preserving\n` +
80
+ `6. **Patterns & Themes** ⭐ — highest value; patterns and how they connect to my work\n` +
81
+ `7. **Relevance** — which areas this matters for and why\n` +
82
+ `8. **Actionable** — content seeds, follow-ups, research threads\n` +
83
+ `9. **Extraction Notes** — your observations on what's rich, what's thin\n\n` +
84
+ `Be thorough. Single-pass extraction misses ~40% of content. Go deep on patterns, relevance, and actionable items.
85
+
86
+ After completing the extraction, call seed_save_extraction with the structured results so they are saved to persistent memory.`,
87
+ );
88
+ return;
89
+ }
90
+
91
+ // /extract <something that's not a URL> — treat as a topic/concept
92
+ pi.sendUserMessage(
93
+ `I want to explore and extract from the concept: "${arg}"\n\n` +
94
+ `Treat this as source material. Run a full 9-dimension extraction:\n` +
95
+ `1. **Summary** — what is this concept/topic\n` +
96
+ `2. **Claims & Arguments** — key claims around this topic\n` +
97
+ `3. **Entities** — key people, orgs, frameworks\n` +
98
+ `4. **Temporal** — only if time-sensitive\n` +
99
+ `5. **Quotes** — notable quotes if you know them\n` +
100
+ `6. **Patterns & Themes** ⭐ — patterns and connections to broader work\n` +
101
+ `7. **Relevance** — where this matters\n` +
102
+ `8. **Actionable** — what can be done with this\n` +
103
+ `9. **Extraction Notes** — what's rich, what needs more research\n\n` +
104
+ `Be thorough and connect everything to real work context.
105
+
106
+ After completing the extraction, call seed_save_extraction with the structured results so they are saved to persistent memory.`,
107
+ );
108
+ },
109
+ });
110
+
111
+ // ── /clubtone shorthand ───────────────────────────────────────────
112
+
113
+ pi.registerCommand("clubtone", {
114
+ description: "Turn extracted knowledge into content directions",
115
+ handler: async (args, ctx) => {
116
+ const context = args?.trim();
117
+ const contextLine = context
118
+ ? `\n\nAdditional context for content direction: ${context}`
119
+ : "";
120
+
121
+ pi.sendUserMessage(
122
+ `Look at the most recent extraction or source material in our conversation. Turn it into content directions.\n\n` +
123
+ `Generate 3-5 content directions I could take. For each:\n` +
124
+ `1. **Hook** — opening line that stops a scroll\n` +
125
+ `2. **Core argument** — what I'd actually say, grounded in the source\n` +
126
+ `3. **Format** — tweet thread, short post, essay, newsletter, conversation starter\n` +
127
+ `4. **Source grounding** — which claims, patterns, quotes feed this\n` +
128
+ `5. **Voice note** — how I'd explain this to a friend in 2 sentences\n\n` +
129
+ `Don't be generic. My voice, not the source's voice.
130
+
131
+ After generating content directions, call seed_save_content_seeds to save them (you'll need the extraction ID from the most recent seed_save_extraction call).${contextLine}`,
132
+ );
133
+ },
134
+ });
135
+ }
136
+
137
+ // ── /extractions — list saved extractions ─────────────────────────────
138
+
139
+ pi.registerCommand("extractions", {
140
+ description: "Browse your saved extractions",
141
+ handler: async (args, ctx) => {
142
+ pi.sendUserMessage(
143
+ "List my recent extractions using seed_list_extractions. Show them in a clean format with title, date, and counts of patterns/claims/actionable items. If I have none, let me know I can use /extract to get started."
144
+ );
145
+ },
146
+ });
147
+
148
+ // ── Helpers ─────────────────────────────────────────────────────────
149
+
150
+ function extractUrl(text: string): string | null {
151
+ // Direct URL
152
+ if (text.match(/^https?:\/\//)) return text.split(/\s/)[0];
153
+
154
+ // URL somewhere in text
155
+ const match = text.match(/(https?:\/\/[^\s]+)/);
156
+ return match ? match[1] : null;
157
+ }
158
+
159
+ function extractTitle(markdown: string): string | null {
160
+ // Try to find a title from Jina Reader markdown output
161
+ const titleMatch = markdown.match(/^#\s+(.+)$/m);
162
+ if (titleMatch) return titleMatch[1].trim();
163
+
164
+ // Try Title: header
165
+ const metaMatch = markdown.match(/^Title:\s*(.+)$/m);
166
+ if (metaMatch) return metaMatch[1].trim();
167
+
168
+ return null;
169
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * L1 Raw Capture — fetch full-fidelity content from URLs.
3
+ *
4
+ * Uses Jina Reader (r.jina.ai) for articles/pages.
5
+ * Returns markdown content without LLM processing.
6
+ */
7
+
8
+ import type { ExtensionAPI } from "@mariozechner/Seed Club-coding-agent";
9
+
10
+ export interface CaptureResult {
11
+ content: string;
12
+ contentType: "markdown" | "text" | "html";
13
+ fetchMethod: string;
14
+ complete: boolean;
15
+ note?: string;
16
+ }
17
+
18
+ /**
19
+ * Fetch full content from a URL via Jina Reader.
20
+ * Returns clean markdown — no summarization, no LLM processing.
21
+ */
22
+ export async function captureUrl(url: string, pi: ExtensionAPI): Promise<CaptureResult> {
23
+ const jinaUrl = `https://r.jina.ai/${url}`;
24
+
25
+ const result = await pi.exec("curl", ["-sL", "-H", "Accept: text/markdown", jinaUrl], {
26
+ timeout: 30000,
27
+ });
28
+
29
+ if (result.code !== 0 || !result.stdout?.trim()) {
30
+ // Fallback: try direct fetch
31
+ const direct = await pi.exec("curl", ["-sL", url], { timeout: 15000 });
32
+ if (direct.code === 0 && direct.stdout?.trim()) {
33
+ return {
34
+ content: direct.stdout,
35
+ contentType: "html",
36
+ fetchMethod: "direct-curl",
37
+ complete: true,
38
+ note: "Jina Reader failed; raw HTML captured",
39
+ };
40
+ }
41
+
42
+ return {
43
+ content: "",
44
+ contentType: "text",
45
+ fetchMethod: "failed",
46
+ complete: false,
47
+ note: `Fetch failed for ${url}`,
48
+ };
49
+ }
50
+
51
+ return {
52
+ content: result.stdout,
53
+ contentType: "markdown",
54
+ fetchMethod: "jina-reader",
55
+ complete: true,
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Capture a tweet via FxTwitter API (returns JSON with full tweet data).
61
+ */
62
+ export async function captureTweet(url: string, pi: ExtensionAPI): Promise<CaptureResult> {
63
+ // Convert x.com/twitter.com URL to fxtwitter API
64
+ const fxUrl = url
65
+ .replace("x.com", "api.fxtwitter.com")
66
+ .replace("twitter.com", "api.fxtwitter.com");
67
+
68
+ const result = await pi.exec("curl", ["-sL", fxUrl], { timeout: 15000 });
69
+
70
+ if (result.code !== 0 || !result.stdout?.trim()) {
71
+ // Fallback to Jina
72
+ return captureUrl(url, pi);
73
+ }
74
+
75
+ return {
76
+ content: result.stdout,
77
+ contentType: "text",
78
+ fetchMethod: "fxtwitter",
79
+ complete: true,
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Auto-detect content type and use the right capture method.
85
+ */
86
+ export async function capture(url: string, pi: ExtensionAPI): Promise<CaptureResult> {
87
+ if (url.match(/(?:x\.com|twitter\.com)\/\w+\/status\/\d+/)) {
88
+ return captureTweet(url, pi);
89
+ }
90
+ return captureUrl(url, pi);
91
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * L2 Extraction Schema — the 9-dimension extraction format.
3
+ *
4
+ * Every source gets extracted into these dimensions.
5
+ * This is where value compounds — connecting source material to your context.
6
+ */
7
+
8
+ export interface Claim {
9
+ claim: string;
10
+ evidence: string;
11
+ confidence: "high" | "medium" | "low";
12
+ implicit: boolean;
13
+ notes?: string;
14
+ }
15
+
16
+ export interface Entity {
17
+ name: string;
18
+ type: "person" | "org" | "concept" | "product" | "technology";
19
+ role: string;
20
+ }
21
+
22
+ export interface Quote {
23
+ text: string;
24
+ attribution: string;
25
+ context?: string;
26
+ }
27
+
28
+ export interface Pattern {
29
+ pattern: string;
30
+ connection: string; // How it connects to operator's context/work
31
+ strength: "strong" | "moderate" | "emerging";
32
+ }
33
+
34
+ export interface Relevance {
35
+ area: string;
36
+ rating: "high" | "medium" | "low";
37
+ why: string;
38
+ }
39
+
40
+ export interface Actionable {
41
+ type: "content_seed" | "follow_up" | "design_audit" | "research" | "outreach";
42
+ description: string;
43
+ priority: "high" | "medium" | "low";
44
+ }
45
+
46
+ export interface ExtractionNotes {
47
+ observations: string[];
48
+ schemaFitness: string;
49
+ secondPassCandidate: boolean;
50
+ }
51
+
52
+ /**
53
+ * The full L2 extraction output for a single source.
54
+ */
55
+ export interface L2Extraction {
56
+ sourceId: string;
57
+ sourceUrl: string;
58
+ sourceTitle: string;
59
+ extractedAt: string;
60
+
61
+ // The 9 dimensions
62
+ summary: string;
63
+ claims: Claim[];
64
+ entities: Entity[];
65
+ temporal?: { events: string[]; note: string }; // Optional — only if time-sensitive
66
+ quotes: Quote[];
67
+ patterns: Pattern[];
68
+ relevance: Relevance[];
69
+ actionable: Actionable[];
70
+ extractionNotes: ExtractionNotes;
71
+ }
72
+
73
+ /**
74
+ * Returns the extraction prompt for Claude.
75
+ * This is what gets injected when the user runs /extract.
76
+ */
77
+ export function buildExtractionPrompt(sourceContent: string, sourceUrl: string, sourceTitle: string): string {
78
+ return `Extract structured knowledge from this source material using the 9-dimension schema below.
79
+
80
+ ## Source
81
+ - **URL:** ${sourceUrl}
82
+ - **Title:** ${sourceTitle}
83
+
84
+ ## Content
85
+ <source_content>
86
+ ${sourceContent}
87
+ </source_content>
88
+
89
+ ## Extraction Schema (9 Dimensions)
90
+
91
+ Extract into these dimensions. Be thorough — single-pass extraction misses ~40% of content.
92
+
93
+ ### 1. Summary
94
+ 2-3 sentences capturing the core contribution.
95
+
96
+ ### 2. Claims & Arguments
97
+ Every claim the source makes, including implicit ones.
98
+ For each: the claim, supporting evidence, confidence (high/medium/low), whether it's implicit, and any notes.
99
+
100
+ ### 3. Entities
101
+ People, organizations, concepts, products, technologies mentioned.
102
+ For each: name, type, and their role in the source.
103
+
104
+ ### 4. Temporal (only if time-sensitive)
105
+ Skip if the content isn't date-dependent. If it is: key events and timeline notes.
106
+
107
+ ### 5. Quotes
108
+ Direct quotes worth preserving, with attribution and context.
109
+
110
+ ### 6. Patterns & Themes ⭐ (highest compound value)
111
+ Patterns you see in this material and how they connect to broader work — strategy, product thinking, market dynamics, creative practice, etc.
112
+ For each: the pattern, the connection to operator context, and strength (strong/moderate/emerging).
113
+
114
+ ### 7. Relevance
115
+ Which areas of work this is relevant to and why.
116
+ Rate each: high/medium/low with explanation.
117
+
118
+ ### 8. Actionable
119
+ Content seeds, follow-ups, research threads, outreach opportunities.
120
+ For each: type, description, priority.
121
+
122
+ ### 9. Extraction Notes
123
+ Your observations about the extraction process itself — what was rich, what was thin, whether a second pass would surface more.
124
+
125
+ ## Output Format
126
+ Return a JSON object matching this schema. Be specific in patterns and relevance — generic observations aren't useful. Connect everything to real work context.
127
+
128
+ \`\`\`json
129
+ {
130
+ "sourceId": "extract-${Date.now()}",
131
+ "sourceUrl": "${sourceUrl}",
132
+ "sourceTitle": "${sourceTitle}",
133
+ "extractedAt": "${new Date().toISOString()}",
134
+ "summary": "...",
135
+ "claims": [{ "claim": "...", "evidence": "...", "confidence": "high|medium|low", "implicit": false, "notes": "..." }],
136
+ "entities": [{ "name": "...", "type": "person|org|concept|product|technology", "role": "..." }],
137
+ "temporal": null,
138
+ "quotes": [{ "text": "...", "attribution": "...", "context": "..." }],
139
+ "patterns": [{ "pattern": "...", "connection": "...", "strength": "strong|moderate|emerging" }],
140
+ "relevance": [{ "area": "...", "rating": "high|medium|low", "why": "..." }],
141
+ "actionable": [{ "type": "content_seed|follow_up|design_audit|research|outreach", "description": "...", "priority": "high|medium|low" }],
142
+ "extractionNotes": { "observations": ["..."], "schemaFitness": "...", "secondPassCandidate": false }
143
+ }
144
+ \`\`\``;
145
+ }
146
+
147
+ /**
148
+ * Returns a prompt for the "clubtone" / content seed pass.
149
+ * Takes extraction output and turns it into usable content directions.
150
+ */
151
+ export function buildContentSeedPrompt(extraction: L2Extraction, userContext?: string): string {
152
+ const context = userContext
153
+ ? `\n## User Context\n${userContext}\n`
154
+ : "";
155
+
156
+ return `You have a structured extraction from "${extraction.sourceTitle}". Now turn it into content directions — things the user could write, post, discuss, or build on.
157
+
158
+ ## Extraction Summary
159
+ ${extraction.summary}
160
+
161
+ ## Key Patterns
162
+ ${extraction.patterns.map((p) => `- **${p.pattern}** (${p.strength}): ${p.connection}`).join("\n")}
163
+
164
+ ## Notable Claims
165
+ ${extraction.claims.slice(0, 5).map((c) => `- ${c.claim} [${c.confidence}]`).join("\n")}
166
+
167
+ ## Actionable Items
168
+ ${extraction.actionable.map((a) => `- [${a.type}] ${a.description} (${a.priority})`).join("\n")}
169
+
170
+ ## Best Quotes
171
+ ${extraction.quotes.slice(0, 3).map((q) => `- "${q.text}" — ${q.attribution}`).join("\n")}
172
+ ${context}
173
+ ## What to Produce
174
+
175
+ Generate 3-5 **content directions** the user could take. For each:
176
+
177
+ 1. **Hook** — The opening line or angle (something that would stop a scroll)
178
+ 2. **Core argument** — What you'd actually say, grounded in the source material
179
+ 3. **Format** — Tweet thread, short post, essay section, newsletter bit, conversation starter
180
+ 4. **Source grounding** — Which extraction dimensions feed this (cite specific claims, patterns, quotes)
181
+ 5. **Voice note** — How you'd explain this to a friend in 2 sentences (this is the vibe check)
182
+
183
+ Don't be generic. These should feel like things only someone who deeply read this source would write. The user's voice, not the source's voice.`;
184
+ }
@@ -13,6 +13,8 @@ import { getApiBase, getStoredToken, storeToken } from "./auth.js";
13
13
  import { registerAddInterceptor } from "./commands/add.js";
14
14
  import { registerSeedclubCommand } from "./commands/seedclub.js";
15
15
  import { registerSortCommand } from "./commands/sort.js";
16
+ import { registerExtractCommand } from "./commands/extract.js";
17
+ import { registerExtractionTools } from "./tools/extractions.js";
16
18
  import { registerSignalTools } from "./tools/signals.js";
17
19
  import { getCurrentUser, registerUtilityTools } from "./tools/utility.js";
18
20
  import registerBrandingGuard from "./branding.js";
@@ -34,6 +36,8 @@ export default function (pi: ExtensionAPI) {
34
36
  registerSeedclubCommand(pi, { connect, disconnect });
35
37
  registerAddInterceptor(pi);
36
38
  registerSortCommand(pi);
39
+ registerExtractCommand(pi);
40
+ registerExtractionTools(pi);
37
41
 
38
42
  // Show connection status on session start
39
43
  pi.on("session_start", async (_event, ctx) => {
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Extraction tools — save and retrieve extractions from the Seed Club API.
3
+ *
4
+ * These tools let the LLM persist extractions after running /extract,
5
+ * and save content seeds after running /clubtone.
6
+ */
7
+
8
+ import { Text } from "@mariozechner/pi-tui";
9
+ import { Type } from "@sinclair/typebox";
10
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
11
+ import { ApiError, api } from "../api-client.js";
12
+ import { wrapExecute } from "../tool-utils.js";
13
+
14
+ // ── API handlers ────────────────────────────────────────────────────────
15
+
16
+ async function saveExtraction(args: {
17
+ sourceUrl?: string;
18
+ sourceTitle: string;
19
+ sourceType?: string;
20
+ rawContent?: string;
21
+ fetchMethod?: string;
22
+ summary?: string;
23
+ claims?: any[];
24
+ entities?: any[];
25
+ temporal?: any;
26
+ quotes?: any[];
27
+ patterns?: any[];
28
+ relevance?: any[];
29
+ actionable?: any[];
30
+ extractionNotes?: any;
31
+ depth?: string;
32
+ passCount?: number;
33
+ }) {
34
+ try {
35
+ const response = await api.post<any>("/extractions", args);
36
+ return {
37
+ id: response.extraction.id,
38
+ sourceTitle: response.extraction.sourceTitle,
39
+ status: response.extraction.status,
40
+ };
41
+ } catch (error) {
42
+ if (error instanceof ApiError) return { error: error.message, status: error.status };
43
+ throw error;
44
+ }
45
+ }
46
+
47
+ async function saveContentSeeds(args: {
48
+ seeds: Array<{
49
+ extractionId: string;
50
+ hook: string;
51
+ coreArgument: string;
52
+ format: string;
53
+ sourceGrounding: string;
54
+ voiceNote: string;
55
+ userContext?: string;
56
+ }>;
57
+ }) {
58
+ try {
59
+ const response = await api.post<any>("/extractions", args);
60
+ return { count: response.count, seeds: response.seeds?.map((s: any) => s.id) };
61
+ } catch (error) {
62
+ if (error instanceof ApiError) return { error: error.message, status: error.status };
63
+ throw error;
64
+ }
65
+ }
66
+
67
+ async function listExtractions(args: { limit?: number; search?: string }) {
68
+ try {
69
+ const params: Record<string, string | number | undefined> = {};
70
+ if (args.limit) params.limit = args.limit;
71
+ if (args.search) params.search = args.search;
72
+ const response = await api.get<any>("/extractions", params);
73
+ return { extractions: response.extractions, total: response.total };
74
+ } catch (error) {
75
+ if (error instanceof ApiError) return { error: error.message, status: error.status };
76
+ throw error;
77
+ }
78
+ }
79
+
80
+ async function getExtraction(args: { id: string; seeds?: boolean }) {
81
+ try {
82
+ const params: Record<string, string | number | undefined> = { id: args.id };
83
+ if (args.seeds) params.seeds = "true";
84
+ const response = await api.get<any>("/extractions", params);
85
+ return { extraction: response.extraction, seeds: response.seeds };
86
+ } catch (error) {
87
+ if (error instanceof ApiError) return { error: error.message, status: error.status };
88
+ throw error;
89
+ }
90
+ }
91
+
92
+ // ── Tool registration ───────────────────────────────────────────────────
93
+
94
+ export function registerExtractionTools(pi: ExtensionAPI) {
95
+ pi.registerTool({
96
+ name: "seed_save_extraction",
97
+ label: "Save Extraction",
98
+ description:
99
+ "Save a structured extraction to persistent memory. Call this after completing a /extract analysis to store the 9-dimension extraction. Returns the extraction ID needed for saving content seeds.",
100
+ parameters: Type.Object({
101
+ sourceUrl: Type.Optional(Type.String({ description: "Source URL if from web" })),
102
+ sourceTitle: Type.String({ description: "Title of the source" }),
103
+ sourceType: Type.Optional(Type.String({ description: "article, tweet, essay, conversation, concept" })),
104
+ summary: Type.Optional(Type.String({ description: "2-3 sentence summary" })),
105
+ claims: Type.Optional(Type.Array(Type.Object({
106
+ claim: Type.String(),
107
+ evidence: Type.String(),
108
+ confidence: Type.String(),
109
+ implicit: Type.Boolean(),
110
+ notes: Type.Optional(Type.String()),
111
+ }))),
112
+ entities: Type.Optional(Type.Array(Type.Object({
113
+ name: Type.String(),
114
+ type: Type.String(),
115
+ role: Type.String(),
116
+ }))),
117
+ quotes: Type.Optional(Type.Array(Type.Object({
118
+ text: Type.String(),
119
+ attribution: Type.String(),
120
+ context: Type.Optional(Type.String()),
121
+ }))),
122
+ patterns: Type.Optional(Type.Array(Type.Object({
123
+ pattern: Type.String(),
124
+ connection: Type.String(),
125
+ strength: Type.String(),
126
+ }))),
127
+ relevance: Type.Optional(Type.Array(Type.Object({
128
+ area: Type.String(),
129
+ rating: Type.String(),
130
+ why: Type.String(),
131
+ }))),
132
+ actionable: Type.Optional(Type.Array(Type.Object({
133
+ type: Type.String(),
134
+ description: Type.String(),
135
+ priority: Type.String(),
136
+ }))),
137
+ extractionNotes: Type.Optional(Type.Object({
138
+ observations: Type.Array(Type.String()),
139
+ schemaFitness: Type.String(),
140
+ secondPassCandidate: Type.Boolean(),
141
+ })),
142
+ }),
143
+ execute: wrapExecute(saveExtraction),
144
+ renderResult(result: any, _opts: any, theme: any) {
145
+ if (result.isError) return new Text(theme.fg("error", result.content?.[0]?.text || "Error"), 0, 0);
146
+ const d = result.details || {};
147
+ if (d.error) return new Text(theme.fg("error", d.error), 0, 0);
148
+ return new Text(
149
+ theme.fg("success", `✓ Saved extraction: ${d.sourceTitle || d.id}`),
150
+ 0, 0,
151
+ );
152
+ },
153
+ });
154
+
155
+ pi.registerTool({
156
+ name: "seed_save_content_seeds",
157
+ label: "Save Content Seeds",
158
+ description:
159
+ "Save content seeds (content directions) generated from a /clubtone pass. Each seed needs the extractionId from a saved extraction.",
160
+ parameters: Type.Object({
161
+ seeds: Type.Array(Type.Object({
162
+ extractionId: Type.String({ description: "ID of the source extraction" }),
163
+ hook: Type.String({ description: "Opening line / angle" }),
164
+ coreArgument: Type.String({ description: "What you'd actually say" }),
165
+ format: Type.String({ description: "tweet_thread, short_post, essay, newsletter, etc." }),
166
+ sourceGrounding: Type.String({ description: "Which extraction dimensions feed this" }),
167
+ voiceNote: Type.String({ description: "2-sentence friend explanation" }),
168
+ userContext: Type.Optional(Type.String({ description: "User context that shaped this" })),
169
+ })),
170
+ }),
171
+ execute: wrapExecute(saveContentSeeds),
172
+ renderResult(result: any, _opts: any, theme: any) {
173
+ if (result.isError) return new Text(theme.fg("error", result.content?.[0]?.text || "Error"), 0, 0);
174
+ const d = result.details || {};
175
+ if (d.error) return new Text(theme.fg("error", d.error), 0, 0);
176
+ return new Text(
177
+ theme.fg("success", `✓ Saved ${d.count || 0} content seeds`),
178
+ 0, 0,
179
+ );
180
+ },
181
+ });
182
+
183
+ pi.registerTool({
184
+ name: "seed_list_extractions",
185
+ label: "List Extractions",
186
+ description:
187
+ "List the user's saved extractions. Shows source, summary, and counts of patterns/claims/actionable items.",
188
+ parameters: Type.Object({
189
+ limit: Type.Optional(Type.Number({ description: "Max results (default 20)" })),
190
+ search: Type.Optional(Type.String({ description: "Search by title or summary" })),
191
+ }),
192
+ execute: wrapExecute(listExtractions),
193
+ renderResult(result: any, { expanded }: any, theme: any) {
194
+ if (result.isError) return new Text(theme.fg("error", result.content?.[0]?.text || "Error"), 0, 0);
195
+ const d = result.details || {};
196
+ if (!d.extractions?.length) return new Text(theme.fg("dim", "No extractions yet"), 0, 0);
197
+ let text = theme.fg("muted", `${d.total} extractions`);
198
+ for (const e of d.extractions.slice(0, expanded ? 50 : 10)) {
199
+ const counts = [
200
+ e.patternCount && `${e.patternCount}p`,
201
+ e.claimCount && `${e.claimCount}c`,
202
+ e.actionableCount && `${e.actionableCount}a`,
203
+ ].filter(Boolean).join("/");
204
+ text += `\n ${e.sourceTitle}${counts ? ` [${counts}]` : ""} — ${e.status}`;
205
+ }
206
+ return new Text(text, 0, 0);
207
+ },
208
+ });
209
+
210
+ pi.registerTool({
211
+ name: "seed_get_extraction",
212
+ label: "Get Extraction",
213
+ description:
214
+ "Get a specific extraction by ID, with full 9-dimension data. Optionally include content seeds.",
215
+ parameters: Type.Object({
216
+ id: Type.String({ description: "Extraction ID" }),
217
+ seeds: Type.Optional(Type.Boolean({ description: "Include content seeds" })),
218
+ }),
219
+ execute: wrapExecute(getExtraction),
220
+ });
221
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubnet/seedclub",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "The Human+ Venture Network — AI agent for deal sourcing, research, and signal tracking",
5
5
  "license": "MIT",
6
6
  "repository": {