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.
Files changed (104) hide show
  1. package/README.md +324 -0
  2. package/dist/cli/app.d.ts +16 -0
  3. package/dist/cli/app.js +615 -0
  4. package/dist/cli/banner.d.ts +1 -0
  5. package/dist/cli/banner.js +60 -0
  6. package/dist/cli/commands/compile.d.ts +2 -0
  7. package/dist/cli/commands/compile.js +42 -0
  8. package/dist/cli/commands/ingest.d.ts +2 -0
  9. package/dist/cli/commands/ingest.js +32 -0
  10. package/dist/cli/commands/init.d.ts +2 -0
  11. package/dist/cli/commands/init.js +48 -0
  12. package/dist/cli/commands/lint.d.ts +2 -0
  13. package/dist/cli/commands/lint.js +40 -0
  14. package/dist/cli/commands/query.d.ts +2 -0
  15. package/dist/cli/commands/query.js +36 -0
  16. package/dist/cli/commands/search.d.ts +2 -0
  17. package/dist/cli/commands/search.js +34 -0
  18. package/dist/cli/commands/status.d.ts +2 -0
  19. package/dist/cli/commands/status.js +27 -0
  20. package/dist/cli/components/Banner.d.ts +13 -0
  21. package/dist/cli/components/Banner.js +20 -0
  22. package/dist/cli/components/Markdown.d.ts +14 -0
  23. package/dist/cli/components/Markdown.js +324 -0
  24. package/dist/cli/components/Message.d.ts +14 -0
  25. package/dist/cli/components/Message.js +17 -0
  26. package/dist/cli/components/Spinner.d.ts +7 -0
  27. package/dist/cli/components/Spinner.js +19 -0
  28. package/dist/cli/components/StatusBar.d.ts +14 -0
  29. package/dist/cli/components/StatusBar.js +19 -0
  30. package/dist/cli/components/Suggestions.d.ts +13 -0
  31. package/dist/cli/components/Suggestions.js +14 -0
  32. package/dist/cli/entry.d.ts +2 -0
  33. package/dist/cli/entry.js +61 -0
  34. package/dist/cli/helpers.d.ts +14 -0
  35. package/dist/cli/helpers.js +32 -0
  36. package/dist/cli/hooks/useAutocomplete.d.ts +11 -0
  37. package/dist/cli/hooks/useAutocomplete.js +71 -0
  38. package/dist/cli/hooks/useHistory.d.ts +13 -0
  39. package/dist/cli/hooks/useHistory.js +106 -0
  40. package/dist/cli/hooks/useSession.d.ts +16 -0
  41. package/dist/cli/hooks/useSession.js +133 -0
  42. package/dist/cli/index.d.ts +2 -0
  43. package/dist/cli/index.js +41 -0
  44. package/dist/cli/keystore.d.ts +28 -0
  45. package/dist/cli/keystore.js +59 -0
  46. package/dist/cli/onboarding.d.ts +18 -0
  47. package/dist/cli/onboarding.js +306 -0
  48. package/dist/cli/personality.d.ts +34 -0
  49. package/dist/cli/personality.js +196 -0
  50. package/dist/cli/repl.d.ts +20 -0
  51. package/dist/cli/repl.js +338 -0
  52. package/dist/cli/theme.d.ts +25 -0
  53. package/dist/cli/theme.js +64 -0
  54. package/dist/core/compile/compiler.d.ts +25 -0
  55. package/dist/core/compile/compiler.js +229 -0
  56. package/dist/core/compile/index.d.ts +2 -0
  57. package/dist/core/compile/index.js +1 -0
  58. package/dist/core/config.d.ts +10 -0
  59. package/dist/core/config.js +92 -0
  60. package/dist/core/index.d.ts +12 -0
  61. package/dist/core/index.js +11 -0
  62. package/dist/core/ingest/image.d.ts +3 -0
  63. package/dist/core/ingest/image.js +61 -0
  64. package/dist/core/ingest/index.d.ts +18 -0
  65. package/dist/core/ingest/index.js +79 -0
  66. package/dist/core/ingest/pdf.d.ts +2 -0
  67. package/dist/core/ingest/pdf.js +36 -0
  68. package/dist/core/ingest/text.d.ts +2 -0
  69. package/dist/core/ingest/text.js +38 -0
  70. package/dist/core/ingest/web.d.ts +2 -0
  71. package/dist/core/ingest/web.js +202 -0
  72. package/dist/core/lint/index.d.ts +1 -0
  73. package/dist/core/lint/index.js +1 -0
  74. package/dist/core/lint/linter.d.ts +27 -0
  75. package/dist/core/lint/linter.js +147 -0
  76. package/dist/core/llm/index.d.ts +2 -0
  77. package/dist/core/llm/index.js +1 -0
  78. package/dist/core/llm/provider.d.ts +15 -0
  79. package/dist/core/llm/provider.js +241 -0
  80. package/dist/core/prompts.d.ts +28 -0
  81. package/dist/core/prompts.js +374 -0
  82. package/dist/core/query/engine.d.ts +29 -0
  83. package/dist/core/query/engine.js +131 -0
  84. package/dist/core/query/index.d.ts +2 -0
  85. package/dist/core/query/index.js +1 -0
  86. package/dist/core/sanitization.d.ts +11 -0
  87. package/dist/core/sanitization.js +50 -0
  88. package/dist/core/storage/filesystem.d.ts +23 -0
  89. package/dist/core/storage/filesystem.js +106 -0
  90. package/dist/core/storage/index.d.ts +2 -0
  91. package/dist/core/storage/index.js +2 -0
  92. package/dist/core/storage/sqlite.d.ts +30 -0
  93. package/dist/core/storage/sqlite.js +104 -0
  94. package/dist/core/types.d.ts +95 -0
  95. package/dist/core/types.js +4 -0
  96. package/dist/core/utils.d.ts +8 -0
  97. package/dist/core/utils.js +94 -0
  98. package/dist/core/wiki/index.d.ts +1 -0
  99. package/dist/core/wiki/index.js +1 -0
  100. package/dist/core/wiki/wiki.d.ts +19 -0
  101. package/dist/core/wiki/wiki.js +37 -0
  102. package/dist/index.d.ts +2 -0
  103. package/dist/index.js +3 -0
  104. 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,2 @@
1
+ #!/usr/bin/env node
2
+ import 'dotenv/config';
@@ -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;