@clubnet/seedclub 0.2.10 → 0.2.13
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.
|
@@ -1,169 +1,123 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* /extract
|
|
2
|
+
* /extract, /clubtone, /extractions commands.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
4
|
+
* Key design: the LLM responds BY calling the save tool.
|
|
5
|
+
* The tool parameters ARE the extraction. No text-then-save — saving IS responding.
|
|
13
6
|
*/
|
|
14
7
|
|
|
15
8
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
16
9
|
import { capture } from "../extraction/capture.js";
|
|
17
|
-
import { buildExtractionPrompt
|
|
10
|
+
import { buildExtractionPrompt } from "../extraction/schema.js";
|
|
18
11
|
|
|
19
12
|
export function registerExtractCommand(pi: ExtensionAPI) {
|
|
20
|
-
// ── /extract command ──────────────────────────────────────────────
|
|
21
|
-
|
|
22
13
|
pi.registerCommand("extract", {
|
|
23
14
|
description: "Extract structured knowledge from a URL or conversation content",
|
|
24
15
|
handler: async (args, ctx) => {
|
|
25
16
|
const arg = args?.trim() || "";
|
|
26
17
|
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
);
|
|
18
|
+
if (arg === "clubtone" || arg === "club" || arg === "tone") {
|
|
19
|
+
runClubtone(pi);
|
|
39
20
|
return;
|
|
40
21
|
}
|
|
41
22
|
|
|
42
|
-
// /extract <url> — capture + extract
|
|
43
23
|
const url = extractUrl(arg);
|
|
44
24
|
if (url) {
|
|
45
|
-
ctx.ui.notify(
|
|
46
|
-
|
|
25
|
+
ctx.ui.notify("Capturing " + url + "...", "info");
|
|
47
26
|
const result = await capture(url, pi);
|
|
48
27
|
if (!result.complete || !result.content) {
|
|
49
|
-
ctx.ui.notify(
|
|
28
|
+
ctx.ui.notify("Failed to capture " + url + (result.note ? ": " + result.note : ""), "error");
|
|
50
29
|
return;
|
|
51
30
|
}
|
|
52
|
-
|
|
53
31
|
const title = extractTitle(result.content) || new URL(url).hostname;
|
|
54
|
-
ctx.ui.notify(
|
|
55
|
-
|
|
56
|
-
// Truncate if massive — keep first 40K chars for extraction
|
|
32
|
+
ctx.ui.notify("Captured (" + result.fetchMethod + "). Running extraction...", "info");
|
|
57
33
|
const content = result.content.length > 40000
|
|
58
34
|
? result.content.slice(0, 40000) + "\n\n[Content truncated at 40K chars]"
|
|
59
35
|
: result.content;
|
|
60
|
-
|
|
61
36
|
const prompt = buildExtractionPrompt(content, url, title);
|
|
62
37
|
pi.sendUserMessage(
|
|
63
38
|
prompt +
|
|
64
|
-
"
|
|
65
|
-
|
|
66
|
-
|
|
39
|
+
"\n\nIMPORTANT: Respond ONLY by calling the seed_save_extraction tool with your complete analysis as the parameters. " +
|
|
40
|
+
"Do not write the extraction as text first. The tool call IS your response. " +
|
|
41
|
+
"Fill in every dimension directly in the tool parameters: sourceUrl, sourceTitle, sourceType, summary, claims, entities, quotes, patterns, relevance, actionable, extractionNotes. " +
|
|
42
|
+
"After the tool confirms the save, give a brief summary of what you found."
|
|
67
43
|
);
|
|
68
44
|
return;
|
|
69
45
|
}
|
|
70
46
|
|
|
71
|
-
// /extract (no args) — extract from conversation context
|
|
72
47
|
if (!arg) {
|
|
73
48
|
pi.sendUserMessage(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
After
|
|
49
|
+
"Look at the content shared in this conversation and extract structured knowledge from it.\n\n" +
|
|
50
|
+
"IMPORTANT: Respond ONLY by calling the seed_save_extraction tool. " +
|
|
51
|
+
"Do not write the extraction as text. The tool call IS your response.\n\n" +
|
|
52
|
+
"Fill in all 9 dimensions as tool parameters:\n" +
|
|
53
|
+
"- sourceTitle, sourceType, summary (2-3 sentences)\n" +
|
|
54
|
+
"- claims (every claim with evidence, confidence, implicit flag)\n" +
|
|
55
|
+
"- entities (people, orgs, concepts, products, technologies)\n" +
|
|
56
|
+
"- quotes (direct quotes worth preserving)\n" +
|
|
57
|
+
"- patterns (how themes connect to broader work — highest value dimension)\n" +
|
|
58
|
+
"- relevance (which areas this matters for and why)\n" +
|
|
59
|
+
"- actionable (content seeds, follow-ups, research threads)\n" +
|
|
60
|
+
"- extractionNotes (observations, schema fitness, second pass candidate)\n\n" +
|
|
61
|
+
"Be thorough. After the tool confirms the save, give a brief summary of what you found."
|
|
87
62
|
);
|
|
88
63
|
return;
|
|
89
64
|
}
|
|
90
65
|
|
|
91
|
-
// /extract <something that's not a URL> — treat as a topic/concept
|
|
92
66
|
pi.sendUserMessage(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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.`,
|
|
67
|
+
"Extract structured knowledge from the concept: \"" + arg + "\"\n\n" +
|
|
68
|
+
"IMPORTANT: Respond ONLY by calling the seed_save_extraction tool with your analysis as the parameters. " +
|
|
69
|
+
"Do not write the extraction as text first. The tool call IS your response. " +
|
|
70
|
+
"Fill in all 9 dimensions (sourceTitle, summary, claims, entities, quotes, patterns, relevance, actionable, extractionNotes). " +
|
|
71
|
+
"After the tool confirms the save, give a brief summary of what you found."
|
|
107
72
|
);
|
|
108
73
|
},
|
|
109
74
|
});
|
|
110
75
|
|
|
111
|
-
// ── /clubtone shorthand ───────────────────────────────────────────
|
|
112
|
-
|
|
113
76
|
pi.registerCommand("clubtone", {
|
|
114
77
|
description: "Turn extracted knowledge into content directions",
|
|
115
|
-
handler: async (args,
|
|
116
|
-
|
|
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
|
-
);
|
|
78
|
+
handler: async (args, _ctx) => {
|
|
79
|
+
runClubtone(pi, args?.trim());
|
|
133
80
|
},
|
|
134
81
|
});
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// ── /extractions — list saved extractions ─────────────────────────────
|
|
138
82
|
|
|
139
83
|
pi.registerCommand("extractions", {
|
|
140
84
|
description: "Browse your saved extractions",
|
|
141
|
-
handler: async (
|
|
85
|
+
handler: async (_args, _ctx) => {
|
|
142
86
|
pi.sendUserMessage(
|
|
143
|
-
"
|
|
87
|
+
"Call seed_list_extractions now. Show results in a clean format with title, date, and counts of patterns/claims/actionable items."
|
|
144
88
|
);
|
|
145
89
|
},
|
|
146
90
|
});
|
|
91
|
+
}
|
|
147
92
|
|
|
148
|
-
|
|
93
|
+
function runClubtone(pi: ExtensionAPI, context?: string) {
|
|
94
|
+
const extra = context ? "\n\nAdditional context: " + context : "";
|
|
95
|
+
pi.sendUserMessage(
|
|
96
|
+
"Look at the most recent extraction in our conversation. Generate 3-5 content directions.\n\n" +
|
|
97
|
+
"IMPORTANT: Respond ONLY by calling the seed_save_content_seeds tool. " +
|
|
98
|
+
"Do not write the directions as text first. The tool call IS your response.\n\n" +
|
|
99
|
+
"For each seed, fill in the tool parameters:\n" +
|
|
100
|
+
"- extractionId (from the most recent seed_save_extraction result)\n" +
|
|
101
|
+
"- hook (opening line that stops a scroll)\n" +
|
|
102
|
+
"- coreArgument (what I would actually say, grounded in the source)\n" +
|
|
103
|
+
"- format (tweet_thread, short_post, essay, newsletter, conversation_starter)\n" +
|
|
104
|
+
"- sourceGrounding (which claims, patterns, quotes feed this)\n" +
|
|
105
|
+
"- voiceNote (how I would explain this to a friend in 2 sentences)\n\n" +
|
|
106
|
+
"Do not be generic. My voice, not the source voice. " +
|
|
107
|
+
"After the tool confirms, give a brief summary of the directions." + extra
|
|
108
|
+
);
|
|
109
|
+
}
|
|
149
110
|
|
|
150
111
|
function extractUrl(text: string): string | null {
|
|
151
|
-
// Direct URL
|
|
152
112
|
if (text.match(/^https?:\/\//)) return text.split(/\s/)[0];
|
|
153
|
-
|
|
154
|
-
// URL somewhere in text
|
|
155
113
|
const match = text.match(/(https?:\/\/[^\s]+)/);
|
|
156
114
|
return match ? match[1] : null;
|
|
157
115
|
}
|
|
158
116
|
|
|
159
117
|
function extractTitle(markdown: string): string | null {
|
|
160
|
-
// Try to find a title from Jina Reader markdown output
|
|
161
118
|
const titleMatch = markdown.match(/^#\s+(.+)$/m);
|
|
162
119
|
if (titleMatch) return titleMatch[1].trim();
|
|
163
|
-
|
|
164
|
-
// Try Title: header
|
|
165
120
|
const metaMatch = markdown.match(/^Title:\s*(.+)$/m);
|
|
166
121
|
if (metaMatch) return metaMatch[1].trim();
|
|
167
|
-
|
|
168
122
|
return null;
|
|
169
123
|
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* L1 Raw Capture
|
|
3
|
-
*
|
|
4
|
-
* Uses Jina Reader (r.jina.ai) for articles/pages.
|
|
5
|
-
* Returns markdown content without LLM processing.
|
|
2
|
+
* L1 Raw Capture - fetch full-fidelity content from URLs.
|
|
3
|
+
* Uses Jina Reader for articles, FxTwitter for tweets.
|
|
6
4
|
*/
|
|
7
5
|
|
|
8
|
-
import type { ExtensionAPI } from "@mariozechner/
|
|
6
|
+
import type { ExtensionAPI } from "@mariozechner/Seed Club-coding-agent";
|
|
9
7
|
|
|
10
8
|
export interface CaptureResult {
|
|
11
9
|
content: string;
|
|
@@ -15,74 +13,32 @@ export interface CaptureResult {
|
|
|
15
13
|
note?: string;
|
|
16
14
|
}
|
|
17
15
|
|
|
18
|
-
/**
|
|
19
|
-
* Fetch full content from a URL via Jina Reader.
|
|
20
|
-
* Returns clean markdown — no summarization, no LLM processing.
|
|
21
|
-
*/
|
|
22
16
|
export async function captureUrl(url: string, pi: ExtensionAPI): Promise<CaptureResult> {
|
|
23
|
-
const jinaUrl =
|
|
24
|
-
|
|
25
|
-
const result = await pi.exec("curl", ["-sL", "-H", "Accept: text/markdown", jinaUrl], {
|
|
26
|
-
timeout: 30000,
|
|
27
|
-
});
|
|
17
|
+
const jinaUrl = "https://r.jina.ai/" + url;
|
|
18
|
+
const result = await pi.exec("curl", ["-sL", "-H", "Accept: text/markdown", jinaUrl], { timeout: 30000 });
|
|
28
19
|
|
|
29
20
|
if (result.code !== 0 || !result.stdout?.trim()) {
|
|
30
|
-
// Fallback: try direct fetch
|
|
31
21
|
const direct = await pi.exec("curl", ["-sL", url], { timeout: 15000 });
|
|
32
22
|
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
|
-
};
|
|
23
|
+
return { content: direct.stdout, contentType: "html", fetchMethod: "direct-curl", complete: true, note: "Jina Reader failed; raw HTML captured" };
|
|
40
24
|
}
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
content: "",
|
|
44
|
-
contentType: "text",
|
|
45
|
-
fetchMethod: "failed",
|
|
46
|
-
complete: false,
|
|
47
|
-
note: `Fetch failed for ${url}`,
|
|
48
|
-
};
|
|
25
|
+
return { content: "", contentType: "text", fetchMethod: "failed", complete: false, note: "Fetch failed for " + url };
|
|
49
26
|
}
|
|
50
27
|
|
|
51
|
-
return {
|
|
52
|
-
content: result.stdout,
|
|
53
|
-
contentType: "markdown",
|
|
54
|
-
fetchMethod: "jina-reader",
|
|
55
|
-
complete: true,
|
|
56
|
-
};
|
|
28
|
+
return { content: result.stdout, contentType: "markdown", fetchMethod: "jina-reader", complete: true };
|
|
57
29
|
}
|
|
58
30
|
|
|
59
|
-
/**
|
|
60
|
-
* Capture a tweet via FxTwitter API (returns JSON with full tweet data).
|
|
61
|
-
*/
|
|
62
31
|
export async function captureTweet(url: string, pi: ExtensionAPI): Promise<CaptureResult> {
|
|
63
|
-
|
|
64
|
-
const fxUrl = url
|
|
65
|
-
.replace("x.com", "api.fxtwitter.com")
|
|
66
|
-
.replace("twitter.com", "api.fxtwitter.com");
|
|
67
|
-
|
|
32
|
+
const fxUrl = url.replace("x.com", "api.fxtwitter.com").replace("twitter.com", "api.fxtwitter.com");
|
|
68
33
|
const result = await pi.exec("curl", ["-sL", fxUrl], { timeout: 15000 });
|
|
69
34
|
|
|
70
35
|
if (result.code !== 0 || !result.stdout?.trim()) {
|
|
71
|
-
// Fallback to Jina
|
|
72
36
|
return captureUrl(url, pi);
|
|
73
37
|
}
|
|
74
38
|
|
|
75
|
-
return {
|
|
76
|
-
content: result.stdout,
|
|
77
|
-
contentType: "text",
|
|
78
|
-
fetchMethod: "fxtwitter",
|
|
79
|
-
complete: true,
|
|
80
|
-
};
|
|
39
|
+
return { content: result.stdout, contentType: "text", fetchMethod: "fxtwitter", complete: true };
|
|
81
40
|
}
|
|
82
41
|
|
|
83
|
-
/**
|
|
84
|
-
* Auto-detect content type and use the right capture method.
|
|
85
|
-
*/
|
|
86
42
|
export async function capture(url: string, pi: ExtensionAPI): Promise<CaptureResult> {
|
|
87
43
|
if (url.match(/(?:x\.com|twitter\.com)\/\w+\/status\/\d+/)) {
|
|
88
44
|
return captureTweet(url, pi);
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* L2 Extraction Schema
|
|
3
|
-
*
|
|
4
|
-
* Every source gets extracted into these dimensions.
|
|
5
|
-
* This is where value compounds — connecting source material to your context.
|
|
2
|
+
* L2 Extraction Schema - the 9-dimension extraction format.
|
|
6
3
|
*/
|
|
7
4
|
|
|
8
5
|
export interface Claim {
|
|
@@ -27,7 +24,7 @@ export interface Quote {
|
|
|
27
24
|
|
|
28
25
|
export interface Pattern {
|
|
29
26
|
pattern: string;
|
|
30
|
-
connection: string;
|
|
27
|
+
connection: string;
|
|
31
28
|
strength: "strong" | "moderate" | "emerging";
|
|
32
29
|
}
|
|
33
30
|
|
|
@@ -49,20 +46,15 @@ export interface ExtractionNotes {
|
|
|
49
46
|
secondPassCandidate: boolean;
|
|
50
47
|
}
|
|
51
48
|
|
|
52
|
-
/**
|
|
53
|
-
* The full L2 extraction output for a single source.
|
|
54
|
-
*/
|
|
55
49
|
export interface L2Extraction {
|
|
56
50
|
sourceId: string;
|
|
57
51
|
sourceUrl: string;
|
|
58
52
|
sourceTitle: string;
|
|
59
53
|
extractedAt: string;
|
|
60
|
-
|
|
61
|
-
// The 9 dimensions
|
|
62
54
|
summary: string;
|
|
63
55
|
claims: Claim[];
|
|
64
56
|
entities: Entity[];
|
|
65
|
-
temporal?: { events: string[]; note: string };
|
|
57
|
+
temporal?: { events: string[]; note: string };
|
|
66
58
|
quotes: Quote[];
|
|
67
59
|
patterns: Pattern[];
|
|
68
60
|
relevance: Relevance[];
|
|
@@ -70,115 +62,56 @@ export interface L2Extraction {
|
|
|
70
62
|
extractionNotes: ExtractionNotes;
|
|
71
63
|
}
|
|
72
64
|
|
|
73
|
-
/**
|
|
74
|
-
* Returns the extraction prompt for Claude.
|
|
75
|
-
* This is what gets injected when the user runs /extract.
|
|
76
|
-
*/
|
|
77
65
|
export function buildExtractionPrompt(sourceContent: string, sourceUrl: string, sourceTitle: string): string {
|
|
78
|
-
return
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
- **
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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.`;
|
|
66
|
+
return [
|
|
67
|
+
"Extract structured knowledge from this source material using the 9-dimension schema below.",
|
|
68
|
+
"",
|
|
69
|
+
"## Source",
|
|
70
|
+
"- **URL:** " + sourceUrl,
|
|
71
|
+
"- **Title:** " + sourceTitle,
|
|
72
|
+
"",
|
|
73
|
+
"## Content",
|
|
74
|
+
"<source_content>",
|
|
75
|
+
sourceContent,
|
|
76
|
+
"</source_content>",
|
|
77
|
+
"",
|
|
78
|
+
"## Extraction Schema (9 Dimensions)",
|
|
79
|
+
"",
|
|
80
|
+
"Extract into these dimensions. Be thorough.",
|
|
81
|
+
"",
|
|
82
|
+
"### 1. Summary",
|
|
83
|
+
"2-3 sentences capturing the core contribution.",
|
|
84
|
+
"",
|
|
85
|
+
"### 2. Claims & Arguments",
|
|
86
|
+
"Every claim the source makes, including implicit ones.",
|
|
87
|
+
"For each: the claim, supporting evidence, confidence (high/medium/low), whether implicit, and any notes.",
|
|
88
|
+
"",
|
|
89
|
+
"### 3. Entities",
|
|
90
|
+
"People, organizations, concepts, products, technologies mentioned.",
|
|
91
|
+
"For each: name, type, and their role in the source.",
|
|
92
|
+
"",
|
|
93
|
+
"### 4. Temporal (only if time-sensitive)",
|
|
94
|
+
"Skip if the content is not date-dependent. If it is: key events and timeline notes.",
|
|
95
|
+
"",
|
|
96
|
+
"### 5. Quotes",
|
|
97
|
+
"Direct quotes worth preserving, with attribution and context.",
|
|
98
|
+
"",
|
|
99
|
+
"### 6. Patterns & Themes (highest compound value)",
|
|
100
|
+
"Patterns you see in this material and how they connect to broader work.",
|
|
101
|
+
"For each: the pattern, the connection to operator context, and strength (strong/moderate/emerging).",
|
|
102
|
+
"",
|
|
103
|
+
"### 7. Relevance",
|
|
104
|
+
"Which areas of work this is relevant to and why.",
|
|
105
|
+
"Rate each: high/medium/low with explanation.",
|
|
106
|
+
"",
|
|
107
|
+
"### 8. Actionable",
|
|
108
|
+
"Content seeds, follow-ups, research threads, outreach opportunities.",
|
|
109
|
+
"For each: type, description, priority.",
|
|
110
|
+
"",
|
|
111
|
+
"### 9. Extraction Notes",
|
|
112
|
+
"Your observations about the extraction process itself.",
|
|
113
|
+
"",
|
|
114
|
+
"## Output Format",
|
|
115
|
+
"Return a JSON object with all 9 dimensions. Be specific in patterns and relevance.",
|
|
116
|
+
].join("\n");
|
|
184
117
|
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Extraction tools —
|
|
2
|
+
* Extraction tools — the full L0-L4 tool surface.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* L0: seed_register_source — register a source before extraction
|
|
5
|
+
* L2: seed_save_extraction — save a 9-dimension extraction (tool call IS the save)
|
|
6
|
+
* L3: seed_synthesize — cross-source synthesis from 2+ extractions
|
|
7
|
+
* L4: seed_distill — promote a pattern to stable concept
|
|
8
|
+
* Content: seed_save_content_seeds — save content directions from extraction or synthesis
|
|
9
|
+
* Read: seed_list_extractions, seed_get_extraction, seed_list_sources, seed_list_syntheses
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
12
|
import { Text } from "@mariozechner/pi-tui";
|
|
@@ -13,10 +17,30 @@ import { wrapExecute } from "../tool-utils.js";
|
|
|
13
17
|
|
|
14
18
|
// ── API handlers ────────────────────────────────────────────────────────
|
|
15
19
|
|
|
20
|
+
async function registerSource(args: {
|
|
21
|
+
url?: string;
|
|
22
|
+
title: string;
|
|
23
|
+
sourceType?: string;
|
|
24
|
+
author?: string;
|
|
25
|
+
provenance?: string;
|
|
26
|
+
tier?: string;
|
|
27
|
+
discoveryTrace?: string;
|
|
28
|
+
credibilityNotes?: string;
|
|
29
|
+
}) {
|
|
30
|
+
try {
|
|
31
|
+
const response = await api.post<any>("/extractions/sources", args);
|
|
32
|
+
return { id: response.source.id, title: response.source.title, status: response.source.status };
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error instanceof ApiError) return { error: error.message, status: error.status };
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
16
39
|
async function saveExtraction(args: {
|
|
17
40
|
sourceUrl?: string;
|
|
18
41
|
sourceTitle: string;
|
|
19
42
|
sourceType?: string;
|
|
43
|
+
sourceId?: string;
|
|
20
44
|
rawContent?: string;
|
|
21
45
|
fetchMethod?: string;
|
|
22
46
|
summary?: string;
|
|
@@ -44,9 +68,48 @@ async function saveExtraction(args: {
|
|
|
44
68
|
}
|
|
45
69
|
}
|
|
46
70
|
|
|
71
|
+
async function synthesize(args: {
|
|
72
|
+
title: string;
|
|
73
|
+
thesis?: string;
|
|
74
|
+
extractionIds: string[];
|
|
75
|
+
patterns?: any[];
|
|
76
|
+
entityResolution?: any[];
|
|
77
|
+
themeConvergence?: any[];
|
|
78
|
+
contradictions?: any[];
|
|
79
|
+
bridges?: any[];
|
|
80
|
+
emergentInsights?: string[];
|
|
81
|
+
trigger?: string;
|
|
82
|
+
}) {
|
|
83
|
+
try {
|
|
84
|
+
const response = await api.post<any>("/extractions/syntheses", args);
|
|
85
|
+
return { id: response.synthesis.id, title: response.synthesis.title, extractionCount: response.synthesis.extractionCount };
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (error instanceof ApiError) return { error: error.message, status: error.status };
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function distill(args: {
|
|
93
|
+
title: string;
|
|
94
|
+
concept: string;
|
|
95
|
+
evidenceSummary?: string;
|
|
96
|
+
synthesisIds: string[];
|
|
97
|
+
tags?: string[];
|
|
98
|
+
relatedEntities?: any[];
|
|
99
|
+
}) {
|
|
100
|
+
try {
|
|
101
|
+
const response = await api.post<any>("/extractions/distillations", args);
|
|
102
|
+
return { id: response.distillation.id, title: response.distillation.title, synthesisCount: response.distillation.synthesisCount };
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (error instanceof ApiError) return { error: error.message, status: error.status };
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
47
109
|
async function saveContentSeeds(args: {
|
|
48
110
|
seeds: Array<{
|
|
49
|
-
extractionId
|
|
111
|
+
extractionId?: string;
|
|
112
|
+
synthesisId?: string;
|
|
50
113
|
hook: string;
|
|
51
114
|
coreArgument: string;
|
|
52
115
|
format: string;
|
|
@@ -56,7 +119,7 @@ async function saveContentSeeds(args: {
|
|
|
56
119
|
}>;
|
|
57
120
|
}) {
|
|
58
121
|
try {
|
|
59
|
-
const response = await api.post<any>("/extractions", args);
|
|
122
|
+
const response = await api.post<any>("/extractions/seeds", args);
|
|
60
123
|
return { count: response.count, seeds: response.seeds?.map((s: any) => s.id) };
|
|
61
124
|
} catch (error) {
|
|
62
125
|
if (error instanceof ApiError) return { error: error.message, status: error.status };
|
|
@@ -89,18 +152,96 @@ async function getExtraction(args: { id: string; seeds?: boolean }) {
|
|
|
89
152
|
}
|
|
90
153
|
}
|
|
91
154
|
|
|
155
|
+
async function listSources(args: { limit?: number; status?: string }) {
|
|
156
|
+
try {
|
|
157
|
+
const params: Record<string, string | number | undefined> = {};
|
|
158
|
+
if (args.limit) params.limit = args.limit;
|
|
159
|
+
if (args.status) params.status = args.status;
|
|
160
|
+
const response = await api.get<any>("/extractions/sources", params);
|
|
161
|
+
return { sources: response.sources, total: response.total };
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (error instanceof ApiError) return { error: error.message, status: error.status };
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function listSyntheses(args: { limit?: number }) {
|
|
169
|
+
try {
|
|
170
|
+
const params: Record<string, string | number | undefined> = {};
|
|
171
|
+
if (args.limit) params.limit = args.limit;
|
|
172
|
+
const response = await api.get<any>("/extractions/syntheses", params);
|
|
173
|
+
return { syntheses: response.syntheses, total: response.total };
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (error instanceof ApiError) return { error: error.message, status: error.status };
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function listSeeds(args: { limit?: number; unused?: boolean; extractionId?: string; synthesisId?: string }) {
|
|
181
|
+
try {
|
|
182
|
+
const params: Record<string, string | number | undefined> = {};
|
|
183
|
+
if (args.limit) params.limit = args.limit;
|
|
184
|
+
if (args.unused) params.unused = "true";
|
|
185
|
+
if (args.extractionId) params.extractionId = args.extractionId;
|
|
186
|
+
if (args.synthesisId) params.synthesisId = args.synthesisId;
|
|
187
|
+
const response = await api.get<any>("/extractions/seeds", params);
|
|
188
|
+
return { seeds: response.seeds, total: response.total };
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (error instanceof ApiError) return { error: error.message, status: error.status };
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function markSeedUsed(args: { id: string }) {
|
|
196
|
+
try {
|
|
197
|
+
const response = await api.post<any>("/extractions/seeds/used", { id: args.id });
|
|
198
|
+
return { id: response.seed.id, used: true };
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (error instanceof ApiError) return { error: error.message, status: error.status };
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
92
205
|
// ── Tool registration ───────────────────────────────────────────────────
|
|
93
206
|
|
|
94
207
|
export function registerExtractionTools(pi: ExtensionAPI) {
|
|
208
|
+
|
|
209
|
+
// ── L0: Source Registration ───────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
pi.registerTool({
|
|
212
|
+
name: "seed_register_source",
|
|
213
|
+
label: "Register Source",
|
|
214
|
+
description: "Register a source in the L0 registry before extraction. Records provenance, credibility, and discovery trace. Returns source ID for linking to extraction.",
|
|
215
|
+
parameters: Type.Object({
|
|
216
|
+
url: Type.Optional(Type.String({ description: "Source URL" })),
|
|
217
|
+
title: Type.String({ description: "Human-readable title" }),
|
|
218
|
+
sourceType: Type.Optional(Type.String({ description: "article, tweet, essay, conversation, voice_note, code, transcript" })),
|
|
219
|
+
author: Type.Optional(Type.String({ description: "Creator(s)" })),
|
|
220
|
+
provenance: Type.Optional(Type.String({ description: "authored, licensed, public, restricted" })),
|
|
221
|
+
tier: Type.Optional(Type.String({ description: "external, internal_external, internal" })),
|
|
222
|
+
discoveryTrace: Type.Optional(Type.String({ description: "How found, who referred, what context" })),
|
|
223
|
+
credibilityNotes: Type.Optional(Type.String({ description: "Credibility assessment" })),
|
|
224
|
+
}),
|
|
225
|
+
execute: wrapExecute(registerSource),
|
|
226
|
+
renderResult(result: any, _opts: any, theme: any) {
|
|
227
|
+
if (result.isError) return new Text(theme.fg("error", result.content?.[0]?.text || "Error"), 0, 0);
|
|
228
|
+
const d = result.details || {};
|
|
229
|
+
if (d.error) return new Text(theme.fg("error", d.error), 0, 0);
|
|
230
|
+
return new Text(theme.fg("success", "Registered: " + d.title + " [" + d.id + "]"), 0, 0);
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// ── L2: Save Extraction ───────────────────────────────────────────────
|
|
235
|
+
|
|
95
236
|
pi.registerTool({
|
|
96
237
|
name: "seed_save_extraction",
|
|
97
238
|
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.",
|
|
239
|
+
description: "Save a structured 9-dimension extraction. The tool call IS the extraction — fill all dimensions as parameters. Returns extraction ID for content seeds and synthesis.",
|
|
100
240
|
parameters: Type.Object({
|
|
101
241
|
sourceUrl: Type.Optional(Type.String({ description: "Source URL if from web" })),
|
|
102
242
|
sourceTitle: Type.String({ description: "Title of the source" }),
|
|
103
243
|
sourceType: Type.Optional(Type.String({ description: "article, tweet, essay, conversation, concept" })),
|
|
244
|
+
sourceId: Type.Optional(Type.String({ description: "L0 source registry ID if registered" })),
|
|
104
245
|
summary: Type.Optional(Type.String({ description: "2-3 sentence summary" })),
|
|
105
246
|
claims: Type.Optional(Type.Array(Type.Object({
|
|
106
247
|
claim: Type.String(),
|
|
@@ -145,23 +286,100 @@ export function registerExtractionTools(pi: ExtensionAPI) {
|
|
|
145
286
|
if (result.isError) return new Text(theme.fg("error", result.content?.[0]?.text || "Error"), 0, 0);
|
|
146
287
|
const d = result.details || {};
|
|
147
288
|
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
|
-
);
|
|
289
|
+
return new Text(theme.fg("success", "Saved extraction: " + (d.sourceTitle || d.id)), 0, 0);
|
|
152
290
|
},
|
|
153
291
|
});
|
|
154
292
|
|
|
293
|
+
// ── L3: Synthesize ────────────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
pi.registerTool({
|
|
296
|
+
name: "seed_synthesize",
|
|
297
|
+
label: "Synthesize",
|
|
298
|
+
description: "Create an L3 cross-source synthesis from 2+ extractions. Identifies patterns, contradictions, entity resolution, theme convergence, and emergent insights across sources. The tool call IS the synthesis.",
|
|
299
|
+
parameters: Type.Object({
|
|
300
|
+
title: Type.String({ description: "Synthesis title" }),
|
|
301
|
+
thesis: Type.Optional(Type.String({ description: "Core insight from crossing these sources" })),
|
|
302
|
+
extractionIds: Type.Array(Type.String({ description: "Extraction IDs to synthesize" })),
|
|
303
|
+
patterns: Type.Optional(Type.Array(Type.Object({
|
|
304
|
+
pattern: Type.String(),
|
|
305
|
+
sources: Type.Array(Type.String()),
|
|
306
|
+
strength: Type.String(),
|
|
307
|
+
connection: Type.String(),
|
|
308
|
+
}))),
|
|
309
|
+
entityResolution: Type.Optional(Type.Array(Type.Object({
|
|
310
|
+
entity: Type.String(),
|
|
311
|
+
appearances: Type.Array(Type.Object({ extractionId: Type.String(), role: Type.String() })),
|
|
312
|
+
mergedUnderstanding: Type.String(),
|
|
313
|
+
}))),
|
|
314
|
+
themeConvergence: Type.Optional(Type.Array(Type.Object({
|
|
315
|
+
theme: Type.String(),
|
|
316
|
+
supportingExtractions: Type.Array(Type.String()),
|
|
317
|
+
divergences: Type.String(),
|
|
318
|
+
synthesisNote: Type.String(),
|
|
319
|
+
}))),
|
|
320
|
+
contradictions: Type.Optional(Type.Array(Type.Object({
|
|
321
|
+
claimA: Type.String(),
|
|
322
|
+
sourceA: Type.String(),
|
|
323
|
+
claimB: Type.String(),
|
|
324
|
+
sourceB: Type.String(),
|
|
325
|
+
resolution: Type.String(),
|
|
326
|
+
}))),
|
|
327
|
+
bridges: Type.Optional(Type.Array(Type.Object({
|
|
328
|
+
fromExtraction: Type.String(),
|
|
329
|
+
toExtraction: Type.String(),
|
|
330
|
+
bridgeInsight: Type.String(),
|
|
331
|
+
}))),
|
|
332
|
+
emergentInsights: Type.Optional(Type.Array(Type.String({ description: "Insights only visible when crossing sources" }))),
|
|
333
|
+
trigger: Type.Optional(Type.String({ description: "manual, theme_cluster, project_need" })),
|
|
334
|
+
}),
|
|
335
|
+
execute: wrapExecute(synthesize),
|
|
336
|
+
renderResult(result: any, _opts: any, theme: any) {
|
|
337
|
+
if (result.isError) return new Text(theme.fg("error", result.content?.[0]?.text || "Error"), 0, 0);
|
|
338
|
+
const d = result.details || {};
|
|
339
|
+
if (d.error) return new Text(theme.fg("error", d.error), 0, 0);
|
|
340
|
+
return new Text(theme.fg("success", "Synthesis: " + d.title + " (" + d.extractionCount + " sources)"), 0, 0);
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// ── L4: Distill ───────────────────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
pi.registerTool({
|
|
347
|
+
name: "seed_distill",
|
|
348
|
+
label: "Distill",
|
|
349
|
+
description: "Promote a pattern to an L4 distillation — a stable concept proven across 3+ syntheses. Returns distillation ID.",
|
|
350
|
+
parameters: Type.Object({
|
|
351
|
+
title: Type.String({ description: "Concept title" }),
|
|
352
|
+
concept: Type.String({ description: "The stable pattern or mental model" }),
|
|
353
|
+
evidenceSummary: Type.Optional(Type.String({ description: "Why this graduated" })),
|
|
354
|
+
synthesisIds: Type.Array(Type.String({ description: "Synthesis IDs that support this" })),
|
|
355
|
+
tags: Type.Optional(Type.Array(Type.String({ description: "Freeform tags for routing" }))),
|
|
356
|
+
relatedEntities: Type.Optional(Type.Array(Type.Object({
|
|
357
|
+
name: Type.String(),
|
|
358
|
+
type: Type.String(),
|
|
359
|
+
role: Type.String(),
|
|
360
|
+
}))),
|
|
361
|
+
}),
|
|
362
|
+
execute: wrapExecute(distill),
|
|
363
|
+
renderResult(result: any, _opts: any, theme: any) {
|
|
364
|
+
if (result.isError) return new Text(theme.fg("error", result.content?.[0]?.text || "Error"), 0, 0);
|
|
365
|
+
const d = result.details || {};
|
|
366
|
+
if (d.error) return new Text(theme.fg("error", d.error), 0, 0);
|
|
367
|
+
return new Text(theme.fg("success", "Distilled: " + d.title + " (" + d.synthesisCount + " syntheses)"), 0, 0);
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// ── Content Seeds ─────────────────────────────────────────────────────
|
|
372
|
+
|
|
155
373
|
pi.registerTool({
|
|
156
374
|
name: "seed_save_content_seeds",
|
|
157
375
|
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.",
|
|
376
|
+
description: "Save content directions from an extraction or synthesis. Each seed needs either an extractionId or synthesisId (or both for cross-source seeds).",
|
|
160
377
|
parameters: Type.Object({
|
|
161
378
|
seeds: Type.Array(Type.Object({
|
|
162
|
-
extractionId: Type.String({ description: "
|
|
379
|
+
extractionId: Type.Optional(Type.String({ description: "Source extraction ID" })),
|
|
380
|
+
synthesisId: Type.Optional(Type.String({ description: "Source synthesis ID" })),
|
|
163
381
|
hook: Type.String({ description: "Opening line / angle" }),
|
|
164
|
-
coreArgument: Type.String({ description: "What you
|
|
382
|
+
coreArgument: Type.String({ description: "What you would actually say" }),
|
|
165
383
|
format: Type.String({ description: "tweet_thread, short_post, essay, newsletter, etc." }),
|
|
166
384
|
sourceGrounding: Type.String({ description: "Which extraction dimensions feed this" }),
|
|
167
385
|
voiceNote: Type.String({ description: "2-sentence friend explanation" }),
|
|
@@ -173,18 +391,16 @@ export function registerExtractionTools(pi: ExtensionAPI) {
|
|
|
173
391
|
if (result.isError) return new Text(theme.fg("error", result.content?.[0]?.text || "Error"), 0, 0);
|
|
174
392
|
const d = result.details || {};
|
|
175
393
|
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
|
-
);
|
|
394
|
+
return new Text(theme.fg("success", "Saved " + (d.count || 0) + " content seeds"), 0, 0);
|
|
180
395
|
},
|
|
181
396
|
});
|
|
182
397
|
|
|
398
|
+
// ── Read tools ────────────────────────────────────────────────────────
|
|
399
|
+
|
|
183
400
|
pi.registerTool({
|
|
184
401
|
name: "seed_list_extractions",
|
|
185
402
|
label: "List Extractions",
|
|
186
|
-
description:
|
|
187
|
-
"List the user's saved extractions. Shows source, summary, and counts of patterns/claims/actionable items.",
|
|
403
|
+
description: "List saved extractions with summary and dimension counts.",
|
|
188
404
|
parameters: Type.Object({
|
|
189
405
|
limit: Type.Optional(Type.Number({ description: "Max results (default 20)" })),
|
|
190
406
|
search: Type.Optional(Type.String({ description: "Search by title or summary" })),
|
|
@@ -194,14 +410,14 @@ export function registerExtractionTools(pi: ExtensionAPI) {
|
|
|
194
410
|
if (result.isError) return new Text(theme.fg("error", result.content?.[0]?.text || "Error"), 0, 0);
|
|
195
411
|
const d = result.details || {};
|
|
196
412
|
if (!d.extractions?.length) return new Text(theme.fg("dim", "No extractions yet"), 0, 0);
|
|
197
|
-
let text = theme.fg("muted",
|
|
413
|
+
let text = theme.fg("muted", d.total + " extractions");
|
|
198
414
|
for (const e of d.extractions.slice(0, expanded ? 50 : 10)) {
|
|
199
415
|
const counts = [
|
|
200
|
-
e.patternCount &&
|
|
201
|
-
e.claimCount &&
|
|
202
|
-
e.actionableCount &&
|
|
416
|
+
e.patternCount && e.patternCount + "p",
|
|
417
|
+
e.claimCount && e.claimCount + "c",
|
|
418
|
+
e.actionableCount && e.actionableCount + "a",
|
|
203
419
|
].filter(Boolean).join("/");
|
|
204
|
-
text +=
|
|
420
|
+
text += "\n " + e.sourceTitle + (counts ? " [" + counts + "]" : "") + " - " + e.status;
|
|
205
421
|
}
|
|
206
422
|
return new Text(text, 0, 0);
|
|
207
423
|
},
|
|
@@ -210,12 +426,92 @@ export function registerExtractionTools(pi: ExtensionAPI) {
|
|
|
210
426
|
pi.registerTool({
|
|
211
427
|
name: "seed_get_extraction",
|
|
212
428
|
label: "Get Extraction",
|
|
213
|
-
description:
|
|
214
|
-
"Get a specific extraction by ID, with full 9-dimension data. Optionally include content seeds.",
|
|
429
|
+
description: "Get a specific extraction by ID with full 9-dimension data. Optionally include content seeds.",
|
|
215
430
|
parameters: Type.Object({
|
|
216
431
|
id: Type.String({ description: "Extraction ID" }),
|
|
217
432
|
seeds: Type.Optional(Type.Boolean({ description: "Include content seeds" })),
|
|
218
433
|
}),
|
|
219
434
|
execute: wrapExecute(getExtraction),
|
|
220
435
|
});
|
|
436
|
+
|
|
437
|
+
pi.registerTool({
|
|
438
|
+
name: "seed_list_sources",
|
|
439
|
+
label: "List Sources",
|
|
440
|
+
description: "List registered sources from the L0 registry.",
|
|
441
|
+
parameters: Type.Object({
|
|
442
|
+
limit: Type.Optional(Type.Number({ description: "Max results (default 20)" })),
|
|
443
|
+
status: Type.Optional(Type.String({ description: "Filter by status: registered, captured, extracted, synthesized, distilled" })),
|
|
444
|
+
}),
|
|
445
|
+
execute: wrapExecute(listSources),
|
|
446
|
+
renderResult(result: any, _opts: any, theme: any) {
|
|
447
|
+
if (result.isError) return new Text(theme.fg("error", result.content?.[0]?.text || "Error"), 0, 0);
|
|
448
|
+
const d = result.details || {};
|
|
449
|
+
if (!d.sources?.length) return new Text(theme.fg("dim", "No sources registered"), 0, 0);
|
|
450
|
+
let text = theme.fg("muted", d.total + " sources");
|
|
451
|
+
for (const s of d.sources) {
|
|
452
|
+
text += "\n " + s.title + " [" + s.status + "] " + (s.provenance || "");
|
|
453
|
+
}
|
|
454
|
+
return new Text(text, 0, 0);
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
pi.registerTool({
|
|
459
|
+
name: "seed_list_syntheses",
|
|
460
|
+
label: "List Syntheses",
|
|
461
|
+
description: "List L3 cross-source syntheses.",
|
|
462
|
+
parameters: Type.Object({
|
|
463
|
+
limit: Type.Optional(Type.Number({ description: "Max results (default 20)" })),
|
|
464
|
+
}),
|
|
465
|
+
execute: wrapExecute(listSyntheses),
|
|
466
|
+
renderResult(result: any, _opts: any, theme: any) {
|
|
467
|
+
if (result.isError) return new Text(theme.fg("error", result.content?.[0]?.text || "Error"), 0, 0);
|
|
468
|
+
const d = result.details || {};
|
|
469
|
+
if (!d.syntheses?.length) return new Text(theme.fg("dim", "No syntheses yet"), 0, 0);
|
|
470
|
+
let text = theme.fg("muted", d.total + " syntheses");
|
|
471
|
+
for (const s of d.syntheses) {
|
|
472
|
+
text += "\n " + s.title + " (" + s.extractionCount + " sources)";
|
|
473
|
+
}
|
|
474
|
+
return new Text(text, 0, 0);
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
pi.registerTool({
|
|
479
|
+
name: "seed_list_seeds",
|
|
480
|
+
label: "List Content Seeds",
|
|
481
|
+
description: "List content seeds. Filter by unused, extraction, or synthesis.",
|
|
482
|
+
parameters: Type.Object({
|
|
483
|
+
limit: Type.Optional(Type.Number({ description: "Max results (default 20)" })),
|
|
484
|
+
unused: Type.Optional(Type.Boolean({ description: "Only unused seeds" })),
|
|
485
|
+
extractionId: Type.Optional(Type.String({ description: "Filter by extraction" })),
|
|
486
|
+
synthesisId: Type.Optional(Type.String({ description: "Filter by synthesis" })),
|
|
487
|
+
}),
|
|
488
|
+
execute: wrapExecute(listSeeds),
|
|
489
|
+
renderResult(result: any, _opts: any, theme: any) {
|
|
490
|
+
if (result.isError) return new Text(theme.fg("error", result.content?.[0]?.text || "Error"), 0, 0);
|
|
491
|
+
const d = result.details || {};
|
|
492
|
+
if (!d.seeds?.length) return new Text(theme.fg("dim", "No content seeds yet"), 0, 0);
|
|
493
|
+
let text = theme.fg("muted", d.total + " seeds");
|
|
494
|
+
for (const s of d.seeds) {
|
|
495
|
+
const status = s.used ? "used" : "fresh";
|
|
496
|
+
text += "\n [" + s.format + "] " + s.hook.slice(0, 60) + "... (" + status + ")";
|
|
497
|
+
}
|
|
498
|
+
return new Text(text, 0, 0);
|
|
499
|
+
},
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
pi.registerTool({
|
|
503
|
+
name: "seed_mark_used",
|
|
504
|
+
label: "Mark Seed Used",
|
|
505
|
+
description: "Mark a content seed as used after drafting content from it.",
|
|
506
|
+
parameters: Type.Object({
|
|
507
|
+
id: Type.String({ description: "Content seed ID" }),
|
|
508
|
+
}),
|
|
509
|
+
execute: wrapExecute(markSeedUsed),
|
|
510
|
+
renderResult(result: any, _opts: any, theme: any) {
|
|
511
|
+
if (result.isError) return new Text(theme.fg("error", result.content?.[0]?.text || "Error"), 0, 0);
|
|
512
|
+
const d = result.details || {};
|
|
513
|
+
if (d.error) return new Text(theme.fg("error", d.error), 0, 0);
|
|
514
|
+
return new Text(theme.fg("success", "Marked as used"), 0, 0);
|
|
515
|
+
},
|
|
516
|
+
});
|
|
221
517
|
}
|