osborn 0.5.3 → 0.5.5

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 (37) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/.claude/skills/markdown-to-pdf/SKILL.md +29 -0
  3. package/.claude/skills/pdf-to-markdown/SKILL.md +28 -0
  4. package/.claude/skills/playwright-browser/SKILL.md +75 -0
  5. package/.claude/skills/youtube-transcript/SKILL.md +24 -0
  6. package/dist/claude-llm.d.ts +29 -1
  7. package/dist/claude-llm.js +334 -78
  8. package/dist/config.d.ts +5 -1
  9. package/dist/config.js +4 -1
  10. package/dist/fast-brain.d.ts +70 -16
  11. package/dist/fast-brain.js +662 -99
  12. package/dist/index-3-2-26-legacy.d.ts +1 -0
  13. package/dist/index-3-2-26-legacy.js +2233 -0
  14. package/dist/index.js +752 -423
  15. package/dist/jsonl-search.d.ts +66 -0
  16. package/dist/jsonl-search.js +274 -0
  17. package/dist/leagcyprompts2.d.ts +0 -0
  18. package/dist/leagcyprompts2.js +573 -0
  19. package/dist/pipeline-direct-llm.d.ts +77 -0
  20. package/dist/pipeline-direct-llm.js +216 -0
  21. package/dist/pipeline-fastbrain.d.ts +45 -0
  22. package/dist/pipeline-fastbrain.js +367 -0
  23. package/dist/prompts-2-25-26.d.ts +0 -0
  24. package/dist/prompts-2-25-26.js +518 -0
  25. package/dist/prompts-3-2-26.d.ts +78 -0
  26. package/dist/prompts-3-2-26.js +1319 -0
  27. package/dist/prompts.d.ts +83 -12
  28. package/dist/prompts.js +1991 -588
  29. package/dist/session-access.d.ts +24 -0
  30. package/dist/session-access.js +74 -0
  31. package/dist/summary-index.d.ts +87 -0
  32. package/dist/summary-index.js +570 -0
  33. package/dist/turn-detector-shim.d.ts +24 -0
  34. package/dist/turn-detector-shim.js +83 -0
  35. package/dist/voice-io.d.ts +9 -3
  36. package/dist/voice-io.js +39 -20
  37. package/package.json +13 -10
