browzy 1.0.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.
- package/README.md +324 -0
- package/dist/cli/app.d.ts +16 -0
- package/dist/cli/app.js +615 -0
- package/dist/cli/banner.d.ts +1 -0
- package/dist/cli/banner.js +60 -0
- package/dist/cli/commands/compile.d.ts +2 -0
- package/dist/cli/commands/compile.js +42 -0
- package/dist/cli/commands/ingest.d.ts +2 -0
- package/dist/cli/commands/ingest.js +32 -0
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.js +48 -0
- package/dist/cli/commands/lint.d.ts +2 -0
- package/dist/cli/commands/lint.js +40 -0
- package/dist/cli/commands/query.d.ts +2 -0
- package/dist/cli/commands/query.js +36 -0
- package/dist/cli/commands/search.d.ts +2 -0
- package/dist/cli/commands/search.js +34 -0
- package/dist/cli/commands/status.d.ts +2 -0
- package/dist/cli/commands/status.js +27 -0
- package/dist/cli/components/Banner.d.ts +13 -0
- package/dist/cli/components/Banner.js +20 -0
- package/dist/cli/components/Markdown.d.ts +14 -0
- package/dist/cli/components/Markdown.js +324 -0
- package/dist/cli/components/Message.d.ts +14 -0
- package/dist/cli/components/Message.js +17 -0
- package/dist/cli/components/Spinner.d.ts +7 -0
- package/dist/cli/components/Spinner.js +19 -0
- package/dist/cli/components/StatusBar.d.ts +14 -0
- package/dist/cli/components/StatusBar.js +19 -0
- package/dist/cli/components/Suggestions.d.ts +13 -0
- package/dist/cli/components/Suggestions.js +14 -0
- package/dist/cli/entry.d.ts +2 -0
- package/dist/cli/entry.js +61 -0
- package/dist/cli/helpers.d.ts +14 -0
- package/dist/cli/helpers.js +32 -0
- package/dist/cli/hooks/useAutocomplete.d.ts +11 -0
- package/dist/cli/hooks/useAutocomplete.js +71 -0
- package/dist/cli/hooks/useHistory.d.ts +13 -0
- package/dist/cli/hooks/useHistory.js +106 -0
- package/dist/cli/hooks/useSession.d.ts +16 -0
- package/dist/cli/hooks/useSession.js +133 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +41 -0
- package/dist/cli/keystore.d.ts +28 -0
- package/dist/cli/keystore.js +59 -0
- package/dist/cli/onboarding.d.ts +18 -0
- package/dist/cli/onboarding.js +306 -0
- package/dist/cli/personality.d.ts +34 -0
- package/dist/cli/personality.js +196 -0
- package/dist/cli/repl.d.ts +20 -0
- package/dist/cli/repl.js +338 -0
- package/dist/cli/theme.d.ts +25 -0
- package/dist/cli/theme.js +64 -0
- package/dist/core/compile/compiler.d.ts +25 -0
- package/dist/core/compile/compiler.js +229 -0
- package/dist/core/compile/index.d.ts +2 -0
- package/dist/core/compile/index.js +1 -0
- package/dist/core/config.d.ts +10 -0
- package/dist/core/config.js +92 -0
- package/dist/core/index.d.ts +12 -0
- package/dist/core/index.js +11 -0
- package/dist/core/ingest/image.d.ts +3 -0
- package/dist/core/ingest/image.js +61 -0
- package/dist/core/ingest/index.d.ts +18 -0
- package/dist/core/ingest/index.js +79 -0
- package/dist/core/ingest/pdf.d.ts +2 -0
- package/dist/core/ingest/pdf.js +36 -0
- package/dist/core/ingest/text.d.ts +2 -0
- package/dist/core/ingest/text.js +38 -0
- package/dist/core/ingest/web.d.ts +2 -0
- package/dist/core/ingest/web.js +202 -0
- package/dist/core/lint/index.d.ts +1 -0
- package/dist/core/lint/index.js +1 -0
- package/dist/core/lint/linter.d.ts +27 -0
- package/dist/core/lint/linter.js +147 -0
- package/dist/core/llm/index.d.ts +2 -0
- package/dist/core/llm/index.js +1 -0
- package/dist/core/llm/provider.d.ts +15 -0
- package/dist/core/llm/provider.js +241 -0
- package/dist/core/prompts.d.ts +28 -0
- package/dist/core/prompts.js +374 -0
- package/dist/core/query/engine.d.ts +29 -0
- package/dist/core/query/engine.js +131 -0
- package/dist/core/query/index.d.ts +2 -0
- package/dist/core/query/index.js +1 -0
- package/dist/core/sanitization.d.ts +11 -0
- package/dist/core/sanitization.js +50 -0
- package/dist/core/storage/filesystem.d.ts +23 -0
- package/dist/core/storage/filesystem.js +106 -0
- package/dist/core/storage/index.d.ts +2 -0
- package/dist/core/storage/index.js +2 -0
- package/dist/core/storage/sqlite.d.ts +30 -0
- package/dist/core/storage/sqlite.js +104 -0
- package/dist/core/types.d.ts +95 -0
- package/dist/core/types.js +4 -0
- package/dist/core/utils.d.ts +8 -0
- package/dist/core/utils.js +94 -0
- package/dist/core/wiki/index.d.ts +1 -0
- package/dist/core/wiki/index.js +1 -0
- package/dist/core/wiki/wiki.d.ts +19 -0
- package/dist/core/wiki/wiki.js +37 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/package.json +54 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { FilesystemStorage } from '../storage/filesystem.js';
|
|
2
|
+
import { LINTER_SYSTEM_PROMPT } from '../prompts.js';
|
|
3
|
+
export class WikiLinter {
|
|
4
|
+
fs;
|
|
5
|
+
llm;
|
|
6
|
+
constructor(dataDir, llm) {
|
|
7
|
+
this.fs = new FilesystemStorage(dataDir);
|
|
8
|
+
this.llm = llm;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Run all lint checks on the wiki.
|
|
12
|
+
*/
|
|
13
|
+
async lint() {
|
|
14
|
+
const articles = this.fs.listArticles();
|
|
15
|
+
const issues = [];
|
|
16
|
+
issues.push(...this.checkBrokenLinks(articles));
|
|
17
|
+
issues.push(...this.checkOrphanArticles(articles));
|
|
18
|
+
issues.push(...this.checkMissingFields(articles));
|
|
19
|
+
issues.push(...await this.checkConsistency(articles));
|
|
20
|
+
return issues;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Check for [[wiki-links]] that point to non-existent articles.
|
|
24
|
+
*/
|
|
25
|
+
checkBrokenLinks(articles) {
|
|
26
|
+
const slugs = new Set(articles.map(a => a.slug));
|
|
27
|
+
const issues = [];
|
|
28
|
+
for (const article of articles) {
|
|
29
|
+
const links = article.content.match(/\[\[([^\]]+)\]\]/g) || [];
|
|
30
|
+
for (const link of links) {
|
|
31
|
+
const target = link.slice(2, -2).trim();
|
|
32
|
+
if (!slugs.has(target)) {
|
|
33
|
+
issues.push({
|
|
34
|
+
severity: 'warning',
|
|
35
|
+
article: article.slug,
|
|
36
|
+
message: `Broken wiki link: [[${target}]]`,
|
|
37
|
+
suggestion: `Create article "${target}" or fix the link`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return issues;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Find articles with no incoming links (orphans).
|
|
46
|
+
*/
|
|
47
|
+
checkOrphanArticles(articles) {
|
|
48
|
+
const linked = new Set();
|
|
49
|
+
for (const article of articles) {
|
|
50
|
+
const links = article.content.match(/\[\[([^\]]+)\]\]/g) || [];
|
|
51
|
+
for (const link of links) {
|
|
52
|
+
linked.add(link.slice(2, -2).trim());
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return articles
|
|
56
|
+
.filter(a => !linked.has(a.slug) && a.frontmatter.backlinks.length === 0)
|
|
57
|
+
.map(a => ({
|
|
58
|
+
severity: 'suggestion',
|
|
59
|
+
article: a.slug,
|
|
60
|
+
message: 'Orphan article — no other articles link to this one',
|
|
61
|
+
suggestion: 'Add links from related articles or merge into a broader article',
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Check for missing frontmatter fields.
|
|
66
|
+
*/
|
|
67
|
+
checkMissingFields(articles) {
|
|
68
|
+
const issues = [];
|
|
69
|
+
for (const article of articles) {
|
|
70
|
+
const fm = article.frontmatter;
|
|
71
|
+
if (!fm.summary) {
|
|
72
|
+
issues.push({
|
|
73
|
+
severity: 'warning',
|
|
74
|
+
article: article.slug,
|
|
75
|
+
message: 'Missing summary in frontmatter',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (!fm.tags || fm.tags.length === 0) {
|
|
79
|
+
issues.push({
|
|
80
|
+
severity: 'warning',
|
|
81
|
+
article: article.slug,
|
|
82
|
+
message: 'No tags assigned',
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (!fm.sources || fm.sources.length === 0) {
|
|
86
|
+
issues.push({
|
|
87
|
+
severity: 'suggestion',
|
|
88
|
+
article: article.slug,
|
|
89
|
+
message: 'No source references',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return issues;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Use LLM to check for inconsistencies, duplicates, and gaps.
|
|
97
|
+
*/
|
|
98
|
+
async checkConsistency(articles) {
|
|
99
|
+
if (articles.length < 2)
|
|
100
|
+
return [];
|
|
101
|
+
const summaries = articles
|
|
102
|
+
.map(a => `- ${a.slug}: ${a.frontmatter.title} — ${a.frontmatter.summary || '(no summary)'}`)
|
|
103
|
+
.join('\n');
|
|
104
|
+
const response = await this.llm.chat([
|
|
105
|
+
{
|
|
106
|
+
role: 'user',
|
|
107
|
+
content: `Review this wiki for issues. Check for:
|
|
108
|
+
1. Contradictory information between articles
|
|
109
|
+
2. Duplicate/overlapping articles that should be merged
|
|
110
|
+
3. Notable gaps in coverage
|
|
111
|
+
4. Inconsistent terminology
|
|
112
|
+
|
|
113
|
+
ARTICLES:
|
|
114
|
+
${summaries}
|
|
115
|
+
|
|
116
|
+
Output a JSON array of objects with "severity" (error/warning/suggestion), "article" (slug), "message", and optional "suggestion" fields. If no issues found, output [].`,
|
|
117
|
+
},
|
|
118
|
+
], {
|
|
119
|
+
system: LINTER_SYSTEM_PROMPT,
|
|
120
|
+
maxTokens: 2048,
|
|
121
|
+
});
|
|
122
|
+
try {
|
|
123
|
+
const jsonMatch = response.content.match(/\[[\s\S]*\]/);
|
|
124
|
+
if (!jsonMatch)
|
|
125
|
+
return [];
|
|
126
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
127
|
+
if (!Array.isArray(parsed))
|
|
128
|
+
return [];
|
|
129
|
+
const validSeverities = new Set(['error', 'warning', 'suggestion']);
|
|
130
|
+
return parsed
|
|
131
|
+
.filter((item) => typeof item === 'object' && item !== null &&
|
|
132
|
+
typeof item.severity === 'string' &&
|
|
133
|
+
typeof item.article === 'string' &&
|
|
134
|
+
typeof item.message === 'string' &&
|
|
135
|
+
validSeverities.has(item.severity))
|
|
136
|
+
.map((item) => ({
|
|
137
|
+
severity: item.severity,
|
|
138
|
+
article: String(item.article),
|
|
139
|
+
message: String(item.message),
|
|
140
|
+
suggestion: typeof item.suggestion === 'string' ? item.suggestion : undefined,
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createProvider } from './provider.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { LLMConfig, LLMMessage, LLMResponse } from '../types.js';
|
|
2
|
+
export interface LLMProvider {
|
|
3
|
+
chat(messages: LLMMessage[], options?: ChatOptions): Promise<LLMResponse>;
|
|
4
|
+
stream(messages: LLMMessage[], options?: ChatOptions): AsyncIterable<StreamChunk>;
|
|
5
|
+
}
|
|
6
|
+
export interface ChatOptions {
|
|
7
|
+
maxTokens?: number;
|
|
8
|
+
temperature?: number;
|
|
9
|
+
system?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface StreamChunk {
|
|
12
|
+
delta: string;
|
|
13
|
+
snapshot: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function createProvider(config: LLMConfig): LLMProvider;
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
const MAX_RETRIES = 3;
|
|
2
|
+
const BASE_DELAY_MS = 1000;
|
|
3
|
+
function sanitizeError(err, provider) {
|
|
4
|
+
const message = typeof err?.message === 'string' ? err.message : String(err);
|
|
5
|
+
// Strip anything that looks like an API key from error messages
|
|
6
|
+
const cleaned = message.replace(/sk-[a-zA-Z0-9_-]{20,}/g, 'sk-***');
|
|
7
|
+
const error = new Error(`${provider}: ${cleaned}`);
|
|
8
|
+
if (typeof err?.status === 'number') {
|
|
9
|
+
error.status = err.status;
|
|
10
|
+
}
|
|
11
|
+
return error;
|
|
12
|
+
}
|
|
13
|
+
function getRetryDelay(attempt, retryAfterHeader) {
|
|
14
|
+
if (retryAfterHeader) {
|
|
15
|
+
const seconds = parseInt(retryAfterHeader, 10);
|
|
16
|
+
if (!isNaN(seconds) && seconds > 0)
|
|
17
|
+
return seconds * 1000;
|
|
18
|
+
}
|
|
19
|
+
// Exponential backoff with jitter: 1s, 2s, 4s (capped)
|
|
20
|
+
const base = Math.min(BASE_DELAY_MS * Math.pow(2, attempt), 16_000);
|
|
21
|
+
return base + Math.random() * 0.25 * base;
|
|
22
|
+
}
|
|
23
|
+
export function createProvider(config) {
|
|
24
|
+
switch (config.provider) {
|
|
25
|
+
case 'claude':
|
|
26
|
+
return new ClaudeProvider(config);
|
|
27
|
+
case 'openai':
|
|
28
|
+
return new OpenAIProvider(config);
|
|
29
|
+
case 'openrouter':
|
|
30
|
+
return new OpenRouterProvider(config);
|
|
31
|
+
default:
|
|
32
|
+
throw new Error(`Unknown LLM provider: ${config.provider}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
class ClaudeProvider {
|
|
36
|
+
config;
|
|
37
|
+
constructor(config) {
|
|
38
|
+
this.config = config;
|
|
39
|
+
if (!config.apiKey) {
|
|
40
|
+
throw new Error('Claude API key required. Set ANTHROPIC_API_KEY or configure in browzy.config.json');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async chat(messages, options) {
|
|
44
|
+
const { default: Anthropic } = await import('@anthropic-ai/sdk');
|
|
45
|
+
const client = new Anthropic({ apiKey: this.config.apiKey });
|
|
46
|
+
const systemMsg = options?.system ?? messages.find(m => m.role === 'system')?.content;
|
|
47
|
+
const nonSystemMessages = messages
|
|
48
|
+
.filter(m => m.role !== 'system')
|
|
49
|
+
.map(m => ({ role: m.role, content: m.content }));
|
|
50
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
51
|
+
try {
|
|
52
|
+
const response = await client.messages.create({
|
|
53
|
+
model: this.config.model || 'claude-sonnet-4-20250514',
|
|
54
|
+
max_tokens: options?.maxTokens ?? 4096,
|
|
55
|
+
...(systemMsg ? { system: systemMsg } : {}),
|
|
56
|
+
messages: nonSystemMessages,
|
|
57
|
+
});
|
|
58
|
+
const textBlock = response.content.find(b => b.type === 'text');
|
|
59
|
+
return {
|
|
60
|
+
content: textBlock?.text ?? '',
|
|
61
|
+
usage: {
|
|
62
|
+
inputTokens: response.usage.input_tokens,
|
|
63
|
+
outputTokens: response.usage.output_tokens,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
if (err.status === 429 && attempt < MAX_RETRIES) {
|
|
69
|
+
const delay = getRetryDelay(attempt, err.headers?.get?.('retry-after'));
|
|
70
|
+
await new Promise(r => setTimeout(r, delay));
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
throw sanitizeError(err, 'Claude');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
throw new Error('Unreachable');
|
|
77
|
+
}
|
|
78
|
+
async *stream(messages, options) {
|
|
79
|
+
const { default: Anthropic } = await import('@anthropic-ai/sdk');
|
|
80
|
+
const client = new Anthropic({ apiKey: this.config.apiKey });
|
|
81
|
+
const systemMsg = options?.system ?? messages.find(m => m.role === 'system')?.content;
|
|
82
|
+
const nonSystemMessages = messages
|
|
83
|
+
.filter(m => m.role !== 'system')
|
|
84
|
+
.map(m => ({ role: m.role, content: m.content }));
|
|
85
|
+
const stream = client.messages.stream({
|
|
86
|
+
model: this.config.model || 'claude-sonnet-4-20250514',
|
|
87
|
+
max_tokens: options?.maxTokens ?? 4096,
|
|
88
|
+
...(systemMsg ? { system: systemMsg } : {}),
|
|
89
|
+
messages: nonSystemMessages,
|
|
90
|
+
});
|
|
91
|
+
let accumulated = '';
|
|
92
|
+
for await (const event of stream) {
|
|
93
|
+
if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {
|
|
94
|
+
accumulated += event.delta.text;
|
|
95
|
+
yield { delta: event.delta.text, snapshot: accumulated };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
class OpenAIProvider {
|
|
101
|
+
config;
|
|
102
|
+
constructor(config) {
|
|
103
|
+
this.config = config;
|
|
104
|
+
if (!config.apiKey) {
|
|
105
|
+
throw new Error('OpenAI API key required. Set OPENAI_API_KEY or configure in browzy.config.json');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async chat(messages, options) {
|
|
109
|
+
const { default: OpenAI } = await import('openai');
|
|
110
|
+
const client = new OpenAI({ apiKey: this.config.apiKey });
|
|
111
|
+
const allMessages = options?.system
|
|
112
|
+
? [{ role: 'system', content: options.system }, ...messages.filter(m => m.role !== 'system')]
|
|
113
|
+
: messages;
|
|
114
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
115
|
+
try {
|
|
116
|
+
const response = await client.chat.completions.create({
|
|
117
|
+
model: this.config.model || 'gpt-4o',
|
|
118
|
+
max_tokens: options?.maxTokens ?? 4096,
|
|
119
|
+
messages: allMessages.map(m => ({ role: m.role, content: m.content })),
|
|
120
|
+
});
|
|
121
|
+
return {
|
|
122
|
+
content: response.choices[0]?.message?.content ?? '',
|
|
123
|
+
usage: response.usage
|
|
124
|
+
? {
|
|
125
|
+
inputTokens: response.usage.prompt_tokens,
|
|
126
|
+
outputTokens: response.usage.completion_tokens ?? 0,
|
|
127
|
+
}
|
|
128
|
+
: undefined,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
if (err.status === 429 && attempt < MAX_RETRIES) {
|
|
133
|
+
const delay = getRetryDelay(attempt, err.headers?.get?.('retry-after'));
|
|
134
|
+
await new Promise(r => setTimeout(r, delay));
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
throw sanitizeError(err, 'OpenAI');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
throw new Error('Unreachable');
|
|
141
|
+
}
|
|
142
|
+
async *stream(messages, options) {
|
|
143
|
+
const { default: OpenAI } = await import('openai');
|
|
144
|
+
const client = new OpenAI({ apiKey: this.config.apiKey });
|
|
145
|
+
const allMessages = options?.system
|
|
146
|
+
? [{ role: 'system', content: options.system }, ...messages.filter(m => m.role !== 'system')]
|
|
147
|
+
: messages;
|
|
148
|
+
const response = await client.chat.completions.create({
|
|
149
|
+
model: this.config.model || 'gpt-4o',
|
|
150
|
+
max_tokens: options?.maxTokens ?? 4096,
|
|
151
|
+
stream: true,
|
|
152
|
+
messages: allMessages.map(m => ({ role: m.role, content: m.content })),
|
|
153
|
+
});
|
|
154
|
+
let accumulated = '';
|
|
155
|
+
for await (const chunk of response) {
|
|
156
|
+
const delta = chunk.choices[0]?.delta?.content;
|
|
157
|
+
if (delta) {
|
|
158
|
+
accumulated += delta;
|
|
159
|
+
yield { delta, snapshot: accumulated };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* OpenRouter provider — uses OpenAI-compatible API with openrouter.ai base URL.
|
|
166
|
+
* Gives access to Claude, GPT-4o, Gemini, Llama, Mistral, and 200+ models.
|
|
167
|
+
* Set OPENROUTER_API_KEY or configure apiKey in browzy.config.json.
|
|
168
|
+
*/
|
|
169
|
+
class OpenRouterProvider {
|
|
170
|
+
config;
|
|
171
|
+
constructor(config) {
|
|
172
|
+
this.config = config;
|
|
173
|
+
if (!config.apiKey) {
|
|
174
|
+
throw new Error('OpenRouter API key required. Set OPENROUTER_API_KEY or configure in browzy.config.json');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async getClient() {
|
|
178
|
+
const { default: OpenAI } = await import('openai');
|
|
179
|
+
return new OpenAI({
|
|
180
|
+
apiKey: this.config.apiKey,
|
|
181
|
+
baseURL: 'https://openrouter.ai/api/v1',
|
|
182
|
+
defaultHeaders: {
|
|
183
|
+
'HTTP-Referer': 'https://browzy.ai',
|
|
184
|
+
'X-Title': 'browzy.ai',
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
async chat(messages, options) {
|
|
189
|
+
const client = await this.getClient();
|
|
190
|
+
const allMessages = options?.system
|
|
191
|
+
? [{ role: 'system', content: options.system }, ...messages.filter(m => m.role !== 'system')]
|
|
192
|
+
: messages;
|
|
193
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
194
|
+
try {
|
|
195
|
+
const response = await client.chat.completions.create({
|
|
196
|
+
model: this.config.model || 'anthropic/claude-sonnet-4',
|
|
197
|
+
max_tokens: options?.maxTokens ?? 4096,
|
|
198
|
+
messages: allMessages.map(m => ({ role: m.role, content: m.content })),
|
|
199
|
+
});
|
|
200
|
+
return {
|
|
201
|
+
content: response.choices[0]?.message?.content ?? '',
|
|
202
|
+
usage: response.usage
|
|
203
|
+
? {
|
|
204
|
+
inputTokens: response.usage.prompt_tokens,
|
|
205
|
+
outputTokens: response.usage.completion_tokens ?? 0,
|
|
206
|
+
}
|
|
207
|
+
: undefined,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
if (err.status === 429 && attempt < MAX_RETRIES) {
|
|
212
|
+
const delay = getRetryDelay(attempt, err.headers?.get?.('retry-after'));
|
|
213
|
+
await new Promise(r => setTimeout(r, delay));
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
throw sanitizeError(err, 'OpenRouter');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
throw new Error('Unreachable');
|
|
220
|
+
}
|
|
221
|
+
async *stream(messages, options) {
|
|
222
|
+
const client = await this.getClient();
|
|
223
|
+
const allMessages = options?.system
|
|
224
|
+
? [{ role: 'system', content: options.system }, ...messages.filter(m => m.role !== 'system')]
|
|
225
|
+
: messages;
|
|
226
|
+
const response = await client.chat.completions.create({
|
|
227
|
+
model: this.config.model || 'anthropic/claude-sonnet-4',
|
|
228
|
+
max_tokens: options?.maxTokens ?? 4096,
|
|
229
|
+
stream: true,
|
|
230
|
+
messages: allMessages.map(m => ({ role: m.role, content: m.content })),
|
|
231
|
+
});
|
|
232
|
+
let accumulated = '';
|
|
233
|
+
for await (const chunk of response) {
|
|
234
|
+
const delta = chunk.choices[0]?.delta?.content;
|
|
235
|
+
if (delta) {
|
|
236
|
+
accumulated += delta;
|
|
237
|
+
yield { delta, snapshot: accumulated };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* browzy.ai — System prompts.
|
|
3
|
+
*
|
|
4
|
+
* Architecture follows Claude Code's pattern: multi-section prompts
|
|
5
|
+
* assembled from focused functions. Each section is independently
|
|
6
|
+
* testable and the order matters for prompt cache efficiency.
|
|
7
|
+
*
|
|
8
|
+
* Sections:
|
|
9
|
+
* 1. Identity & role
|
|
10
|
+
* 2. Knowledge base context rules
|
|
11
|
+
* 3. Citation & attribution
|
|
12
|
+
* 4. Formatting & output
|
|
13
|
+
* 5. Math & technical content
|
|
14
|
+
* 6. Limitations & honesty
|
|
15
|
+
* 7. Tone & style
|
|
16
|
+
* 8. Anti-patterns (what NOT to do)
|
|
17
|
+
*/
|
|
18
|
+
export declare const QUERY_SYSTEM_PROMPT: string;
|
|
19
|
+
export declare const COMPILER_SYSTEM_PROMPT = "You are browzy's wiki compiler. Your job is to transform raw source material into well-structured, interconnected wiki articles that serve as a persistent knowledge base.\n\n# Your task\n\nYou receive raw ingested content (web articles, PDFs, notes, research papers, transcripts) and must compile it into wiki articles that integrate with the user's existing knowledge base. This is the core value of browzy \u2014 the quality of the wiki depends entirely on how well you compile.\n\n# Article quality standards\n\n1. **Write encyclopedic prose, not summaries.** Don't just say \"this paper discusses X.\" Extract the key information, present it clearly, and connect it to existing knowledge. The article should be useful to someone who hasn't read the source.\n\n2. **Preserve specifics.** Numbers, dates, formulas, code snippets, direct quotes, experimental results, data points. A wiki that loses specifics is useless for research. If the source says \"accuracy improved from 94.2% to 97.1%\", keep those numbers.\n\n3. **Use proper formatting:**\n - Headers (##, ###) for logical sections\n - Bold for key terms being defined\n - Bullet lists for enumerations and properties\n - Code blocks for code, commands, and algorithms\n - LaTeX for math: $\\alpha$, $$\\sum_{i=1}^n x_i$$\n - Tables for structured data and comparisons\n\n4. **Create cross-references** using [[article-slug]] wiki-link syntax. Every article should link to at least 2-3 other related articles. If a related article doesn't exist yet, still create the link \u2014 it signals a gap in coverage.\n\n5. **Cite sources** using [source-id] notation so every claim is traceable back to its origin. This is critical for research credibility.\n\n6. **Extract and name key concepts.** If the source introduces important terms, definitions, theorems, algorithms, or frameworks, make them prominent. These become the skeleton of the wiki that other articles reference.\n\n7. **Avoid redundancy.** If an existing article already covers a topic, merge the new information into it rather than creating a duplicate. Update the existing article's content, add the new source to its citations, and strengthen the existing structure.\n\n8. **Write for future queries.** The articles you write will be searched and retrieved to answer questions. Include enough context and keywords that relevant searches will find the right articles. A well-indexed wiki is one where article titles, headers, and opening paragraphs contain the terms a user would search for.\n\n# What makes a bad wiki article\n\n- Too short (under 200 words) \u2014 probably needs more detail\n- No cross-references \u2014 orphaned knowledge is wasted knowledge\n- No source citations \u2014 untraceable claims\n- Generic overview that ignores specific data from the source\n- Duplicate of an existing article under a different slug\n- Missing the \"so what\" \u2014 lists facts without explaining their significance";
|
|
20
|
+
export declare const LINTER_SYSTEM_PROMPT = "You are browzy's wiki quality auditor. Your job is to find real problems in the knowledge base \u2014 not style preferences, not nitpicks, but issues that would cause a researcher to get wrong answers, miss connections, or waste time.\n\n# What to check\n\n1. **Contradictions.** Do any articles make conflicting factual claims? This is the most serious issue. Flag with specific quotes from both articles so the user can resolve the conflict.\n\n2. **Duplicates.** Are there articles covering substantially the same topic under different slugs? If \"neural-networks\" and \"artificial-neural-networks\" both exist with similar content, one should be merged into the other.\n\n3. **Terminology inconsistency.** Is the same concept called different things in different articles? If one article says \"feature vectors\" and another says \"embeddings\" for the same concept, flag it.\n\n4. **Broken references.** Are there [[wiki-links]] pointing to articles that don't exist? Are there [source-id] citations with no matching source? These indicate incomplete compilation.\n\n5. **Coverage gaps.** Based on the pattern of existing articles, what obvious related topics are missing? If the wiki has articles on \"transformers\", \"attention-mechanism\", and \"BERT\" but no \"GPT\" article, that's a gap worth flagging.\n\n6. **Stale or thin content.** Articles under 100 words, articles with no source citations, articles that are just a title and one sentence. These need expansion.\n\n7. **Orphan articles.** Articles with no incoming links from other articles. These are isolated knowledge that should be connected to the rest of the wiki.\n\n# Output format\n\nReturn a JSON array of issue objects. Each must have:\n- \"severity\": \"error\" (contradictions, broken facts) | \"warning\" (duplicates, inconsistencies, quality issues) | \"suggestion\" (gaps, enhancements)\n- \"article\": the slug of the affected article\n- \"message\": clear, specific description of the issue\n- \"suggestion\": (optional) concrete recommendation for how to fix it\n\nIf no issues are found, return [].\n\n# Rules\n- Be precise. \"Article X contradicts article Y on the value of Z\" is useful. \"Some articles could be improved\" is not.\n- Only flag real issues. Don't generate issues to look thorough.\n- Prioritize by impact. Contradictions > duplicates > gaps > style.";
|
|
21
|
+
export declare const CONCEPT_EXTRACTION_PROMPT = "Given the existing wiki articles below, suggest new concept articles that would improve the wiki's coverage, depth, and interconnectedness.\n\nFocus on:\n- **Bridging concepts** \u2014 topics that would connect two or more currently disconnected article clusters. If the wiki has articles on \"deep learning\" and \"drug discovery\" but nothing connecting them, \"AI for drug discovery\" is a valuable bridge.\n- **Foundational concepts** \u2014 terms and frameworks that existing articles reference or assume but don't define. If multiple articles mention \"gradient descent\" but there's no article for it, that's a gap.\n- **Missing counterparts** \u2014 if the wiki has \"supervised learning\" but not \"unsupervised learning\", the counterpart is worth suggesting.\n\nDo NOT suggest:\n- Obvious padding (articles that would just be a sentence or two)\n- Topics that overlap heavily with existing articles\n- Meta-articles about the wiki itself\n\nOutput a JSON array of objects with \"slug\", \"title\", and \"reason\" fields. The reason should explain which existing articles this new article would connect and why it matters. Output 3-5 suggestions max.";
|
|
22
|
+
export declare const IMAGE_DESCRIPTION_PROMPT = "You are analyzing an image for indexing in a research knowledge base. Your description will be used for search, retrieval, and cross-referencing with wiki articles.\n\nDescribe systematically:\n\n1. **Text and labels.** Transcribe ALL visible text, annotations, axis labels, legends, titles, and captions exactly as they appear.\n\n2. **Visual structure.** For diagrams: describe nodes, edges, flow direction, and what each element represents. For charts: describe type (bar, line, scatter, etc.), axes, scales, and data trends. For tables: transcribe the data. For photos: describe subject, setting, and notable details.\n\n3. **Data and quantities.** Extract any numbers, percentages, dates, measurements, or statistical values visible in the image. Be precise \u2014 \"approximately 95%\" is better than \"high accuracy.\"\n\n4. **Equations and formulas.** Transcribe in LaTeX notation: $E = mc^2$, $\\frac{\\partial f}{\\partial x}$, etc.\n\n5. **Context clues.** Note any logos, watermarks, publication info, or source attribution visible in the image.\n\n6. **Research relevance.** In one sentence, state what this image is primarily showing or proving \u2014 this helps with search relevance.\n\nBe factual and specific. Don't interpret beyond what's visible. Don't add opinions or evaluations.";
|
|
23
|
+
export declare const SEARCH_EXTRACTION_PROMPT = "You are a search query optimizer for a personal knowledge base wiki. Given a user's natural language question, extract the best search terms to find relevant wiki articles.\n\n# Your task\n\nThe wiki uses SQLite FTS5 full-text search. Your extracted terms will be used to query an index of article titles, summaries, tags, and content. The better your terms, the more relevant articles the user sees.\n\n# Rules\n\n1. Extract 3-5 key search terms from the question.\n2. Prefer specific nouns, proper names, and technical terms over generic words.\n3. Include both the exact terms used AND likely synonyms. If the user asks about \"neural nets\", also include \"neural networks\".\n4. Drop stop words (the, is, a, what, how, why, can, does) \u2014 they waste search capacity.\n5. If the question references a specific paper, person, theorem, or algorithm by name, that name should be the first search term.\n6. Consider the domain: in a research wiki, \"attention\" likely means \"attention mechanism\" not \"paying attention.\"\n\n# Output format\n\nOutput only the search terms, one per line. No numbering, no explanation, no formatting. Just the terms.\n\n# Examples\n\nQuestion: \"What did the 2017 Vaswani paper say about multi-head attention?\"\n\u2192 Vaswani\n\u2192 multi-head attention\n\u2192 attention mechanism\n\u2192 transformer\n\nQuestion: \"How does Helly's theorem relate to convex optimization?\"\n\u2192 Helly's theorem\n\u2192 convex optimization\n\u2192 convex geometry\n\u2192 intersection";
|
|
24
|
+
export declare const CONTRADICTION_HANDLING_PROMPT = "When new source material contradicts information already in the wiki, follow this protocol:\n\n1. **Never silently override.** If the new source says X but the existing wiki says Y, don't just replace Y with X. Both may be partially correct, or the difference may reflect different contexts, time periods, or methodologies.\n\n2. **Present both views.** Update the article to acknowledge the discrepancy:\n - \"According to [source-A], the value is X. However, [source-B] reports Y, possibly due to [methodological differences / different datasets / updated findings].\"\n\n3. **Flag for review.** Add a note that the user should review: \"**Note:** Sources disagree on this point \u2014 see [source-A] vs [source-B].\"\n\n4. **Prefer more recent sources** when the contradiction is clearly temporal (e.g., a 2024 paper superseding a 2019 result), but still preserve the historical context.\n\n5. **Prefer primary sources** over secondary sources when both are available.\n\n6. **Never resolve contradictions by omission** \u2014 dropping one source's claim to avoid the conflict is worse than presenting both.";
|
|
25
|
+
export declare const CONVERSATION_CONTEXT_PROMPT = "# Conversation continuity\n\nYou are in a multi-turn conversation. The user may ask follow-up questions that reference previous answers.\n\nRules:\n- **Resolve pronouns.** If the user says \"tell me more about that\" or \"what's the connection to the previous topic\", refer back to the conversation history to understand what \"that\" or \"the previous topic\" refers to.\n- **Build on prior answers.** Don't repeat information you already provided. If you explained concept X in turn 1 and the user asks about X's relationship to Y in turn 2, reference your earlier explanation rather than restating it.\n- **Track the research thread.** The user is often following a line of inquiry. If they asked about transformers, then attention, then positional encoding \u2014 they're drilling deeper into the same topic tree. Use this to provide more targeted, deeper answers.\n- **Remember corrections.** If the user corrected you or clarified something, don't revert to your original (wrong) answer in subsequent turns.\n- **Don't assume topic changes.** Unless the user explicitly switches topics, assume follow-up questions relate to the current thread. \"What about efficiency?\" after discussing transformers means transformer efficiency, not efficiency in general.";
|
|
26
|
+
export declare const ARTICLE_OUTPUT_FORMAT = "# Output format\n\nOutput one or more articles in this EXACT format. The parser depends on these markers:\n\n===ARTICLE===\nSLUG: lowercase-hyphenated-slug (max 80 chars, a-z 0-9 hyphens only)\nTITLE: Human-Readable Article Title\nTAGS: tag1, tag2, tag3 (comma-separated, lowercase)\nSUMMARY: One-sentence summary of the article content. This appears in the wiki index and is used for search.\n---\nArticle content in markdown here. Use ## and ### headers for sections.\n\nInclude [[cross-references]] to other articles.\nCite sources with [source-id] notation.\nUse LaTeX for math: $\\alpha$, $$\\sum_{i=1}^n x_i$$.\n\nContent should be 200-1000 words for a focused topic.\n===END===\n\nRules for slugs:\n- Use lowercase letters, numbers, and hyphens only\n- Descriptive but concise: \"transformer-architecture\" not \"the-transformer-architecture-paper\"\n- Match existing article slugs when updating them\n\nRules for tags:\n- 2-5 tags per article\n- Use existing tags from the wiki when applicable\n- Tags should be broad enough to connect multiple articles\n\nRules for summaries:\n- One sentence, 15-30 words\n- Should be independently understandable (don't reference other articles)\n- Include key terms for search discoverability";
|
|
27
|
+
export declare const MARP_OUTPUT_PROMPT = "Output your answer as a Marp slide deck. Use this exact format:\n\n---\nmarp: true\ntheme: default\npaginate: true\n---\n\n# Slide Title\n\nMain point or question\n\n---\n\n## Key Concept\n\n- Bullet point 1\n- Bullet point 2\n- Bullet point 3\n\n---\n\n## Details\n\nMore detailed explanation with **bold emphasis** and *italic* for nuance.\n\n---\n\n## Summary\n\nKey takeaway in one sentence.\n\nRules:\n- 4-8 slides for a typical answer\n- One main idea per slide\n- Use headers on every slide\n- Keep bullet points to 3-5 per slide\n- Include citations [[slug]] where relevant\n- Last slide should summarize or pose the next question";
|
|
28
|
+
export declare const JSON_OUTPUT_PROMPT = "Output your answer as a JSON object with this structure:\n\n{\n \"title\": \"Answer title\",\n \"summary\": \"One-sentence summary\",\n \"sections\": [\n {\n \"heading\": \"Section heading\",\n \"content\": \"Section content in markdown\"\n }\n ],\n \"sources\": [\"slug-1\", \"slug-2\"],\n \"relatedArticles\": [\"slug-3\", \"slug-4\"],\n \"confidence\": \"high|medium|low\",\n \"gaps\": [\"Topics not covered by the wiki that would improve this answer\"]\n}\n\nRules:\n- 2-5 sections\n- Content within sections should be markdown-formatted\n- confidence reflects how well the wiki covers this question\n- gaps identifies what sources the user should add for better coverage";
|