Jessedbrown commited on
Commit
63458bf
·
0 Parent(s):

init AI doc extractor

Browse files
.gitignore ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Deno
2
+ .env
3
+ deno.lock
4
+
5
+ # Node modules
6
+ node_modules/
7
+
8
+ # Generated docs
9
+ generated-docs/*.md
10
+
11
+ # Frontend build
12
+ frontend/dist/
13
+ frontend/node_modules/
14
+
15
+ # OS
16
+ .DS_Store
17
+ Thumbs.db
18
+
README.md ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Docs Extractor - AI-Powered Documentation Generator
2
+
3
+ An intelligent documentation extraction tool powered by Zypher Agent and Firecrawl. This tool automatically crawls documentation websites and generates comprehensive guides based on your needs.
4
+
5
+
6
+ ## Setup
7
+
8
+ ### Prerequisites
9
+
10
+ 1. [Deno](https://deno.com) installed
11
+ 2. [Node.js and npm](https://nodejs.org) installed (for frontend)
12
+ 3. API Keys:
13
+ - Anthropic API key (for Claude AI)
14
+ - Firecrawl API key (for web crawling)
15
+
16
+ ### Installation
17
+
18
+ 1. **Set up environment variables:**
19
+
20
+ Create a `.env` file in the `docs-extractor` directory:
21
+
22
+ ```bash
23
+ ANTHROPIC_API_KEY=your_anthropic_api_key_here
24
+ FIRECRAWL_API_KEY=your_firecrawl_api_key_here
25
+ ```
26
+
27
+ Get your API keys:
28
+ - Anthropic: https://console.anthropic.com/
29
+ - Firecrawl: https://firecrawl.dev/
30
+
31
+ 2. **Install frontend dependencies:**
32
+
33
+ ```bash
34
+ cd frontend
35
+ npm install
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ### Option 1: With UI (Recommended)
41
+
42
+ 1. **Start the backend server:**
43
+
44
+ ```bash
45
+ deno task server
46
+ ```
47
+
48
+ The API server will start on `http://localhost:8000`
49
+
50
+ 2. **Start the frontend (in a new terminal):**
51
+
52
+ ```bash
53
+ cd frontend
54
+ npm run dev
55
+ ```
56
+
57
+ The UI will be available at `http://localhost:5173`
58
+
59
+ 3. **Use the web interface:**
60
+ - Enter a documentation URL (e.g., `https://react.dev/docs`)
61
+ - Specify what you want to extract (e.g., "Quickstart guide with installation and examples")
62
+ - Click "Extract Documentation"
63
+ - Wait for the AI to process and generate the documentation
64
+ - View and download the generated markdown file
65
+
66
+
deno.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "tasks": {
3
+ "dev": "deno run -A main.ts",
4
+ "server": "deno run -A server.ts"
5
+ },
6
+ "imports": {
7
+ "@std/dotenv": "jsr:@std/dotenv@^0.225.0",
8
+ "@corespeed/zypher": "jsr:@corespeed/zypher@^0.4.0",
9
+ "@oak/oak": "jsr:@oak/oak@^17.1.3",
10
+ "rxjs-for-await": "npm:rxjs-for-await@^1.0.0"
11
+ }
12
+ }
frontend/index.html ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Docs Extractor - AI-Powered Documentation Generator</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
14
+
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "docs-extractor-frontend",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc && vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "react": "^18.3.1",
13
+ "react-dom": "^18.3.1",
14
+ "react-markdown": "^9.0.1",
15
+ "remark-gfm": "^4.0.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/react": "^18.3.12",
19
+ "@types/react-dom": "^18.3.1",
20
+ "@vitejs/plugin-react": "^4.3.4",
21
+ "typescript": "^5.7.2",
22
+ "vite": "^6.0.1"
23
+ }
24
+ }
25
+
frontend/public/vite.svg ADDED
frontend/src/App.css ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .app {
2
+ min-height: 100vh;
3
+ display: flex;
4
+ flex-direction: column;
5
+ }
6
+
7
+ .form-section {
8
+ display: flex;
9
+ flex-direction: column;
10
+ }
11
+
12
+ .output-section {
13
+ flex: 1;
14
+ }
15
+
frontend/src/App.tsx ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react';
2
+ import ReactMarkdown from 'react-markdown';
3
+ import remarkGfm from 'remark-gfm';
4
+ import './App.css';
5
+
6
+ interface Job {
7
+ status: 'processing' | 'completed' | 'failed';
8
+ result?: {
9
+ response: {
10
+ content: Array<{ type: string; text: string }>;
11
+ };
12
+ };
13
+ error?: string;
14
+ }
15
+
16
+ interface Doc {
17
+ name: string;
18
+ path: string;
19
+ }
20
+
21
+ function App() {
22
+ const [docsUrl, setDocsUrl] = useState('');
23
+ const [extractionType, setExtractionType] = useState('');
24
+ const [loading, setLoading] = useState(false);
25
+ const [jobId, setJobId] = useState<string | null>(null);
26
+ const [result, setResult] = useState<string | null>(null);
27
+ const [error, setError] = useState<string | null>(null);
28
+ const [generatedDocs, setGeneratedDocs] = useState<Doc[]>([]);
29
+ const [selectedDoc, setSelectedDoc] = useState<string | null>(null);
30
+ const [docContent, setDocContent] = useState<string | null>(null);
31
+
32
+ const API_BASE = 'http://localhost:8000';
33
+
34
+ const loadGeneratedDocs = async () => {
35
+ try {
36
+ const response = await fetch(`${API_BASE}/api/docs`);
37
+ const data = await response.json();
38
+ setGeneratedDocs(data.docs || []);
39
+ } catch (err) {
40
+ console.error('Error loading docs:', err);
41
+ }
42
+ };
43
+
44
+ const loadDocContent = async (filename: string) => {
45
+ try {
46
+ const response = await fetch(`${API_BASE}/api/docs/${filename}`);
47
+ const content = await response.text();
48
+ setDocContent(content);
49
+ setSelectedDoc(filename);
50
+ } catch (err) {
51
+ console.error('Error loading doc content:', err);
52
+ }
53
+ };
54
+
55
+ useEffect(() => {
56
+ loadGeneratedDocs();
57
+ }, []);
58
+
59
+ useEffect(() => {
60
+ if (!jobId) return;
61
+
62
+ const pollInterval = setInterval(async () => {
63
+ try {
64
+ const response = await fetch(`${API_BASE}/api/job/${jobId}`);
65
+ const job: Job = await response.json();
66
+
67
+ if (job.status === 'completed') {
68
+ clearInterval(pollInterval);
69
+ setLoading(false);
70
+
71
+ if (job.result?.response?.content) {
72
+ const textContent = job.result.response.content
73
+ .filter(item => item.type === 'text')
74
+ .map(item => item.text)
75
+ .join('\n\n');
76
+ setResult(textContent);
77
+ }
78
+
79
+ setJobId(null);
80
+ loadGeneratedDocs();
81
+ } else if (job.status === 'failed') {
82
+ clearInterval(pollInterval);
83
+ setLoading(false);
84
+ setError(job.error || 'Extraction failed');
85
+ setJobId(null);
86
+ }
87
+ } catch (err) {
88
+ console.error('Error polling job:', err);
89
+ }
90
+ }, 2000);
91
+
92
+ return () => clearInterval(pollInterval);
93
+ }, [jobId]);
94
+
95
+ const handleSubmit = async (e: React.FormEvent) => {
96
+ e.preventDefault();
97
+ setLoading(true);
98
+ setError(null);
99
+ setResult(null);
100
+
101
+ try {
102
+ const response = await fetch(`${API_BASE}/api/extract`, {
103
+ method: 'POST',
104
+ headers: {
105
+ 'Content-Type': 'application/json',
106
+ },
107
+ body: JSON.stringify({ docsUrl, extractionType }),
108
+ });
109
+
110
+ if (!response.ok) {
111
+ throw new Error('Failed to start extraction');
112
+ }
113
+
114
+ const data = await response.json();
115
+ setJobId(data.jobId);
116
+ } catch (err) {
117
+ setLoading(false);
118
+ setError(err instanceof Error ? err.message : 'An error occurred');
119
+ }
120
+ };
121
+
122
+ const exampleTypes = [
123
+ 'Quick installation guide',
124
+ 'Main features overview',
125
+ 'Getting started tutorial',
126
+ 'API reference summary',
127
+ 'Configuration guide',
128
+ ];
129
+
130
+ return (
131
+ <div className="app">
132
+ <header className="header">
133
+ <div className="container">
134
+ <h1>📚 Docs Extractor</h1>
135
+ <p className="subtitle">AI-powered documentation generator using Zypher Agent</p>
136
+ </div>
137
+ </header>
138
+
139
+ <main className="container main-content">
140
+ <div className="grid">
141
+ <div className="form-section">
142
+ <div className="card">
143
+ <h2>Extract Documentation</h2>
144
+ <form onSubmit={handleSubmit}>
145
+ <div className="form-group">
146
+ <label htmlFor="docsUrl">
147
+ Documentation URL
148
+ <span className="required">*</span>
149
+ </label>
150
+ <input
151
+ id="docsUrl"
152
+ type="url"
153
+ placeholder="https://example.com/docs"
154
+ value={docsUrl}
155
+ onChange={(e) => setDocsUrl(e.target.value)}
156
+ required
157
+ disabled={loading}
158
+ />
159
+ <small>Enter the URL of the documentation you want to extract</small>
160
+ </div>
161
+
162
+ <div className="form-group">
163
+ <label htmlFor="extractionType">
164
+ What do you want?
165
+ <span className="required">*</span>
166
+ </label>
167
+ <textarea
168
+ id="extractionType"
169
+ placeholder="e.g., Quickstart guide with installation steps and examples"
170
+ value={extractionType}
171
+ onChange={(e) => setExtractionType(e.target.value)}
172
+ required
173
+ disabled={loading}
174
+ rows={3}
175
+ />
176
+ <small>Describe what you want to extract from the documentation</small>
177
+ </div>
178
+
179
+ <div className="example-types">
180
+ <strong>Example requests:</strong>
181
+ {exampleTypes.map((type, i) => (
182
+ <button
183
+ key={i}
184
+ type="button"
185
+ className="example-btn"
186
+ onClick={() => setExtractionType(type)}
187
+ disabled={loading}
188
+ >
189
+ {type}
190
+ </button>
191
+ ))}
192
+ </div>
193
+
194
+ <button type="submit" className="btn btn-primary" disabled={loading}>
195
+ {loading ? (
196
+ <>
197
+ <span className="spinner"></span>
198
+ Processing...
199
+ </>
200
+ ) : (
201
+ 'Extract Documentation'
202
+ )}
203
+ </button>
204
+ </form>
205
+
206
+ {error && (
207
+ <div className="alert alert-error">
208
+ <strong>Error:</strong> {error}
209
+ </div>
210
+ )}
211
+ </div>
212
+
213
+ <div className="card">
214
+ <h3>Generated Documents</h3>
215
+ {generatedDocs.length === 0 ? (
216
+ <p className="empty-state">No documents generated yet</p>
217
+ ) : (
218
+ <ul className="docs-list">
219
+ {generatedDocs.map((doc) => (
220
+ <li key={doc.name}>
221
+ <button
222
+ className={`doc-btn ${selectedDoc === doc.name ? 'active' : ''}`}
223
+ onClick={() => loadDocContent(doc.name)}
224
+ >
225
+ 📄 {doc.name}
226
+ </button>
227
+ </li>
228
+ ))}
229
+ </ul>
230
+ )}
231
+ </div>
232
+ </div>
233
+
234
+ <div className="output-section">
235
+ <div className="card output-card">
236
+ <h2>Output</h2>
237
+ {loading && (
238
+ <div className="loading-state">
239
+ <div className="spinner large"></div>
240
+ <p>AI is extracting and generating documentation...</p>
241
+ <small>This may take a few minutes depending on the size of the documentation</small>
242
+ </div>
243
+ )}
244
+
245
+ {result && (
246
+ <div className="result">
247
+ <div className="result-header">
248
+ <span className="badge badge-success">✓ Generated</span>
249
+ </div>
250
+ <div className="markdown-content">
251
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>
252
+ {result}
253
+ </ReactMarkdown>
254
+ </div>
255
+ </div>
256
+ )}
257
+
258
+ {docContent && !result && (
259
+ <div className="result">
260
+ <div className="result-header">
261
+ <span className="badge badge-info">📄 {selectedDoc}</span>
262
+ </div>
263
+ <div className="markdown-content">
264
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>
265
+ {docContent}
266
+ </ReactMarkdown>
267
+ </div>
268
+ </div>
269
+ )}
270
+
271
+ {!loading && !result && !docContent && (
272
+ <div className="empty-state">
273
+ <p>👈 Enter a documentation URL and select what you want to extract</p>
274
+ <p className="hint">The AI will analyze the documentation and generate a comprehensive guide for you</p>
275
+ </div>
276
+ )}
277
+ </div>
278
+ </div>
279
+ </div>
280
+ </main>
281
+
282
+ <footer className="footer">
283
+ <div className="container">
284
+ <p>
285
+ Powered by <a href="https://zypher.corespeed.io" target="_blank" rel="noopener noreferrer">Zypher Agent</a>
286
+ {' '}&amp;{' '}
287
+ <a href="https://firecrawl.dev" target="_blank" rel="noopener noreferrer">Firecrawl</a>
288
+ </p>
289
+ </div>
290
+ </footer>
291
+ </div>
292
+ );
293
+ }
294
+
295
+ export default App;
296
+
frontend/src/index.css ADDED
@@ -0,0 +1,464 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --primary: #6366f1;
3
+ --primary-dark: #4f46e5;
4
+ --success: #10b981;
5
+ --error: #ef4444;
6
+ --warning: #f59e0b;
7
+ --bg: #0f172a;
8
+ --bg-card: #1e293b;
9
+ --bg-hover: #334155;
10
+ --text: #f1f5f9;
11
+ --text-muted: #94a3b8;
12
+ --border: #334155;
13
+ --radius: 12px;
14
+ --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
15
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4);
16
+ }
17
+
18
+ * {
19
+ margin: 0;
20
+ padding: 0;
21
+ box-sizing: border-box;
22
+ }
23
+
24
+ body {
25
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
26
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
27
+ sans-serif;
28
+ -webkit-font-smoothing: antialiased;
29
+ -moz-osx-font-smoothing: grayscale;
30
+ background: var(--bg);
31
+ color: var(--text);
32
+ line-height: 1.6;
33
+ }
34
+
35
+ code {
36
+ font-family: 'Courier New', Courier, monospace;
37
+ }
38
+
39
+ .container {
40
+ max-width: 1400px;
41
+ margin: 0 auto;
42
+ padding: 0 2rem;
43
+ }
44
+
45
+ .header {
46
+ background: var(--bg-card);
47
+ border-bottom: 1px solid var(--border);
48
+ padding: 2rem 0;
49
+ margin-bottom: 2rem;
50
+ }
51
+
52
+ .header h1 {
53
+ font-size: 2.5rem;
54
+ font-weight: 700;
55
+ margin-bottom: 0.5rem;
56
+ background: linear-gradient(135deg, var(--primary) 0%, #8b5cf6 100%);
57
+ -webkit-background-clip: text;
58
+ -webkit-text-fill-color: transparent;
59
+ background-clip: text;
60
+ }
61
+
62
+ .subtitle {
63
+ color: var(--text-muted);
64
+ font-size: 1.1rem;
65
+ }
66
+
67
+ .main-content {
68
+ padding-bottom: 4rem;
69
+ }
70
+
71
+ .grid {
72
+ display: grid;
73
+ grid-template-columns: 450px 1fr;
74
+ gap: 2rem;
75
+ align-items: start;
76
+ }
77
+
78
+ @media (max-width: 1024px) {
79
+ .grid {
80
+ grid-template-columns: 1fr;
81
+ }
82
+ }
83
+
84
+ .card {
85
+ background: var(--bg-card);
86
+ border: 1px solid var(--border);
87
+ border-radius: var(--radius);
88
+ padding: 1.5rem;
89
+ box-shadow: var(--shadow);
90
+ margin-bottom: 1.5rem;
91
+ }
92
+
93
+ .card h2 {
94
+ font-size: 1.5rem;
95
+ margin-bottom: 1.5rem;
96
+ color: var(--text);
97
+ }
98
+
99
+ .card h3 {
100
+ font-size: 1.2rem;
101
+ margin-bottom: 1rem;
102
+ color: var(--text);
103
+ }
104
+
105
+ .form-group {
106
+ margin-bottom: 1.5rem;
107
+ }
108
+
109
+ .form-group label {
110
+ display: block;
111
+ margin-bottom: 0.5rem;
112
+ font-weight: 500;
113
+ color: var(--text);
114
+ }
115
+
116
+ .required {
117
+ color: var(--error);
118
+ margin-left: 0.25rem;
119
+ }
120
+
121
+ .form-group input,
122
+ .form-group textarea {
123
+ width: 100%;
124
+ padding: 0.75rem;
125
+ background: var(--bg);
126
+ border: 1px solid var(--border);
127
+ border-radius: 8px;
128
+ color: var(--text);
129
+ font-size: 1rem;
130
+ transition: all 0.2s;
131
+ }
132
+
133
+ .form-group input:focus,
134
+ .form-group textarea:focus {
135
+ outline: none;
136
+ border-color: var(--primary);
137
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
138
+ }
139
+
140
+ .form-group input:disabled,
141
+ .form-group textarea:disabled {
142
+ opacity: 0.6;
143
+ cursor: not-allowed;
144
+ }
145
+
146
+ .form-group small {
147
+ display: block;
148
+ margin-top: 0.5rem;
149
+ color: var(--text-muted);
150
+ font-size: 0.875rem;
151
+ }
152
+
153
+ .example-types {
154
+ margin-bottom: 1.5rem;
155
+ padding: 1rem;
156
+ background: var(--bg);
157
+ border-radius: 8px;
158
+ }
159
+
160
+ .example-types strong {
161
+ display: block;
162
+ margin-bottom: 0.75rem;
163
+ color: var(--text-muted);
164
+ font-size: 0.875rem;
165
+ }
166
+
167
+ .example-btn {
168
+ display: block;
169
+ width: 100%;
170
+ text-align: left;
171
+ padding: 0.5rem 0.75rem;
172
+ margin-bottom: 0.5rem;
173
+ background: transparent;
174
+ border: 1px solid var(--border);
175
+ border-radius: 6px;
176
+ color: var(--text);
177
+ font-size: 0.875rem;
178
+ cursor: pointer;
179
+ transition: all 0.2s;
180
+ }
181
+
182
+ .example-btn:hover:not(:disabled) {
183
+ background: var(--bg-hover);
184
+ border-color: var(--primary);
185
+ }
186
+
187
+ .example-btn:disabled {
188
+ opacity: 0.5;
189
+ cursor: not-allowed;
190
+ }
191
+
192
+ .btn {
193
+ padding: 0.875rem 1.5rem;
194
+ border: none;
195
+ border-radius: 8px;
196
+ font-size: 1rem;
197
+ font-weight: 600;
198
+ cursor: pointer;
199
+ transition: all 0.2s;
200
+ display: inline-flex;
201
+ align-items: center;
202
+ justify-content: center;
203
+ gap: 0.5rem;
204
+ }
205
+
206
+ .btn-primary {
207
+ background: var(--primary);
208
+ color: white;
209
+ width: 100%;
210
+ }
211
+
212
+ .btn-primary:hover:not(:disabled) {
213
+ background: var(--primary-dark);
214
+ transform: translateY(-1px);
215
+ box-shadow: var(--shadow-lg);
216
+ }
217
+
218
+ .btn-primary:disabled {
219
+ opacity: 0.6;
220
+ cursor: not-allowed;
221
+ transform: none;
222
+ }
223
+
224
+ .spinner {
225
+ display: inline-block;
226
+ width: 16px;
227
+ height: 16px;
228
+ border: 2px solid rgba(255, 255, 255, 0.3);
229
+ border-top-color: white;
230
+ border-radius: 50%;
231
+ animation: spin 0.8s linear infinite;
232
+ }
233
+
234
+ .spinner.large {
235
+ width: 48px;
236
+ height: 48px;
237
+ border-width: 4px;
238
+ }
239
+
240
+ @keyframes spin {
241
+ to { transform: rotate(360deg); }
242
+ }
243
+
244
+ .alert {
245
+ padding: 1rem;
246
+ border-radius: 8px;
247
+ margin-top: 1rem;
248
+ }
249
+
250
+ .alert-error {
251
+ background: rgba(239, 68, 68, 0.1);
252
+ border: 1px solid var(--error);
253
+ color: #fca5a5;
254
+ }
255
+
256
+ .docs-list {
257
+ list-style: none;
258
+ }
259
+
260
+ .docs-list li {
261
+ margin-bottom: 0.5rem;
262
+ }
263
+
264
+ .doc-btn {
265
+ width: 100%;
266
+ text-align: left;
267
+ padding: 0.75rem 1rem;
268
+ background: var(--bg);
269
+ border: 1px solid var(--border);
270
+ border-radius: 8px;
271
+ color: var(--text);
272
+ cursor: pointer;
273
+ transition: all 0.2s;
274
+ }
275
+
276
+ .doc-btn:hover {
277
+ background: var(--bg-hover);
278
+ border-color: var(--primary);
279
+ }
280
+
281
+ .doc-btn.active {
282
+ background: var(--primary);
283
+ border-color: var(--primary);
284
+ color: white;
285
+ }
286
+
287
+ .output-card {
288
+ min-height: 600px;
289
+ position: sticky;
290
+ top: 2rem;
291
+ overflow: hidden;
292
+ }
293
+
294
+ .output-card .markdown-content {
295
+ max-width: 100%;
296
+ overflow-x: hidden;
297
+ }
298
+
299
+ .loading-state {
300
+ text-align: center;
301
+ padding: 4rem 2rem;
302
+ }
303
+
304
+ .loading-state p {
305
+ margin-top: 1rem;
306
+ font-size: 1.1rem;
307
+ }
308
+
309
+ .loading-state small {
310
+ display: block;
311
+ margin-top: 0.5rem;
312
+ color: var(--text-muted);
313
+ }
314
+
315
+ .result-header {
316
+ margin-bottom: 1.5rem;
317
+ padding-bottom: 1rem;
318
+ border-bottom: 1px solid var(--border);
319
+ }
320
+
321
+ .badge {
322
+ display: inline-flex;
323
+ align-items: center;
324
+ padding: 0.375rem 0.75rem;
325
+ border-radius: 6px;
326
+ font-size: 0.875rem;
327
+ font-weight: 600;
328
+ }
329
+
330
+ .badge-success {
331
+ background: rgba(16, 185, 129, 0.1);
332
+ color: var(--success);
333
+ }
334
+
335
+ .badge-info {
336
+ background: rgba(99, 102, 241, 0.1);
337
+ color: var(--primary);
338
+ }
339
+
340
+ .empty-state {
341
+ text-align: center;
342
+ padding: 4rem 2rem;
343
+ color: var(--text-muted);
344
+ }
345
+
346
+ .empty-state p {
347
+ margin-bottom: 0.5rem;
348
+ }
349
+
350
+ .hint {
351
+ font-size: 0.875rem;
352
+ }
353
+
354
+ .markdown-content {
355
+ color: var(--text);
356
+ line-height: 1.8;
357
+ word-wrap: break-word;
358
+ overflow-wrap: break-word;
359
+ max-width: 100%;
360
+ }
361
+
362
+ .markdown-content h1,
363
+ .markdown-content h2,
364
+ .markdown-content h3,
365
+ .markdown-content h4 {
366
+ margin-top: 2rem;
367
+ margin-bottom: 1rem;
368
+ color: var(--text);
369
+ word-wrap: break-word;
370
+ }
371
+
372
+ .markdown-content h1 { font-size: 2rem; }
373
+ .markdown-content h2 { font-size: 1.5rem; }
374
+ .markdown-content h3 { font-size: 1.25rem; }
375
+
376
+ .markdown-content p {
377
+ margin-bottom: 1rem;
378
+ word-wrap: break-word;
379
+ overflow-wrap: break-word;
380
+ }
381
+
382
+ .markdown-content a {
383
+ color: var(--primary);
384
+ text-decoration: none;
385
+ word-wrap: break-word;
386
+ overflow-wrap: break-word;
387
+ }
388
+
389
+ .markdown-content a:hover {
390
+ text-decoration: underline;
391
+ }
392
+
393
+ .markdown-content code {
394
+ background: var(--bg);
395
+ padding: 0.2rem 0.4rem;
396
+ border-radius: 4px;
397
+ font-size: 0.875em;
398
+ word-wrap: break-word;
399
+ white-space: pre-wrap;
400
+ }
401
+
402
+ .markdown-content pre {
403
+ background: var(--bg);
404
+ padding: 1rem;
405
+ border-radius: 8px;
406
+ overflow-x: auto;
407
+ margin: 1rem 0;
408
+ max-width: 100%;
409
+ }
410
+
411
+ .markdown-content pre code {
412
+ background: none;
413
+ padding: 0;
414
+ white-space: pre;
415
+ word-wrap: normal;
416
+ }
417
+
418
+ .markdown-content ul,
419
+ .markdown-content ol {
420
+ margin-left: 1.5rem;
421
+ margin-bottom: 1rem;
422
+ }
423
+
424
+ .markdown-content blockquote {
425
+ border-left: 4px solid var(--primary);
426
+ padding-left: 1rem;
427
+ margin: 1rem 0;
428
+ color: var(--text-muted);
429
+ }
430
+
431
+ .markdown-content table {
432
+ width: 100%;
433
+ border-collapse: collapse;
434
+ margin: 1rem 0;
435
+ }
436
+
437
+ .markdown-content th,
438
+ .markdown-content td {
439
+ padding: 0.75rem;
440
+ border: 1px solid var(--border);
441
+ }
442
+
443
+ .markdown-content th {
444
+ background: var(--bg);
445
+ font-weight: 600;
446
+ }
447
+
448
+ .footer {
449
+ border-top: 1px solid var(--border);
450
+ padding: 2rem 0;
451
+ margin-top: 4rem;
452
+ text-align: center;
453
+ color: var(--text-muted);
454
+ }
455
+
456
+ .footer a {
457
+ color: var(--primary);
458
+ text-decoration: none;
459
+ }
460
+
461
+ .footer a:hover {
462
+ text-decoration: underline;
463
+ }
464
+
frontend/src/main.tsx ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import App from './App.tsx'
4
+ import './index.css'
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')!).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ )
11
+
frontend/tsconfig.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "noEmit": true,
13
+ "jsx": "react-jsx",
14
+ "strict": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "noFallthroughCasesInSwitch": true
18
+ },
19
+ "include": ["src"],
20
+ "references": [{ "path": "./tsconfig.node.json" }]
21
+ }
22
+
frontend/tsconfig.node.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "skipLibCheck": true,
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "allowSyntheticDefaultImports": true
8
+ },
9
+ "include": ["vite.config.ts"]
10
+ }
11
+
frontend/vite.config.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: {
7
+ host: '0.0.0.0', // Allows access from any network interface
8
+ port: 5173,
9
+ },
10
+ })
11
+
generated-docs/.gitkeep ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # Generated Documentation
2
+
3
+ This folder contains documentation files generated by the Docs Extractor agent.
4
+
5
+ Files are automatically saved here when you use the extraction tool.
6
+
main.ts ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import "@std/dotenv/load";
2
+ import {
3
+ AnthropicModelProvider,
4
+ ZypherAgent,
5
+ createZypherContext,
6
+ } from "@corespeed/zypher";
7
+ import { eachValueFrom } from "rxjs-for-await";
8
+
9
+ function getRequiredEnv(name: string): string {
10
+ const value = Deno.env.get(name);
11
+ if (!value) {
12
+ throw new Error(`Environment variable ${name} is not set`);
13
+ }
14
+ return value;
15
+ }
16
+
17
+ const prompt = `You are a documentation extraction agent. Extract and format documentation as clean markdown.
18
+
19
+ RULES:
20
+ - Output ONLY the final markdown document
21
+ - Do NOT include meta-commentary like "I'll help you...", "Let me create...", "Perfect!", etc.
22
+ - Do NOT narrate what you're doing
23
+ - Start directly with the documentation title
24
+
25
+ PROCESS:
26
+ 1. Crawl the main page (be fast - don't follow multiple links)
27
+ 2. Extract: installation, key concepts, 1-2 code examples
28
+ 3. Output the markdown document immediately
29
+
30
+ FORMAT:
31
+ # [Document Title]
32
+ > Source: [original URL]
33
+
34
+ [Your extracted content with headings, code blocks, and links]
35
+
36
+ Be concise and practical. Output the document, not commentary about creating it.`;
37
+
38
+ export class DocsExtractorAgent {
39
+ public zypher: ZypherAgent;
40
+
41
+ private constructor(agent: ZypherAgent) {
42
+ this.zypher = agent;
43
+ }
44
+
45
+ static async create() {
46
+ // Initialize the agent execution context
47
+ const workingDir = Deno.cwd();
48
+ const homeDir = Deno.env.get("HOME") || Deno.env.get("USERPROFILE") || workingDir;
49
+
50
+ const zypherContext = await createZypherContext(workingDir, {
51
+ zypherDir: `${homeDir}/.zypher`,
52
+ enableBuiltInTools: true, // Explicitly enable built-in tools
53
+ });
54
+
55
+ // Create the agent with Anthropic model provider
56
+ const agent = new ZypherAgent(
57
+ zypherContext,
58
+ new AnthropicModelProvider({
59
+ apiKey: getRequiredEnv("ANTHROPIC_API_KEY"),
60
+ }),
61
+ {
62
+ customInstructions: prompt,
63
+ },
64
+ );
65
+
66
+ // Register Firecrawl MCP server
67
+ try {
68
+ await agent.mcp.registerServer({
69
+ id: "firecrawl",
70
+ type: "command",
71
+ command: {
72
+ command: "npx",
73
+ args: ["-y", "firecrawl-mcp"],
74
+ env: {
75
+ FIRECRAWL_API_KEY: getRequiredEnv("FIRECRAWL_API_KEY"),
76
+ FIRECRAWL_TIMEOUT: "30000", // 30 second timeout
77
+ FIRECRAWL_MAX_DEPTH: "1", // Only crawl 1 level deep
78
+ },
79
+ },
80
+ });
81
+
82
+ console.log("✅ Registered Firecrawl MCP server");
83
+ } catch (error) {
84
+ console.error("⚠️ Failed to register Firecrawl MCP server:", error);
85
+ }
86
+
87
+ return new DocsExtractorAgent(agent);
88
+ }
89
+
90
+ async extract(docsUrl: string, extractionType: string, model: string = "claude-sonnet-4-20250514") {
91
+ const userPrompt = `Extract from ${docsUrl}: ${extractionType}
92
+
93
+ OUTPUT ONLY THE MARKDOWN DOCUMENT. No meta-commentary.
94
+
95
+ Start with:
96
+ # [Title for ${extractionType}]
97
+ > Source: ${docsUrl}
98
+
99
+ Then include:
100
+ - Installation/setup (if relevant)
101
+ - Key concepts
102
+ - 1-2 code examples
103
+ - Links to original docs
104
+
105
+ Keep it concise and practical. Output the document directly.`;
106
+
107
+ console.log(`🔍 Starting extraction from ${docsUrl}...`);
108
+
109
+ const event$ = this.zypher.runTask(userPrompt, model);
110
+
111
+ let responseText = "";
112
+ const toolUses: Array<{ name: string; input: unknown }> = [];
113
+
114
+ for await (const event of eachValueFrom(event$)) {
115
+ switch (event.type) {
116
+ case "text":
117
+ case "message":
118
+ if (event.content?.text) {
119
+ responseText += event.content.text;
120
+ } else if (typeof event.content === "string") {
121
+ responseText += event.content;
122
+ }
123
+ break;
124
+
125
+ case "tool_use":
126
+ case "tool_use_input":
127
+ if (event.content?.name) {
128
+ toolUses.push({
129
+ name: event.content.name,
130
+ input: event.content.input,
131
+ });
132
+ } else if (event.tool || event.name) {
133
+ const toolName = event.tool || event.name;
134
+ const toolInput = event.input || event.content;
135
+ toolUses.push({ name: toolName, input: toolInput });
136
+ }
137
+ break;
138
+
139
+ case "tool_result":
140
+ case "tool_use_approved":
141
+ default:
142
+ break;
143
+ }
144
+ }
145
+
146
+ console.log("✅ Extraction completed!");
147
+
148
+ // Save the response text to a file
149
+ try {
150
+ if (responseText) {
151
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
152
+ const filename = `extraction-${timestamp}.md`;
153
+ const filepath = `./generated-docs/${filename}`;
154
+ await Deno.writeTextFile(filepath, responseText);
155
+ console.log(`📄 Saved: ${filename}`);
156
+ }
157
+ } catch (error) {
158
+ console.error("❌ Error saving file:", error);
159
+ }
160
+
161
+ return {
162
+ response: {
163
+ content: [
164
+ {
165
+ type: "text",
166
+ text: responseText,
167
+ },
168
+ ],
169
+ },
170
+ toolUses,
171
+ };
172
+ }
173
+
174
+ async cleanup() {
175
+ }
176
+ }
177
+
178
+ // Terminal mode: Run agent in interactive CLI
179
+ if (import.meta.main) {
180
+ const { runAgentInTerminal } = await import("@corespeed/zypher");
181
+ const agent = await DocsExtractorAgent.create();
182
+ await runAgentInTerminal(agent.zypher, "claude-sonnet-4-20250514");
183
+ await agent.cleanup();
184
+ }
server.ts ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import "@std/dotenv/load";
2
+ import { Application, Router } from "@oak/oak";
3
+ import { DocsExtractorAgent } from "./main.ts";
4
+
5
+ const jobs = new Map<string, { status: string; result?: unknown; error?: string }>();
6
+
7
+ const router = new Router();
8
+
9
+ router
10
+ .post("/api/extract", async (ctx) => {
11
+ try {
12
+ const body = await ctx.request.body.json();
13
+ const { docsUrl, extractionType } = body;
14
+
15
+ if (!docsUrl || !extractionType) {
16
+ ctx.response.status = 400;
17
+ ctx.response.body = { error: "Missing required fields: docsUrl, extractionType" };
18
+ return;
19
+ }
20
+
21
+ const jobId = crypto.randomUUID();
22
+ jobs.set(jobId, { status: "processing" });
23
+
24
+ // Process extraction in background
25
+ (async () => {
26
+ try {
27
+ const agent = await DocsExtractorAgent.create();
28
+ const result = await agent.extract(docsUrl, extractionType, "claude-sonnet-4-20250514");
29
+ jobs.set(jobId, { status: "completed", result });
30
+ await agent.cleanup();
31
+ } catch (error) {
32
+ console.error("Extraction error:", error);
33
+ jobs.set(jobId, {
34
+ status: "failed",
35
+ error: error instanceof Error ? error.message : String(error)
36
+ });
37
+ }
38
+ })();
39
+
40
+ ctx.response.body = { jobId, status: "processing" };
41
+ } catch (error) {
42
+ console.error("API error:", error);
43
+ ctx.response.status = 500;
44
+ ctx.response.body = {
45
+ error: error instanceof Error ? error.message : "Internal server error"
46
+ };
47
+ }
48
+ })
49
+ .get("/api/job/:jobId", (ctx) => {
50
+ const jobId = ctx.params.jobId;
51
+ const job = jobs.get(jobId);
52
+
53
+ if (!job) {
54
+ ctx.response.status = 404;
55
+ ctx.response.body = { error: "Job not found" };
56
+ return;
57
+ }
58
+
59
+ ctx.response.body = job;
60
+ })
61
+ .get("/api/docs", async (ctx) => {
62
+ try {
63
+ const docs: Array<{ name: string; path: string }> = [];
64
+ for await (const entry of Deno.readDir("./generated-docs")) {
65
+ if (entry.isFile && entry.name.endsWith(".md")) {
66
+ docs.push({ name: entry.name, path: `./generated-docs/${entry.name}` });
67
+ }
68
+ }
69
+ ctx.response.body = { docs };
70
+ } catch (error) {
71
+ console.error("Error reading docs:", error);
72
+ ctx.response.body = { docs: [] };
73
+ }
74
+ })
75
+ .get("/api/docs/:filename", async (ctx) => {
76
+ try {
77
+ const filename = ctx.params.filename;
78
+ const content = await Deno.readTextFile(`./generated-docs/${filename}`);
79
+ ctx.response.headers.set("Content-Type", "text/markdown");
80
+ ctx.response.body = content;
81
+ } catch (error) {
82
+ ctx.response.status = 404;
83
+ ctx.response.body = { error: "Document not found" };
84
+ }
85
+ });
86
+
87
+ const app = new Application();
88
+
89
+ // CORS middleware
90
+ app.use(async (ctx, next) => {
91
+ ctx.response.headers.set("Access-Control-Allow-Origin", "*");
92
+ ctx.response.headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
93
+ ctx.response.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
94
+ if (ctx.request.method === "OPTIONS") {
95
+ ctx.response.status = 200;
96
+ return;
97
+ }
98
+ await next();
99
+ });
100
+
101
+ app.use(router.routes());
102
+ app.use(router.allowedMethods());
103
+
104
+ const PORT = 8000;
105
+ console.log(`🚀 Docs Extractor API running on http://localhost:${PORT}`);
106
+ await app.listen({ port: PORT });