openbuilder 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * builder-report.ts — Generate a full AI meeting report
4
+ *
5
+ * Usage:
6
+ * npx openbuilder report # report on latest transcript
7
+ * npx openbuilder report /path/to/transcript.txt # report on specific file
8
+ *
9
+ * Generates: summary, chapters, action items, key decisions, key questions,
10
+ * and speaker analytics. Output is a markdown report saved to disk.
11
+ */
12
+
13
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
14
+ import { basename } from "node:path";
15
+
16
+ import { getConfig, TRANSCRIPTS_DIR, REPORTS_DIR, ensureDirs } from "../src/utils/config.js";
17
+ import { parseTranscript, formatTranscriptForAI, chunkTranscript } from "../src/utils/transcript-parser.js";
18
+ import { ClaudeProvider } from "../src/ai/claude.js";
19
+ import { OpenAIProvider } from "../src/ai/openai.js";
20
+ import { getMeetingAnalysisPrompt, getMergeAnalysisPrompt } from "../src/ai/prompts.js";
21
+ import { calculateSpeakerStats } from "../src/analytics/speaker-stats.js";
22
+ import { parseAnalysisResponse, generateReport } from "../src/report/generator.js";
23
+ import type { AIProvider } from "../src/ai/provider.js";
24
+
25
+ function findLatestTranscript(): string | null {
26
+ if (!existsSync(TRANSCRIPTS_DIR)) return null;
27
+
28
+ const files = readdirSync(TRANSCRIPTS_DIR)
29
+ .filter((f) => f.endsWith(".txt"))
30
+ .map((f) => ({
31
+ path: `${TRANSCRIPTS_DIR}/${f}`,
32
+ mtime: statSync(`${TRANSCRIPTS_DIR}/${f}`).mtimeMs,
33
+ }))
34
+ .sort((a, b) => b.mtime - a.mtime);
35
+
36
+ return files.length > 0 ? files[0]!.path : null;
37
+ }
38
+
39
+ function getProvider(): AIProvider {
40
+ const config = getConfig();
41
+
42
+ if (config.aiProvider === "openai" && config.openaiApiKey) {
43
+ return new OpenAIProvider();
44
+ }
45
+ if (config.anthropicApiKey) {
46
+ return new ClaudeProvider();
47
+ }
48
+ if (config.openaiApiKey) {
49
+ return new OpenAIProvider();
50
+ }
51
+
52
+ console.error("No AI API key configured.");
53
+ console.error("Set one of these environment variables or use `openbuilder config`:");
54
+ console.error(" ANTHROPIC_API_KEY=your-key");
55
+ console.error(" OPENAI_API_KEY=your-key");
56
+ process.exit(1);
57
+ }
58
+
59
+ async function main() {
60
+ const args = process.argv.slice(2);
61
+ const transcriptPath = args.find((a) => !a.startsWith("--"));
62
+
63
+ let filePath: string;
64
+ if (transcriptPath) {
65
+ if (!existsSync(transcriptPath)) {
66
+ console.error(`File not found: ${transcriptPath}`);
67
+ process.exit(1);
68
+ }
69
+ filePath = transcriptPath;
70
+ } else {
71
+ const latest = findLatestTranscript();
72
+ if (!latest) {
73
+ console.error("No transcript files found.");
74
+ console.error("Provide a transcript path: npx openbuilder report /path/to/transcript.txt");
75
+ process.exit(1);
76
+ }
77
+ filePath = latest;
78
+ }
79
+
80
+ const content = readFileSync(filePath, "utf-8").trim();
81
+ if (!content) {
82
+ console.error("Transcript file is empty.");
83
+ process.exit(1);
84
+ }
85
+
86
+ ensureDirs();
87
+
88
+ console.log(`Generating meeting report for: ${filePath}\n`);
89
+
90
+ const provider = getProvider();
91
+ const transcript = parseTranscript(content);
92
+ const analytics = calculateSpeakerStats(transcript);
93
+
94
+ // Run AI analysis
95
+ const chunks = chunkTranscript(transcript, 30000);
96
+ let analysisResponse: string;
97
+
98
+ if (chunks.length === 1) {
99
+ console.log("Analyzing transcript...");
100
+ const formatted = formatTranscriptForAI(transcript);
101
+ analysisResponse = await provider.complete({
102
+ messages: [
103
+ { role: "system", content: "You are an expert meeting analyst. Return only valid JSON." },
104
+ { role: "user", content: getMeetingAnalysisPrompt(formatted) },
105
+ ],
106
+ maxTokens: 4096,
107
+ temperature: 0.3,
108
+ });
109
+ } else {
110
+ const chunkResults: string[] = [];
111
+ for (let i = 0; i < chunks.length; i++) {
112
+ console.log(`Analyzing chunk ${i + 1}/${chunks.length}...`);
113
+ const result = await provider.complete({
114
+ messages: [
115
+ { role: "system", content: "You are an expert meeting analyst. Return only valid JSON." },
116
+ {
117
+ role: "user",
118
+ content: getMeetingAnalysisPrompt(chunks[i]!, `chunk ${i + 1} of ${chunks.length}`),
119
+ },
120
+ ],
121
+ maxTokens: 4096,
122
+ temperature: 0.3,
123
+ });
124
+ chunkResults.push(result);
125
+ }
126
+
127
+ console.log("Merging analyses...");
128
+ analysisResponse = await provider.complete({
129
+ messages: [
130
+ { role: "system", content: "You are an expert meeting analyst. Return only valid JSON." },
131
+ { role: "user", content: getMergeAnalysisPrompt(chunkResults) },
132
+ ],
133
+ maxTokens: 4096,
134
+ temperature: 0.3,
135
+ });
136
+ }
137
+
138
+ const analysis = parseAnalysisResponse(analysisResponse);
139
+
140
+ // Derive meeting ID from filename
141
+ const meetingId = basename(filePath, ".txt");
142
+
143
+ const report = generateReport({
144
+ meetingId,
145
+ date: new Date().toISOString().split("T")[0],
146
+ transcriptPath: filePath,
147
+ analysis,
148
+ analytics,
149
+ });
150
+
151
+ // Save report
152
+ mkdirSync(REPORTS_DIR, { recursive: true });
153
+ const reportPath = `${REPORTS_DIR}/${meetingId}-report.md`;
154
+ writeFileSync(reportPath, report);
155
+
156
+ console.log("\n" + report);
157
+ console.log(`\nReport saved to: ${reportPath}`);
158
+ }
159
+
160
+ const isMain = process.argv[1]?.endsWith("builder-report.ts");
161
+ if (isMain) {
162
+ main().catch((err) => {
163
+ console.error("Fatal:", err instanceof Error ? err.message : String(err));
164
+ process.exit(1);
165
+ });
166
+ }
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * builder-screenshot.ts — Request an on-demand screenshot from a running bot
4
+ *
5
+ * Usage:
6
+ * npx openbuilder screenshot
7
+ *
8
+ * Sends SIGUSR1 to the running builder-join process, waits for the
9
+ * screenshot to be saved, and prints the path.
10
+ */
11
+
12
+ import { existsSync, readFileSync, unlinkSync } from "node:fs";
13
+ import { PID_FILE, SCREENSHOT_READY_FILE } from "../src/utils/config.js";
14
+
15
+ function sleep(ms: number): Promise<void> {
16
+ return new Promise((resolve) => setTimeout(resolve, ms));
17
+ }
18
+
19
+ async function main() {
20
+ if (!existsSync(PID_FILE)) {
21
+ console.error("No running OpenBuilder bot found (missing PID file).");
22
+ console.error("Start a meeting first: npx openbuilder join <url> --auth|--anon");
23
+ process.exit(1);
24
+ }
25
+
26
+ const pid = parseInt(readFileSync(PID_FILE, "utf-8").trim(), 10);
27
+ if (isNaN(pid)) {
28
+ console.error("Invalid PID file contents.");
29
+ process.exit(1);
30
+ }
31
+
32
+ try {
33
+ process.kill(pid, 0);
34
+ } catch {
35
+ console.error(`OpenBuilder process (PID ${pid}) is not running.`);
36
+ try {
37
+ unlinkSync(PID_FILE);
38
+ } catch {
39
+ /* ignore */
40
+ }
41
+ process.exit(1);
42
+ }
43
+
44
+ try {
45
+ if (existsSync(SCREENSHOT_READY_FILE)) unlinkSync(SCREENSHOT_READY_FILE);
46
+ } catch {
47
+ /* ignore */
48
+ }
49
+
50
+ console.log(`Requesting screenshot from OpenBuilder (PID ${pid})...`);
51
+ process.kill(pid, "SIGUSR1");
52
+
53
+ const timeoutMs = 10_000;
54
+ const pollMs = 500;
55
+ const start = Date.now();
56
+
57
+ while (Date.now() - start < timeoutMs) {
58
+ if (existsSync(SCREENSHOT_READY_FILE)) {
59
+ try {
60
+ const data = JSON.parse(readFileSync(SCREENSHOT_READY_FILE, "utf-8")) as {
61
+ path: string;
62
+ timestamp: number;
63
+ };
64
+ console.log(`[OPENBUILDER_SCREENSHOT] ${data.path}`);
65
+ return;
66
+ } catch {
67
+ // File partially written, retry
68
+ }
69
+ }
70
+ await sleep(pollMs);
71
+ }
72
+
73
+ console.error("Timed out waiting for screenshot (10s).");
74
+ process.exit(1);
75
+ }
76
+
77
+ main().catch((err) => {
78
+ console.error("Fatal:", err instanceof Error ? err.message : String(err));
79
+ process.exit(1);
80
+ });
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * builder-summarize.ts — Run AI summary on a transcript file
4
+ *
5
+ * Usage:
6
+ * npx openbuilder summarize # summarize latest transcript
7
+ * npx openbuilder summarize /path/to/transcript.txt # summarize specific file
8
+ *
9
+ * Works standalone on any transcript file in [HH:MM:SS] Speaker: text format.
10
+ */
11
+
12
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
13
+
14
+ import { getConfig, TRANSCRIPTS_DIR } from "../src/utils/config.js";
15
+ import { parseTranscript, formatTranscriptForAI, chunkTranscript } from "../src/utils/transcript-parser.js";
16
+ import { ClaudeProvider } from "../src/ai/claude.js";
17
+ import { OpenAIProvider } from "../src/ai/openai.js";
18
+ import { getQuickSummaryPrompt } from "../src/ai/prompts.js";
19
+ import type { AIProvider } from "../src/ai/provider.js";
20
+
21
+ function findLatestTranscript(): string | null {
22
+ if (!existsSync(TRANSCRIPTS_DIR)) return null;
23
+
24
+ const files = readdirSync(TRANSCRIPTS_DIR)
25
+ .filter((f) => f.endsWith(".txt"))
26
+ .map((f) => ({
27
+ path: `${TRANSCRIPTS_DIR}/${f}`,
28
+ mtime: statSync(`${TRANSCRIPTS_DIR}/${f}`).mtimeMs,
29
+ }))
30
+ .sort((a, b) => b.mtime - a.mtime);
31
+
32
+ return files.length > 0 ? files[0]!.path : null;
33
+ }
34
+
35
+ function getProvider(): AIProvider {
36
+ const config = getConfig();
37
+
38
+ if (config.aiProvider === "openai" && config.openaiApiKey) {
39
+ return new OpenAIProvider();
40
+ }
41
+ if (config.anthropicApiKey) {
42
+ return new ClaudeProvider();
43
+ }
44
+ if (config.openaiApiKey) {
45
+ return new OpenAIProvider();
46
+ }
47
+
48
+ console.error("No AI API key configured.");
49
+ console.error("Set one of these environment variables or use `openbuilder config`:");
50
+ console.error(" ANTHROPIC_API_KEY=your-key");
51
+ console.error(" OPENAI_API_KEY=your-key");
52
+ process.exit(1);
53
+ }
54
+
55
+ async function main() {
56
+ const args = process.argv.slice(2);
57
+ const transcriptPath = args.find((a) => !a.startsWith("--"));
58
+
59
+ let filePath: string;
60
+ if (transcriptPath) {
61
+ if (!existsSync(transcriptPath)) {
62
+ console.error(`File not found: ${transcriptPath}`);
63
+ process.exit(1);
64
+ }
65
+ filePath = transcriptPath;
66
+ } else {
67
+ const latest = findLatestTranscript();
68
+ if (!latest) {
69
+ console.error("No transcript files found.");
70
+ console.error("Provide a transcript path: npx openbuilder summarize /path/to/transcript.txt");
71
+ process.exit(1);
72
+ }
73
+ filePath = latest;
74
+ }
75
+
76
+ const content = readFileSync(filePath, "utf-8").trim();
77
+ if (!content) {
78
+ console.error("Transcript file is empty.");
79
+ process.exit(1);
80
+ }
81
+
82
+ console.log(`Summarizing: ${filePath}\n`);
83
+
84
+ const provider = getProvider();
85
+ const transcript = parseTranscript(content);
86
+ const chunks = chunkTranscript(transcript, 30000);
87
+
88
+ let summary: string;
89
+
90
+ if (chunks.length === 1) {
91
+ const formatted = formatTranscriptForAI(transcript);
92
+ summary = await provider.complete({
93
+ messages: [
94
+ { role: "system", content: "You are an expert meeting analyst. Write clear, professional summaries." },
95
+ { role: "user", content: getQuickSummaryPrompt(formatted) },
96
+ ],
97
+ maxTokens: 2048,
98
+ temperature: 0.3,
99
+ });
100
+ } else {
101
+ // For long transcripts, summarize each chunk then combine
102
+ const chunkSummaries: string[] = [];
103
+ for (let i = 0; i < chunks.length; i++) {
104
+ console.log(`Processing chunk ${i + 1}/${chunks.length}...`);
105
+ const result = await provider.complete({
106
+ messages: [
107
+ { role: "system", content: "You are an expert meeting analyst. Write clear, professional summaries." },
108
+ { role: "user", content: getQuickSummaryPrompt(chunks[i]!) },
109
+ ],
110
+ maxTokens: 2048,
111
+ temperature: 0.3,
112
+ });
113
+ chunkSummaries.push(result);
114
+ }
115
+
116
+ summary = await provider.complete({
117
+ messages: [
118
+ { role: "system", content: "You are an expert meeting analyst. Combine these summaries into one cohesive summary." },
119
+ {
120
+ role: "user",
121
+ content: `Combine these meeting summaries into a single 3-5 paragraph summary:\n\n${chunkSummaries.join("\n\n---\n\n")}`,
122
+ },
123
+ ],
124
+ maxTokens: 2048,
125
+ temperature: 0.3,
126
+ });
127
+ }
128
+
129
+ console.log("---\n");
130
+ console.log(summary);
131
+ console.log("\n---");
132
+ console.log(`\nTranscript: ${filePath}`);
133
+ console.log(`Lines: ${transcript.lines.length}, Speakers: ${transcript.speakers.join(", ")}`);
134
+ }
135
+
136
+ const isMain = process.argv[1]?.endsWith("builder-summarize.ts");
137
+ if (isMain) {
138
+ main().catch((err) => {
139
+ console.error("Fatal:", err instanceof Error ? err.message : String(err));
140
+ process.exit(1);
141
+ });
142
+ }
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * builder-transcript.ts — Print the latest transcript from a meeting
4
+ *
5
+ * Usage:
6
+ * npx openbuilder transcript
7
+ * npx openbuilder transcript --last 20
8
+ *
9
+ * Finds the most recent transcript file and prints its contents.
10
+ */
11
+
12
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
13
+ import { TRANSCRIPTS_DIR } from "../src/utils/config.js";
14
+
15
+ function main() {
16
+ const args = process.argv.slice(2);
17
+ const lastIdx = args.indexOf("--last");
18
+ const lastN = lastIdx >= 0 ? parseInt(args[lastIdx + 1] ?? "0", 10) : 0;
19
+
20
+ if (!existsSync(TRANSCRIPTS_DIR)) {
21
+ console.error("No transcripts directory found. Is the bot running in a meeting?");
22
+ process.exit(1);
23
+ }
24
+
25
+ const files = readdirSync(TRANSCRIPTS_DIR)
26
+ .filter((f) => f.endsWith(".txt"))
27
+ .map((f) => ({
28
+ name: f,
29
+ path: `${TRANSCRIPTS_DIR}/${f}`,
30
+ mtime: statSync(`${TRANSCRIPTS_DIR}/${f}`).mtimeMs,
31
+ }))
32
+ .sort((a, b) => b.mtime - a.mtime);
33
+
34
+ if (files.length === 0) {
35
+ console.error("No transcript files found. Captions may not have been captured yet.");
36
+ console.error("Make sure the bot is in a meeting and captions are enabled.");
37
+ process.exit(1);
38
+ }
39
+
40
+ const latest = files[0]!;
41
+ const content = readFileSync(latest.path, "utf-8").trim();
42
+
43
+ if (!content) {
44
+ console.log(`[OPENBUILDER_TRANSCRIPT] ${latest.path}`);
45
+ console.log("\n(No captions captured yet — is someone speaking?)");
46
+ return;
47
+ }
48
+
49
+ const lines = content.split("\n");
50
+
51
+ console.log(`[OPENBUILDER_TRANSCRIPT] ${latest.path}`);
52
+ console.log(`Transcript: ${latest.name} (${lines.length} lines)\n`);
53
+
54
+ if (lastN > 0 && lines.length > lastN) {
55
+ console.log(`(showing last ${lastN} of ${lines.length} lines)\n`);
56
+ console.log(lines.slice(-lastN).join("\n"));
57
+ } else {
58
+ console.log(content);
59
+ }
60
+ }
61
+
62
+ main();
@@ -0,0 +1,59 @@
1
+ /**
2
+ * claude.ts — Claude/Anthropic AI provider implementation
3
+ *
4
+ * Uses the @anthropic-ai/sdk package (optional peer dependency).
5
+ * Dynamically imported to avoid crashing if not installed.
6
+ */
7
+
8
+ import { type AICompletionOptions, type AIProvider, AIProviderError } from "./provider.js";
9
+ import { getConfig } from "../utils/config.js";
10
+
11
+ export class ClaudeProvider implements AIProvider {
12
+ readonly name = "Claude";
13
+
14
+ async complete(options: AICompletionOptions): Promise<string> {
15
+ const config = getConfig();
16
+ const apiKey = config.anthropicApiKey;
17
+
18
+ if (!apiKey) {
19
+ throw new AIProviderError(
20
+ "claude",
21
+ "Anthropic API key not configured. Set ANTHROPIC_API_KEY environment variable or run: openbuilder config set anthropicApiKey <key>",
22
+ );
23
+ }
24
+
25
+ let Anthropic: typeof import("@anthropic-ai/sdk").default;
26
+ try {
27
+ const mod = await import("@anthropic-ai/sdk");
28
+ Anthropic = mod.default;
29
+ } catch {
30
+ throw new AIProviderError(
31
+ "claude",
32
+ "The @anthropic-ai/sdk package is not installed. Run: npm install @anthropic-ai/sdk",
33
+ );
34
+ }
35
+
36
+ const client = new Anthropic({ apiKey });
37
+
38
+ // Separate system message from user/assistant messages
39
+ const systemMessages = options.messages.filter((m) => m.role === "system");
40
+ const chatMessages = options.messages.filter((m) => m.role !== "system");
41
+
42
+ const response = await client.messages.create({
43
+ model: "claude-sonnet-4-5-20250929",
44
+ max_tokens: options.maxTokens ?? 4096,
45
+ temperature: options.temperature ?? 0.3,
46
+ system: systemMessages.map((m) => m.content).join("\n\n") || undefined,
47
+ messages: chatMessages.map((m) => ({
48
+ role: m.role as "user" | "assistant",
49
+ content: m.content,
50
+ })),
51
+ });
52
+
53
+ const textBlock = response.content.find((b) => b.type === "text");
54
+ if (!textBlock || textBlock.type !== "text") {
55
+ throw new AIProviderError("claude", "No text content in Claude response");
56
+ }
57
+ return textBlock.text;
58
+ }
59
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * openai.ts — OpenAI AI provider implementation
3
+ *
4
+ * Uses the openai package (optional peer dependency).
5
+ * Dynamically imported to avoid crashing if not installed.
6
+ */
7
+
8
+ import { type AICompletionOptions, type AIProvider, AIProviderError } from "./provider.js";
9
+ import { getConfig } from "../utils/config.js";
10
+
11
+ export class OpenAIProvider implements AIProvider {
12
+ readonly name = "OpenAI";
13
+
14
+ async complete(options: AICompletionOptions): Promise<string> {
15
+ const config = getConfig();
16
+ const apiKey = config.openaiApiKey;
17
+
18
+ if (!apiKey) {
19
+ throw new AIProviderError(
20
+ "openai",
21
+ "OpenAI API key not configured. Set OPENAI_API_KEY environment variable or run: openbuilder config set openaiApiKey <key>",
22
+ );
23
+ }
24
+
25
+ let OpenAI: typeof import("openai").default;
26
+ try {
27
+ const mod = await import("openai");
28
+ OpenAI = mod.default;
29
+ } catch {
30
+ throw new AIProviderError(
31
+ "openai",
32
+ "The openai package is not installed. Run: npm install openai",
33
+ );
34
+ }
35
+
36
+ const client = new OpenAI({ apiKey });
37
+
38
+ const response = await client.chat.completions.create({
39
+ model: "gpt-4o",
40
+ max_tokens: options.maxTokens ?? 4096,
41
+ temperature: options.temperature ?? 0.3,
42
+ messages: options.messages.map((m) => ({
43
+ role: m.role,
44
+ content: m.content,
45
+ })),
46
+ });
47
+
48
+ const content = response.choices[0]?.message?.content;
49
+ if (!content) {
50
+ throw new AIProviderError("openai", "No content in OpenAI response");
51
+ }
52
+ return content;
53
+ }
54
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * prompts.ts — Meeting analysis prompts for AI processing
3
+ *
4
+ * Designed to work with transcripts in [HH:MM:SS] Speaker: text format.
5
+ * Handles long meetings via chunking when needed.
6
+ */
7
+
8
+ export const SYSTEM_PROMPT = `You are an expert meeting analyst. You analyze meeting transcripts and produce structured, actionable meeting intelligence. Be concise but thorough. Focus on what matters most.`;
9
+
10
+ /**
11
+ * Prompt for generating a full meeting analysis from a transcript.
12
+ * Returns structured JSON for programmatic parsing.
13
+ */
14
+ export function getMeetingAnalysisPrompt(transcript: string, chunkInfo?: string): string {
15
+ const chunkNote = chunkInfo
16
+ ? `\n\nNOTE: This is ${chunkInfo} of a longer meeting. Analyze this portion thoroughly.`
17
+ : "";
18
+
19
+ return `Analyze this meeting transcript and return a JSON object with the following structure. Be thorough but concise.${chunkNote}
20
+
21
+ Return ONLY valid JSON — no markdown fences, no explanation before or after.
22
+
23
+ {
24
+ "summary": "2-3 paragraph summary covering the main topics discussed, key outcomes, and overall meeting flow",
25
+ "chapters": [
26
+ { "timestamp": "HH:MM", "title": "Topic name", "description": "Brief description of what was discussed" }
27
+ ],
28
+ "actionItems": [
29
+ { "description": "What needs to be done", "assignee": "Person name or null if unspecified" }
30
+ ],
31
+ "keyDecisions": [
32
+ "Decision that was made"
33
+ ],
34
+ "keyQuestions": [
35
+ { "question": "Question that was asked", "status": "answered" or "unanswered" }
36
+ ]
37
+ }
38
+
39
+ Guidelines:
40
+ - For chapters: Group discussion into logical topic segments with approximate timestamps
41
+ - For action items: Look for commitments, tasks, follow-ups. Detect assignees from context (e.g. "Alice will..." → assignee: "Alice")
42
+ - For key decisions: Only include explicit decisions, not suggestions or ideas
43
+ - For key questions: Include important questions raised. Mark as "answered" if the transcript shows a response
44
+ - Use the speaker names exactly as they appear in the transcript
45
+ - Timestamps should use HH:MM format from the transcript
46
+
47
+ TRANSCRIPT:
48
+ ${transcript}`;
49
+ }
50
+
51
+ /**
52
+ * Prompt for merging multiple chunk analyses into a single cohesive report.
53
+ * Used when a transcript is too long to process in one pass.
54
+ */
55
+ export function getMergeAnalysisPrompt(chunkResults: string[]): string {
56
+ const numbered = chunkResults
57
+ .map((r, i) => `--- CHUNK ${i + 1} ANALYSIS ---\n${r}`)
58
+ .join("\n\n");
59
+
60
+ return `You were given a long meeting transcript in chunks. Below are the analyses of each chunk. Merge them into a single cohesive meeting analysis.
61
+
62
+ Return ONLY valid JSON with the same structure — no markdown fences:
63
+
64
+ {
65
+ "summary": "Unified 2-3 paragraph summary of the entire meeting",
66
+ "chapters": [{ "timestamp": "HH:MM", "title": "...", "description": "..." }],
67
+ "actionItems": [{ "description": "...", "assignee": "..." }],
68
+ "keyDecisions": ["..."],
69
+ "keyQuestions": [{ "question": "...", "status": "answered|unanswered" }]
70
+ }
71
+
72
+ Guidelines:
73
+ - Combine and deduplicate items across chunks
74
+ - Create a unified summary that flows naturally
75
+ - Merge chapter lists chronologically
76
+ - Deduplicate action items, decisions, and questions
77
+
78
+ CHUNK ANALYSES:
79
+ ${numbered}`;
80
+ }
81
+
82
+ /**
83
+ * Prompt for a quick summary (used by the summarize command).
84
+ */
85
+ export function getQuickSummaryPrompt(transcript: string): string {
86
+ return `Summarize this meeting transcript in 3-5 paragraphs. Focus on:
87
+ 1. What was discussed (main topics)
88
+ 2. Key outcomes or decisions
89
+ 3. Action items or next steps mentioned
90
+
91
+ Write in clear, professional prose. Use the speaker names from the transcript.
92
+
93
+ TRANSCRIPT:
94
+ ${transcript}`;
95
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * provider.ts — AI provider interface for OpenBuilder
3
+ *
4
+ * Clean abstraction layer so adding new AI providers is trivial.
5
+ * Providers are dynamically imported — the app won't crash if SDKs aren't installed.
6
+ */
7
+
8
+ export interface AIMessage {
9
+ role: "system" | "user" | "assistant";
10
+ content: string;
11
+ }
12
+
13
+ export interface AICompletionOptions {
14
+ messages: AIMessage[];
15
+ maxTokens?: number;
16
+ temperature?: number;
17
+ }
18
+
19
+ export interface AIProvider {
20
+ /** Human-readable name of this provider (e.g. "Claude", "OpenAI") */
21
+ readonly name: string;
22
+
23
+ /**
24
+ * Send a chat completion request and return the response text.
25
+ * Throws if the API key is missing or the request fails.
26
+ */
27
+ complete(options: AICompletionOptions): Promise<string>;
28
+ }
29
+
30
+ /** Error thrown when an AI provider is not available (SDK not installed or no API key). */
31
+ export class AIProviderError extends Error {
32
+ constructor(
33
+ public readonly provider: string,
34
+ message: string,
35
+ ) {
36
+ super(message);
37
+ this.name = "AIProviderError";
38
+ }
39
+ }