@@ -0,0 +1,66 @@
1
+ /**
2
+ * jsonl-search.ts — Ripgrep search and BM25 search utilities for JSONL session files
3
+ *
4
+ * Provides two search strategies over Claude Agent SDK session data:
5
+ * 1. ripgrepSearch — fast regex search via rg (or grep fallback) across files
6
+ * 2. bm25Search — in-memory full-text search over parsed session messages
7
+ *
8
+ * Also provides resolveJsonlDir() for locating the JSONL project directory
9
+ * and invalidateBM25Cache() for cache management.
10
+ */
11
+ export interface RipgrepResult {
12
+ lineNumber: number;
13
+ filePath: string;
14
+ content: string;
15
+ }
16
+ export interface BM25Result {
17
+ content: string;
18
+ score: number;
19
+ type?: string;
20
+ }
21
+ /**
22
+ * Search files using ripgrep (rg) with fallback to grep.
23
+ *
24
+ * @param searchDir - Directory to search in
25
+ * @param pattern - Regex pattern to search for (validated for safety)
26
+ * @param opts.maxResults - Maximum number of results (default: 100)
27
+ * @param opts.include - Glob pattern for file inclusion (e.g., "*.jsonl")
28
+ * @returns Array of matches with line number, file path, and content
29
+ */
30
+ export declare function ripgrepSearch(searchDir: string, pattern: string, opts?: {
31
+ maxResults?: number;
32
+ include?: string;
33
+ contextLines?: number;
34
+ fromEnd?: boolean;
35
+ }): RipgrepResult[];
36
+ /**
37
+ * Full-text search over session messages using BM25 ranking via MiniSearch.
38
+ *
39
+ * Builds an in-memory index from readSessionHistory() and caches it per session.
40
+ * The cache is invalidated when the sessionId changes or invalidateBM25Cache() is called.
41
+ *
42
+ * @param sessionId - The session UUID
43
+ * @param workingDir - The project working directory
44
+ * @param query - The search query (natural language or keywords)
45
+ * @param opts.maxResults - Maximum number of results (default: 20)
46
+ * @returns Ranked array of matches with content, score, and optional type
47
+ */
48
+ export declare function bm25Search(sessionId: string, workingDir: string, query: string, opts?: {
49
+ maxResults?: number;
50
+ }): Promise<BM25Result[]>;
51
+ /**
52
+ * Resolve the JSONL directory for a session.
53
+ * Uses the same slug resolution as session-access.ts.
54
+ *
55
+ * @param sessionId - The session UUID
56
+ * @param workingDir - The project working directory
57
+ * @returns The directory path containing JSONL files, or null if it doesn't exist
58
+ */
59
+ export declare function resolveJsonlDir(sessionId: string, workingDir: string): string | null;
60
+ /**
61
+ * Clear the BM25 cache. Call this when session data has changed
62
+ * and you want the next bm25Search() call to rebuild the index.
63
+ *
64
+ * @param sessionId - The session ID to invalidate (currently clears any cached session)
65
+ */
66
+ export declare function invalidateBM25Cache(sessionId: string): void;
@@ -0,0 +1,274 @@
1
+ /**
2
+ * jsonl-search.ts — Ripgrep search and BM25 search utilities for JSONL session files
3
+ *
4
+ * Provides two search strategies over Claude Agent SDK session data:
5
+ * 1. ripgrepSearch — fast regex search via rg (or grep fallback) across files
6
+ * 2. bm25Search — in-memory full-text search over parsed session messages
7
+ *
8
+ * Also provides resolveJsonlDir() for locating the JSONL project directory
9
+ * and invalidateBM25Cache() for cache management.
10
+ */
11
+ import { spawnSync } from 'child_process';
12
+ import { existsSync } from 'fs';
13
+ import { homedir } from 'os';
14
+ import { join } from 'path';
15
+ import { readSessionHistory } from './session-access.js';
16
+ // ============================================================
17
+ // BM25 CACHE
18
+ // ============================================================
19
+ let bm25Cache = null;
20
+ // ============================================================
21
+ // INTERNAL HELPERS
22
+ // ============================================================
23
+ /** Resolve the claude config directory (same logic as session-access.ts) */
24
+ function resolveClaudeDir() {
25
+ return process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
26
+ }
27
+ /** Convert a project path to a slug (same logic as session-access.ts) */
28
+ function projectPathToSlug(projectPath) {
29
+ return projectPath.replace(/\//g, '-');
30
+ }
31
+ /** Find the rg binary — uses @vscode/ripgrep npm package (bundled binary) */
32
+ function findRgBinary() {
33
+ try {
34
+ // @vscode/ripgrep bundles the rg binary — this is our own dependency
35
+ const { rgPath } = require('@vscode/ripgrep');
36
+ if (rgPath && existsSync(rgPath))
37
+ return rgPath;
38
+ }
39
+ catch { }
40
+ return null;
41
+ }
42
+ /** Validate a search pattern — reject empty and shell-injection characters */
43
+ function validatePattern(pattern) {
44
+ if (!pattern || pattern.trim().length === 0)
45
+ return false;
46
+ // Block shell-injection characters
47
+ if (/[\$`|;&]/.test(pattern))
48
+ return false;
49
+ return true;
50
+ }
51
+ // ============================================================
52
+ // PUBLIC API
53
+ // ============================================================
54
+ /**
55
+ * Search files using ripgrep (rg) with fallback to grep.
56
+ *
57
+ * @param searchDir - Directory to search in
58
+ * @param pattern - Regex pattern to search for (validated for safety)
59
+ * @param opts.maxResults - Maximum number of results (default: 100)
60
+ * @param opts.include - Glob pattern for file inclusion (e.g., "*.jsonl")
61
+ * @returns Array of matches with line number, file path, and content
62
+ */
63
+ export function ripgrepSearch(searchDir, pattern, opts) {
64
+ if (!validatePattern(pattern))
65
+ return [];
66
+ if (!existsSync(searchDir))
67
+ return [];
68
+ const maxResults = opts?.maxResults ?? 100;
69
+ const fromEnd = opts?.fromEnd ?? false; // caller decides; pipeline uses true for main, false for sub-agents
70
+ const rgBinary = findRgBinary();
71
+ if (rgBinary) {
72
+ if (fromEnd) {
73
+ // Search ALL matches (no --max-count), take last N — gets most recent
74
+ const allResults = runRipgrep(rgBinary, searchDir, pattern, 0, opts?.include, opts?.contextLines);
75
+ return allResults.slice(-maxResults);
76
+ }
77
+ return runRipgrep(rgBinary, searchDir, pattern, maxResults, opts?.include, opts?.contextLines);
78
+ }
79
+ // Fallback to grep
80
+ return runGrepFallback(searchDir, pattern, maxResults, opts?.include);
81
+ }
82
+ /** Run ripgrep and parse results */
83
+ function runRipgrep(rgPath, searchDir, pattern, maxResults, include, contextLines) {
84
+ const args = [
85
+ '--no-heading',
86
+ '--with-filename',
87
+ '--line-number',
88
+ '-i',
89
+ ];
90
+ // maxResults 0 = no cap (search all, caller handles slicing)
91
+ if (maxResults > 0) {
92
+ args.push('--max-count', String(maxResults));
93
+ }
94
+ // Add context lines for surrounding message context (default: 3 lines each side)
95
+ const ctx = contextLines ?? 3;
96
+ if (ctx > 0) {
97
+ args.push('-C', String(ctx));
98
+ }
99
+ if (include) {
100
+ args.push('--glob', include);
101
+ }
102
+ args.push(pattern, searchDir);
103
+ const result = spawnSync(rgPath, args, {
104
+ maxBuffer: 2 * 1024 * 1024, // 2MB
105
+ timeout: 5000,
106
+ encoding: 'utf-8',
107
+ });
108
+ // Exit code 1 = no matches (not an error)
109
+ if (result.status === 1)
110
+ return [];
111
+ // Any other non-zero exit is an error
112
+ if (result.status !== 0 && result.status !== null)
113
+ return [];
114
+ return parseSearchOutput(result.stdout || '');
115
+ }
116
+ /** Fallback to grep if rg is not found */
117
+ function runGrepFallback(searchDir, pattern, maxResults, include) {
118
+ const args = [
119
+ '-r',
120
+ '-n',
121
+ '-i',
122
+ '-m', String(maxResults),
123
+ ];
124
+ if (include) {
125
+ args.push('--include', include);
126
+ }
127
+ args.push(pattern, searchDir);
128
+ const result = spawnSync('grep', args, {
129
+ maxBuffer: 2 * 1024 * 1024, // 2MB
130
+ timeout: 5000,
131
+ encoding: 'utf-8',
132
+ });
133
+ // Exit code 1 = no matches
134
+ if (result.status === 1)
135
+ return [];
136
+ if (result.status !== 0 && result.status !== null)
137
+ return [];
138
+ return parseSearchOutput(result.stdout || '');
139
+ }
140
+ /** Parse output lines in the format: filePath:lineNumber:content */
141
+ function parseSearchOutput(stdout) {
142
+ if (!stdout || !stdout.trim())
143
+ return [];
144
+ const results = [];
145
+ const lines = stdout.trim().split('\n');
146
+ for (const line of lines) {
147
+ // Format: filePath:lineNumber:content
148
+ const match = line.match(/^(.+?):(\d+):(.*)$/);
149
+ if (match) {
150
+ results.push({
151
+ filePath: match[1],
152
+ lineNumber: parseInt(match[2], 10),
153
+ content: match[3],
154
+ });
155
+ }
156
+ }
157
+ return results;
158
+ }
159
+ /**
160
+ * Full-text search over session messages using BM25 ranking via MiniSearch.
161
+ *
162
+ * Builds an in-memory index from readSessionHistory() and caches it per session.
163
+ * The cache is invalidated when the sessionId changes or invalidateBM25Cache() is called.
164
+ *
165
+ * @param sessionId - The session UUID
166
+ * @param workingDir - The project working directory
167
+ * @param query - The search query (natural language or keywords)
168
+ * @param opts.maxResults - Maximum number of results (default: 20)
169
+ * @returns Ranked array of matches with content, score, and optional type
170
+ */
171
+ export async function bm25Search(sessionId, workingDir, query, opts) {
172
+ if (!query || !query.trim())
173
+ return [];
174
+ const maxResults = opts?.maxResults ?? 20;
175
+ // Rebuild index if cache is stale or for a different session
176
+ if (!bm25Cache || bm25Cache.sessionId !== sessionId) {
177
+ const { default: MiniSearch } = await import('minisearch');
178
+ const miniSearch = new MiniSearch({
179
+ fields: ['text'],
180
+ storeFields: ['text', 'type', 'toolName', 'timestamp'],
181
+ searchOptions: {
182
+ fuzzy: 0.2,
183
+ prefix: true,
184
+ boost: { text: 2 },
185
+ },
186
+ });
187
+ // Load recent session messages only — cap at 500 to keep index build fast
188
+ // For a 4400-message session, loading all would take 10+ seconds
189
+ const messages = readSessionHistory(sessionId, workingDir, { lastN: 500 });
190
+ // Build documents — filter to entries with meaningful text
191
+ const docs = [];
192
+ for (let i = 0; i < messages.length; i++) {
193
+ const msg = messages[i];
194
+ const text = extractMessageText(msg);
195
+ if (text && text.length > 10) {
196
+ docs.push({
197
+ id: i,
198
+ text: text.substring(0, 2000),
199
+ type: msg.type,
200
+ toolName: msg.toolName,
201
+ timestamp: msg.timestamp,
202
+ });
203
+ }
204
+ }
205
+ miniSearch.addAll(docs);
206
+ bm25Cache = {
207
+ index: miniSearch,
208
+ sessionId,
209
+ builtAt: Date.now(),
210
+ };
211
+ }
212
+ // Search the index
213
+ const results = bm25Cache.index.search(query, { limit: maxResults });
214
+ return results.map((r) => ({
215
+ content: r.text || '',
216
+ score: r.score,
217
+ type: r.type,
218
+ }));
219
+ }
220
+ /** Extract searchable text from a SessionMessage */
221
+ function extractMessageText(msg) {
222
+ switch (msg.type) {
223
+ case 'user':
224
+ case 'assistant':
225
+ return msg.text;
226
+ case 'tool_use':
227
+ // Include tool name + any associated text + stringified input
228
+ const parts = [];
229
+ if (msg.toolName)
230
+ parts.push(msg.toolName);
231
+ if (msg.text)
232
+ parts.push(msg.text);
233
+ if (msg.toolInput) {
234
+ try {
235
+ parts.push(JSON.stringify(msg.toolInput));
236
+ }
237
+ catch {
238
+ // skip
239
+ }
240
+ }
241
+ return parts.join(' ') || undefined;
242
+ case 'tool_result':
243
+ return msg.toolResultContent;
244
+ default:
245
+ return undefined;
246
+ }
247
+ }
248
+ /**
249
+ * Resolve the JSONL directory for a session.
250
+ * Uses the same slug resolution as session-access.ts.
251
+ *
252
+ * @param sessionId - The session UUID
253
+ * @param workingDir - The project working directory
254
+ * @returns The directory path containing JSONL files, or null if it doesn't exist
255
+ */
256
+ export function resolveJsonlDir(sessionId, workingDir) {
257
+ const claudeDir = resolveClaudeDir();
258
+ const slug = projectPathToSlug(workingDir);
259
+ const projectsDir = join(claudeDir, 'projects', slug);
260
+ if (!existsSync(projectsDir))
261
+ return null;
262
+ return projectsDir;
263
+ }
264
+ /**
265
+ * Clear the BM25 cache. Call this when session data has changed
266
+ * and you want the next bm25Search() call to rebuild the index.
267
+ *
268
+ * @param sessionId - The session ID to invalidate (currently clears any cached session)
269
+ */
270
+ export function invalidateBM25Cache(sessionId) {
271
+ if (bm25Cache && bm25Cache.sessionId === sessionId) {
272
+ bm25Cache = null;
273
+ }
274
+ }
File without changes