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,13 @@
|
|
|
1
|
+
export declare function useHistory(): {
|
|
2
|
+
history: string[];
|
|
3
|
+
historyIndex: number;
|
|
4
|
+
addToHistory: (input: string) => void;
|
|
5
|
+
navigateHistory: (direction: "up" | "down", currentInput: string) => string | null;
|
|
6
|
+
searchMode: boolean;
|
|
7
|
+
setSearchMode: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
8
|
+
searchQuery: string;
|
|
9
|
+
setSearchQuery: import("react").Dispatch<import("react").SetStateAction<string>>;
|
|
10
|
+
searchHistory: (query: string) => string[];
|
|
11
|
+
stashedInput: string;
|
|
12
|
+
setStashedInput: import("react").Dispatch<import("react").SetStateAction<string>>;
|
|
13
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { useState, useCallback, useRef } from 'react';
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
const HISTORY_FILE = join(homedir(), '.browzy', 'history.json');
|
|
6
|
+
const MAX_HISTORY = 500;
|
|
7
|
+
function loadHistory() {
|
|
8
|
+
try {
|
|
9
|
+
if (existsSync(HISTORY_FILE)) {
|
|
10
|
+
const data = JSON.parse(readFileSync(HISTORY_FILE, 'utf-8'));
|
|
11
|
+
if (Array.isArray(data))
|
|
12
|
+
return data;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
catch { /* ignore */ }
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
function saveHistory(items) {
|
|
19
|
+
mkdirSync(join(homedir(), '.browzy'), { recursive: true });
|
|
20
|
+
writeFileSync(HISTORY_FILE, JSON.stringify(items.slice(-MAX_HISTORY)), 'utf-8');
|
|
21
|
+
}
|
|
22
|
+
export function useHistory() {
|
|
23
|
+
const [history, setHistory] = useState(() => loadHistory());
|
|
24
|
+
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
25
|
+
const [searchMode, setSearchMode] = useState(false);
|
|
26
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
27
|
+
const [stashedInput, setStashedInput] = useState('');
|
|
28
|
+
// Refs to avoid stale closures during rapid keypresses
|
|
29
|
+
const historyIndexRef = useRef(historyIndex);
|
|
30
|
+
historyIndexRef.current = historyIndex;
|
|
31
|
+
const historyRef = useRef(history);
|
|
32
|
+
historyRef.current = history;
|
|
33
|
+
const stashedRef = useRef(stashedInput);
|
|
34
|
+
stashedRef.current = stashedInput;
|
|
35
|
+
const addToHistory = useCallback((input) => {
|
|
36
|
+
if (!input.trim())
|
|
37
|
+
return;
|
|
38
|
+
setHistory(prev => {
|
|
39
|
+
// Deduplicate consecutive
|
|
40
|
+
if (prev[prev.length - 1] === input)
|
|
41
|
+
return prev;
|
|
42
|
+
const next = [...prev, input];
|
|
43
|
+
saveHistory(next);
|
|
44
|
+
return next;
|
|
45
|
+
});
|
|
46
|
+
setHistoryIndex(-1);
|
|
47
|
+
}, []);
|
|
48
|
+
const navigateHistory = useCallback((direction, currentInput) => {
|
|
49
|
+
const h = historyRef.current;
|
|
50
|
+
const idx = historyIndexRef.current;
|
|
51
|
+
if (h.length === 0)
|
|
52
|
+
return null;
|
|
53
|
+
if (idx === -1 && direction === 'up') {
|
|
54
|
+
setStashedInput(currentInput);
|
|
55
|
+
const newIdx = h.length - 1;
|
|
56
|
+
setHistoryIndex(newIdx);
|
|
57
|
+
historyIndexRef.current = newIdx;
|
|
58
|
+
return h[newIdx];
|
|
59
|
+
}
|
|
60
|
+
if (direction === 'up') {
|
|
61
|
+
const newIdx = Math.max(0, idx - 1);
|
|
62
|
+
setHistoryIndex(newIdx);
|
|
63
|
+
historyIndexRef.current = newIdx;
|
|
64
|
+
return h[newIdx];
|
|
65
|
+
}
|
|
66
|
+
// Down
|
|
67
|
+
if (idx >= h.length - 1) {
|
|
68
|
+
setHistoryIndex(-1);
|
|
69
|
+
historyIndexRef.current = -1;
|
|
70
|
+
return stashedRef.current;
|
|
71
|
+
}
|
|
72
|
+
const newIdx = idx + 1;
|
|
73
|
+
setHistoryIndex(newIdx);
|
|
74
|
+
historyIndexRef.current = newIdx;
|
|
75
|
+
return h[newIdx];
|
|
76
|
+
}, []);
|
|
77
|
+
const searchHistory = useCallback((query) => {
|
|
78
|
+
const h = historyRef.current;
|
|
79
|
+
if (!query)
|
|
80
|
+
return h.slice(-10).reverse();
|
|
81
|
+
const lower = query.toLowerCase();
|
|
82
|
+
const seen = new Set();
|
|
83
|
+
const results = [];
|
|
84
|
+
for (let i = h.length - 1; i >= 0 && results.length < 10; i--) {
|
|
85
|
+
const item = h[i];
|
|
86
|
+
if (item.toLowerCase().includes(lower) && !seen.has(item)) {
|
|
87
|
+
seen.add(item);
|
|
88
|
+
results.push(item);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return results;
|
|
92
|
+
}, []);
|
|
93
|
+
return {
|
|
94
|
+
history,
|
|
95
|
+
historyIndex,
|
|
96
|
+
addToHistory,
|
|
97
|
+
navigateHistory,
|
|
98
|
+
searchMode,
|
|
99
|
+
setSearchMode,
|
|
100
|
+
searchQuery,
|
|
101
|
+
setSearchQuery,
|
|
102
|
+
searchHistory,
|
|
103
|
+
stashedInput,
|
|
104
|
+
setStashedInput,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { MessageData } from '../components/Message.js';
|
|
2
|
+
export interface Session {
|
|
3
|
+
id: string;
|
|
4
|
+
messages: MessageData[];
|
|
5
|
+
createdAt: string;
|
|
6
|
+
lastUpdated: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function useSession(): {
|
|
9
|
+
sessionId: string;
|
|
10
|
+
messages: MessageData[];
|
|
11
|
+
addMessage: (role: MessageData["role"], content: string, sources?: string[]) => MessageData;
|
|
12
|
+
setMessages: import("react").Dispatch<import("react").SetStateAction<MessageData[]>>;
|
|
13
|
+
saveSession: () => void;
|
|
14
|
+
loadLastSession: () => Session | null;
|
|
15
|
+
exportSession: (outputPath: string) => string;
|
|
16
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, unlinkSync, statSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
const SESSIONS_DIR = join(homedir(), '.browzy', 'sessions');
|
|
6
|
+
const MAX_SESSIONS = 50;
|
|
7
|
+
function generateId() {
|
|
8
|
+
return Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Redact sensitive patterns from exported text.
|
|
12
|
+
*/
|
|
13
|
+
function redactSecrets(text) {
|
|
14
|
+
return text
|
|
15
|
+
.replace(/sk-ant-[a-zA-Z0-9\-_]{20,}/g, '[REDACTED_API_KEY]')
|
|
16
|
+
.replace(/sk-[a-zA-Z0-9\-_]{48,}/g, '[REDACTED_API_KEY]')
|
|
17
|
+
.replace(/Bearer\s+[a-zA-Z0-9\-_.]{20,}/gi, 'Bearer [REDACTED]');
|
|
18
|
+
}
|
|
19
|
+
export function useSession() {
|
|
20
|
+
const [sessionId] = useState(() => generateId());
|
|
21
|
+
const [messages, setMessages] = useState([]);
|
|
22
|
+
const addMessage = useCallback((role, content, sources) => {
|
|
23
|
+
const msg = {
|
|
24
|
+
id: generateId(),
|
|
25
|
+
role,
|
|
26
|
+
content,
|
|
27
|
+
sources,
|
|
28
|
+
timestamp: Date.now(),
|
|
29
|
+
};
|
|
30
|
+
setMessages(prev => [...prev, msg]);
|
|
31
|
+
return msg;
|
|
32
|
+
}, []);
|
|
33
|
+
const saveSession = useCallback(() => {
|
|
34
|
+
if (messages.length === 0)
|
|
35
|
+
return;
|
|
36
|
+
mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
37
|
+
const session = {
|
|
38
|
+
id: sessionId,
|
|
39
|
+
messages,
|
|
40
|
+
createdAt: messages[0]?.timestamp ? new Date(messages[0].timestamp).toISOString() : new Date().toISOString(),
|
|
41
|
+
lastUpdated: new Date().toISOString(),
|
|
42
|
+
};
|
|
43
|
+
// Atomic write: write to temp file then rename
|
|
44
|
+
const finalPath = join(SESSIONS_DIR, `${sessionId}.json`);
|
|
45
|
+
const tmpPath = finalPath + '.tmp';
|
|
46
|
+
writeFileSync(tmpPath, JSON.stringify(session, null, 2), 'utf-8');
|
|
47
|
+
const { renameSync } = require('fs');
|
|
48
|
+
try {
|
|
49
|
+
renameSync(tmpPath, finalPath);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Fallback: direct write if rename fails (cross-device)
|
|
53
|
+
writeFileSync(finalPath, JSON.stringify(session, null, 2), 'utf-8');
|
|
54
|
+
try {
|
|
55
|
+
unlinkSync(tmpPath);
|
|
56
|
+
}
|
|
57
|
+
catch { /* ignore */ }
|
|
58
|
+
}
|
|
59
|
+
// Prune old sessions — sort by mtime, keep newest
|
|
60
|
+
try {
|
|
61
|
+
const files = readdirSync(SESSIONS_DIR)
|
|
62
|
+
.filter(f => f.endsWith('.json') && !f.endsWith('.tmp'))
|
|
63
|
+
.map(f => ({
|
|
64
|
+
name: f,
|
|
65
|
+
mtime: statSync(join(SESSIONS_DIR, f)).mtimeMs,
|
|
66
|
+
}))
|
|
67
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
68
|
+
for (const file of files.slice(MAX_SESSIONS)) {
|
|
69
|
+
try {
|
|
70
|
+
unlinkSync(join(SESSIONS_DIR, file.name));
|
|
71
|
+
}
|
|
72
|
+
catch { /* ignore */ }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch { /* ignore */ }
|
|
76
|
+
}, [sessionId, messages]);
|
|
77
|
+
const loadLastSession = useCallback(() => {
|
|
78
|
+
try {
|
|
79
|
+
if (!existsSync(SESSIONS_DIR))
|
|
80
|
+
return null;
|
|
81
|
+
const files = readdirSync(SESSIONS_DIR)
|
|
82
|
+
.filter(f => f.endsWith('.json') && !f.endsWith('.tmp'))
|
|
83
|
+
.map(f => ({
|
|
84
|
+
name: f,
|
|
85
|
+
mtime: statSync(join(SESSIONS_DIR, f)).mtimeMs,
|
|
86
|
+
}))
|
|
87
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
88
|
+
if (files.length === 0)
|
|
89
|
+
return null;
|
|
90
|
+
return JSON.parse(readFileSync(join(SESSIONS_DIR, files[0].name), 'utf-8'));
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}, []);
|
|
96
|
+
const exportSession = useCallback((outputPath) => {
|
|
97
|
+
mkdirSync(join(outputPath, '..'), { recursive: true });
|
|
98
|
+
const lines = [
|
|
99
|
+
'---',
|
|
100
|
+
`title: "browzy session ${sessionId}"`,
|
|
101
|
+
`date: "${new Date().toISOString()}"`,
|
|
102
|
+
`messageCount: ${messages.length}`,
|
|
103
|
+
'---',
|
|
104
|
+
'',
|
|
105
|
+
];
|
|
106
|
+
for (const msg of messages) {
|
|
107
|
+
if (msg.role === 'user') {
|
|
108
|
+
lines.push(`**> ${msg.content}**`, '');
|
|
109
|
+
}
|
|
110
|
+
else if (msg.role === 'assistant') {
|
|
111
|
+
lines.push(redactSecrets(msg.content), '');
|
|
112
|
+
if (msg.sources?.length) {
|
|
113
|
+
lines.push(`*Sources: ${msg.sources.join(', ')}*`, '');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
lines.push(`> ${redactSecrets(msg.content)}`, '');
|
|
118
|
+
}
|
|
119
|
+
lines.push('---', '');
|
|
120
|
+
}
|
|
121
|
+
writeFileSync(outputPath, lines.join('\n'), 'utf-8');
|
|
122
|
+
return outputPath;
|
|
123
|
+
}, [sessionId, messages]);
|
|
124
|
+
return {
|
|
125
|
+
sessionId,
|
|
126
|
+
messages,
|
|
127
|
+
addMessage,
|
|
128
|
+
setMessages,
|
|
129
|
+
saveSession,
|
|
130
|
+
loadLastSession,
|
|
131
|
+
exportSession,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import 'dotenv/config';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { initCommand } from './commands/init.js';
|
|
5
|
+
import { ingestCommand } from './commands/ingest.js';
|
|
6
|
+
import { compileCommand } from './commands/compile.js';
|
|
7
|
+
import { queryCommand } from './commands/query.js';
|
|
8
|
+
import { lintCommand } from './commands/lint.js';
|
|
9
|
+
import { statusCommand } from './commands/status.js';
|
|
10
|
+
import { searchCommand } from './commands/search.js';
|
|
11
|
+
import { showBanner } from './banner.js';
|
|
12
|
+
import { BrowzyRepl } from './repl.js';
|
|
13
|
+
import { needsOnboarding, runOnboarding } from './onboarding.js';
|
|
14
|
+
const program = new Command();
|
|
15
|
+
program
|
|
16
|
+
.name('browzy')
|
|
17
|
+
.description('LLM-powered personal knowledge base engine')
|
|
18
|
+
.version('1.0.0');
|
|
19
|
+
program.addCommand(initCommand);
|
|
20
|
+
program.addCommand(ingestCommand);
|
|
21
|
+
program.addCommand(compileCommand);
|
|
22
|
+
program.addCommand(queryCommand);
|
|
23
|
+
program.addCommand(lintCommand);
|
|
24
|
+
program.addCommand(statusCommand);
|
|
25
|
+
program.addCommand(searchCommand);
|
|
26
|
+
// No args → interactive mode
|
|
27
|
+
if (process.argv.length <= 2) {
|
|
28
|
+
(async () => {
|
|
29
|
+
if (needsOnboarding()) {
|
|
30
|
+
const success = await runOnboarding();
|
|
31
|
+
if (!success)
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
showBanner();
|
|
35
|
+
const repl = new BrowzyRepl();
|
|
36
|
+
repl.start();
|
|
37
|
+
})();
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
program.parse();
|
|
41
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* browzy.ai — API key storage.
|
|
3
|
+
*
|
|
4
|
+
* Stores API keys in ~/.browzy/keys.json with restricted permissions.
|
|
5
|
+
* Keys are loaded into memory at startup and can be added at runtime
|
|
6
|
+
* via /model without restarting.
|
|
7
|
+
*/
|
|
8
|
+
interface KeyStore {
|
|
9
|
+
anthropic?: string;
|
|
10
|
+
openai?: string;
|
|
11
|
+
openrouter?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function loadKeys(): KeyStore;
|
|
14
|
+
export declare function saveKey(provider: 'anthropic' | 'openai' | 'openrouter', key: string): void;
|
|
15
|
+
/**
|
|
16
|
+
* Get API key for a provider. Checks:
|
|
17
|
+
* 1. Environment variable
|
|
18
|
+
* 2. ~/.browzy/keys.json
|
|
19
|
+
*/
|
|
20
|
+
export declare function getKey(provider: 'anthropic' | 'openai' | 'openrouter'): string | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Detect if a string looks like an API key.
|
|
23
|
+
*/
|
|
24
|
+
export declare function looksLikeApiKey(input: string): {
|
|
25
|
+
provider: 'anthropic' | 'openai' | 'openrouter';
|
|
26
|
+
key: string;
|
|
27
|
+
} | null;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* browzy.ai — API key storage.
|
|
3
|
+
*
|
|
4
|
+
* Stores API keys in ~/.browzy/keys.json with restricted permissions.
|
|
5
|
+
* Keys are loaded into memory at startup and can be added at runtime
|
|
6
|
+
* via /model without restarting.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { homedir } from 'os';
|
|
11
|
+
const KEYS_FILE = join(homedir(), '.browzy', 'keys.json');
|
|
12
|
+
export function loadKeys() {
|
|
13
|
+
try {
|
|
14
|
+
if (existsSync(KEYS_FILE)) {
|
|
15
|
+
const parsed = JSON.parse(readFileSync(KEYS_FILE, 'utf-8'));
|
|
16
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
17
|
+
return {};
|
|
18
|
+
return parsed;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch { /* ignore */ }
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
export function saveKey(provider, key) {
|
|
25
|
+
mkdirSync(join(homedir(), '.browzy'), { recursive: true });
|
|
26
|
+
const keys = loadKeys();
|
|
27
|
+
keys[provider] = key;
|
|
28
|
+
writeFileSync(KEYS_FILE, JSON.stringify(keys, null, 2), { encoding: 'utf-8', mode: 0o600 });
|
|
29
|
+
try {
|
|
30
|
+
chmodSync(KEYS_FILE, 0o600);
|
|
31
|
+
}
|
|
32
|
+
catch { /* ensure perms even if file existed */ }
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Get API key for a provider. Checks:
|
|
36
|
+
* 1. Environment variable
|
|
37
|
+
* 2. ~/.browzy/keys.json
|
|
38
|
+
*/
|
|
39
|
+
export function getKey(provider) {
|
|
40
|
+
const envMap = {
|
|
41
|
+
anthropic: 'ANTHROPIC_API_KEY',
|
|
42
|
+
openai: 'OPENAI_API_KEY',
|
|
43
|
+
openrouter: 'OPENROUTER_API_KEY',
|
|
44
|
+
};
|
|
45
|
+
return process.env[envMap[provider]] || loadKeys()[provider] || undefined;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Detect if a string looks like an API key.
|
|
49
|
+
*/
|
|
50
|
+
export function looksLikeApiKey(input) {
|
|
51
|
+
const trimmed = input.trim();
|
|
52
|
+
if (trimmed.startsWith('sk-ant-'))
|
|
53
|
+
return { provider: 'anthropic', key: trimmed };
|
|
54
|
+
if (trimmed.startsWith('sk-or-'))
|
|
55
|
+
return { provider: 'openrouter', key: trimmed };
|
|
56
|
+
if (trimmed.startsWith('sk-') && trimmed.length > 30)
|
|
57
|
+
return { provider: 'openai', key: trimmed };
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface UserProfile {
|
|
2
|
+
name: string;
|
|
3
|
+
createdAt: string;
|
|
4
|
+
lastSeen: string;
|
|
5
|
+
sessionCount: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function loadProfile(): UserProfile | null;
|
|
8
|
+
export declare function saveProfile(profile: UserProfile): void;
|
|
9
|
+
export declare function touchProfile(): UserProfile | null;
|
|
10
|
+
export declare function getWelcomeMessage(profile: UserProfile, stats?: {
|
|
11
|
+
sources: number;
|
|
12
|
+
articles: number;
|
|
13
|
+
}): string;
|
|
14
|
+
export declare function runOnboarding(): Promise<boolean>;
|
|
15
|
+
/**
|
|
16
|
+
* Check if onboarding is needed (no profile exists).
|
|
17
|
+
*/
|
|
18
|
+
export declare function needsOnboarding(): boolean;
|