osborn 0.5.2 → 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 +346 -79
  8. package/dist/config.d.ts +6 -2
  9. package/dist/config.js +6 -1
  10. package/dist/fast-brain.d.ts +124 -12
  11. package/dist/fast-brain.js +1361 -96
  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 +889 -394
  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 -8
  28. package/dist/prompts.js +1990 -374
  29. package/dist/session-access.d.ts +60 -2
  30. package/dist/session-access.js +172 -2
  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 +18 -11
@@ -259,6 +259,30 @@ export interface SessionTranscripts {
259
259
  * @param opts.claudeDir - Override the claude directory path
260
260
  */
261
261
  export declare function getSessionPaths(sessionId: string, projectDir: string, opts?: SessionAccessOptions): SessionPaths;
262
+ export interface SubAgentInfo {
263
+ agentId: string;
264
+ description: string;
265
+ status: string;
266
+ agentFile: string;
267
+ agentFileExists: boolean;
268
+ totalTokens: number;
269
+ totalToolUseCount: number;
270
+ durationMs: number;
271
+ }
272
+ /**
273
+ * Find all sub-agents spawned by a session.
274
+ * Extracts agentId from toolUseResult fields in the main JSONL,
275
+ * then checks for corresponding agent-{id}.jsonl files at the project level.
276
+ *
277
+ * Sub-agent files live at: ~/.claude/projects/{slug}/agent-{id}.jsonl
278
+ * (NOT inside the session subdirectory — that's only for the Osborn agent SDK path)
279
+ */
280
+ export declare function getSessionSubAgents(sessionId: string, projectDir: string, opts?: SessionAccessOptions): SubAgentInfo[];
281
+ /**
282
+ * Get all searchable file paths for a session — main JSONL + all sub-agent JSOLs.
283
+ * This is what the pipeline fast brain should ripgrep across.
284
+ */
285
+ export declare function getSessionSearchPaths(sessionId: string, projectDir: string, opts?: SessionAccessOptions): string[];
262
286
  /**
263
287
  * Read raw JSON objects from a JSONL file — no parsing into SessionMessage.
264
288
  * Returns the actual JSON as-is so you can inspect the full object shapes.
@@ -318,9 +342,12 @@ export declare function getSessionPlan(sessionId: string, projectDir: string, op
318
342
  * Get recent tool use/result pairs from a session.
319
343
  * Returns matched pairs of (tool_use → tool_result) in chronological order.
320
344
  *
321
- * @param lastN - Number of recent pairs to return (default: 10)
345
+ * @param lastN - Number of recent pairs to return (default: 10, 0 = all)
346
+ * @param opts.toolNameFilter - Optional: only return results from these tool names (e.g., ['Read', 'WebSearch'])
322
347
  */
323
- export declare function getRecentToolResults(sessionId: string, projectDir: string, lastN?: number, opts?: SessionAccessOptions): ToolResultEntry[];
348
+ export declare function getRecentToolResults(sessionId: string, projectDir: string, lastN?: number, opts?: SessionAccessOptions & {
349
+ toolNameFilter?: string[];
350
+ }): ToolResultEntry[];
324
351
  /**
325
352
  * Watch a session JSONL file for new entries.
326
353
  * Calls back with each new parsed entry as it's appended.
@@ -347,6 +374,37 @@ export declare function getConversationText(sessionId: string, projectDir: strin
347
374
  role: string;
348
375
  text: string;
349
376
  }[];
377
+ /**
378
+ * Search the session JSONL for entries matching a keyword (case-insensitive).
379
+ * Searches across text, toolResultContent, toolName, and toolInput.
380
+ * Returns matching entries with 500-char excerpts around the match.
381
+ *
382
+ * @param keyword - The search keyword (case-insensitive)
383
+ * @param opts.maxResults - Maximum number of results to return (default: 20)
384
+ */
385
+ export declare function searchSessionJsonl(sessionId: string, projectDir: string, keyword: string, opts?: SessionAccessOptions & {
386
+ maxResults?: number;
387
+ }): {
388
+ type: string;
389
+ text: string;
390
+ timestamp?: string;
391
+ }[];
392
+ /**
393
+ * Get session stats: message counts, tool usage breakdown, data sizes.
394
+ * Helps the fast brain decide how much data to read and which tools to query.
395
+ */
396
+ export declare function getSessionStats(sessionId: string, projectDir: string, opts?: SessionAccessOptions): {
397
+ totalMessages: number;
398
+ userMessages: number;
399
+ assistantMessages: number;
400
+ toolUseCount: number;
401
+ toolResultCount: number;
402
+ toolBreakdown: Record<string, number>;
403
+ subagentCount: number;
404
+ fileSizeBytes: number;
405
+ firstTimestamp?: string;
406
+ lastTimestamp?: string;
407
+ } | null;
350
408
  /**
351
409
  * Get a quick summary of a session: slug, message count, timestamps, tools used.
352
410
  */
@@ -228,6 +228,80 @@ export function getSessionPaths(sessionId, projectDir, opts) {
228
228
  exists: existsSync(conversationPath),
229
229
  };
230
230
  }
231
+ /**
232
+ * Find all sub-agents spawned by a session.
233
+ * Extracts agentId from toolUseResult fields in the main JSONL,
234
+ * then checks for corresponding agent-{id}.jsonl files at the project level.
235
+ *
236
+ * Sub-agent files live at: ~/.claude/projects/{slug}/agent-{id}.jsonl
237
+ * (NOT inside the session subdirectory — that's only for the Osborn agent SDK path)
238
+ */
239
+ export function getSessionSubAgents(sessionId, projectDir, opts) {
240
+ const claudeDir = resolveClaudeDir(opts);
241
+ const slug = projectPathToSlug(projectDir);
242
+ const projectsDir = join(claudeDir, 'projects', slug);
243
+ const conversationPath = join(projectsDir, `${sessionId}.jsonl`);
244
+ if (!existsSync(conversationPath))
245
+ return [];
246
+ const agents = [];
247
+ const seenIds = new Set();
248
+ try {
249
+ const content = readFileSync(conversationPath, 'utf-8');
250
+ for (const line of content.split('\n')) {
251
+ if (!line.trim())
252
+ continue;
253
+ try {
254
+ const obj = JSON.parse(line);
255
+ const tur = obj.toolUseResult;
256
+ if (tur && typeof tur === 'object' && tur.agentId) {
257
+ const id = tur.agentId;
258
+ if (seenIds.has(id))
259
+ continue;
260
+ seenIds.add(id);
261
+ const agentFile = join(projectsDir, `agent-${id}.jsonl`);
262
+ agents.push({
263
+ agentId: id,
264
+ description: (tur.prompt || '').substring(0, 200),
265
+ status: tur.status || '',
266
+ agentFile,
267
+ agentFileExists: existsSync(agentFile),
268
+ totalTokens: tur.totalTokens || 0,
269
+ totalToolUseCount: tur.totalToolUseCount || 0,
270
+ durationMs: tur.totalDurationMs || 0,
271
+ });
272
+ }
273
+ }
274
+ catch { }
275
+ }
276
+ }
277
+ catch { }
278
+ return agents;
279
+ }
280
+ /**
281
+ * Get all searchable file paths for a session — main JSONL + all sub-agent JSOLs.
282
+ * This is what the pipeline fast brain should ripgrep across.
283
+ */
284
+ export function getSessionSearchPaths(sessionId, projectDir, opts) {
285
+ const paths = getSessionPaths(sessionId, projectDir, opts);
286
+ const files = [];
287
+ // Main conversation
288
+ if (existsSync(paths.conversation)) {
289
+ files.push(paths.conversation);
290
+ }
291
+ // Sub-agents from session subdirectory (Osborn agent SDK path)
292
+ for (const f of paths.subagents) {
293
+ if (existsSync(f))
294
+ files.push(f);
295
+ }
296
+ // Sub-agents at project level (Claude Code CLI path)
297
+ const agents = getSessionSubAgents(sessionId, projectDir, opts);
298
+ for (const a of agents) {
299
+ if (a.agentFileExists && !files.includes(a.agentFile)) {
300
+ files.push(a.agentFile);
301
+ }
302
+ }
303
+ return files;
304
+ }
231
305
  // ============================================================
232
306
  // JSONL PARSING
233
307
  // ============================================================
@@ -530,13 +604,15 @@ export function getSessionPlan(sessionId, projectDir, opts) {
530
604
  * Get recent tool use/result pairs from a session.
531
605
  * Returns matched pairs of (tool_use → tool_result) in chronological order.
532
606
  *
533
- * @param lastN - Number of recent pairs to return (default: 10)
607
+ * @param lastN - Number of recent pairs to return (default: 10, 0 = all)
608
+ * @param opts.toolNameFilter - Optional: only return results from these tool names (e.g., ['Read', 'WebSearch'])
534
609
  */
535
610
  export function getRecentToolResults(sessionId, projectDir, lastN = 10, opts) {
536
611
  const messages = readSessionHistory(sessionId, projectDir, { claudeDir: opts?.claudeDir });
537
612
  // Build a map of tool_use by tool ID
538
613
  const toolUseMap = new Map();
539
614
  const results = [];
615
+ const toolFilter = opts?.toolNameFilter?.map(t => t.toLowerCase());
540
616
  for (const msg of messages) {
541
617
  if (msg.type === 'tool_use' && msg.toolId) {
542
618
  toolUseMap.set(msg.toolId, msg);
@@ -544,6 +620,10 @@ export function getRecentToolResults(sessionId, projectDir, lastN = 10, opts) {
544
620
  if (msg.type === 'tool_result' && msg.toolUseId) {
545
621
  const toolUse = toolUseMap.get(msg.toolUseId);
546
622
  if (toolUse) {
623
+ // Apply tool name filter if provided
624
+ if (toolFilter && !toolFilter.includes(toolUse.toolName.toLowerCase())) {
625
+ continue;
626
+ }
547
627
  results.push({
548
628
  toolName: toolUse.toolName,
549
629
  toolId: toolUse.toolId,
@@ -555,7 +635,7 @@ export function getRecentToolResults(sessionId, projectDir, lastN = 10, opts) {
555
635
  }
556
636
  }
557
637
  }
558
- return results.slice(-lastN);
638
+ return lastN > 0 ? results.slice(-lastN) : results;
559
639
  }
560
640
  /**
561
641
  * Watch a session JSONL file for new entries.
@@ -648,6 +728,96 @@ export function getConversationText(sessionId, projectDir, lastN = 30, maxCharsP
648
728
  }));
649
729
  return exchanges.slice(-lastN);
650
730
  }
731
+ /**
732
+ * Search the session JSONL for entries matching a keyword (case-insensitive).
733
+ * Searches across text, toolResultContent, toolName, and toolInput.
734
+ * Returns matching entries with 500-char excerpts around the match.
735
+ *
736
+ * @param keyword - The search keyword (case-insensitive)
737
+ * @param opts.maxResults - Maximum number of results to return (default: 20)
738
+ */
739
+ export function searchSessionJsonl(sessionId, projectDir, keyword, opts) {
740
+ const messages = readSessionHistory(sessionId, projectDir, { claudeDir: opts?.claudeDir });
741
+ const maxResults = opts?.maxResults || 20;
742
+ const results = [];
743
+ const lowerKeyword = keyword.toLowerCase();
744
+ for (const msg of messages) {
745
+ if (results.length >= maxResults)
746
+ break;
747
+ // Search in text content
748
+ if (msg.text && msg.text.toLowerCase().includes(lowerKeyword)) {
749
+ const idx = msg.text.toLowerCase().indexOf(lowerKeyword);
750
+ const start = Math.max(0, idx - 100);
751
+ const end = Math.min(msg.text.length, idx + keyword.length + 400);
752
+ const excerpt = (start > 0 ? '...' : '') + msg.text.substring(start, end) + (end < msg.text.length ? '...' : '');
753
+ results.push({ type: msg.type, text: excerpt, timestamp: msg.timestamp });
754
+ continue;
755
+ }
756
+ // Search in tool result content
757
+ if (msg.toolResultContent && msg.toolResultContent.toLowerCase().includes(lowerKeyword)) {
758
+ const idx = msg.toolResultContent.toLowerCase().indexOf(lowerKeyword);
759
+ const start = Math.max(0, idx - 100);
760
+ const end = Math.min(msg.toolResultContent.length, idx + keyword.length + 400);
761
+ const excerpt = `[tool_result] ` + (start > 0 ? '...' : '') + msg.toolResultContent.substring(start, end) + (end < msg.toolResultContent.length ? '...' : '');
762
+ results.push({ type: msg.type, text: excerpt, timestamp: msg.timestamp });
763
+ continue;
764
+ }
765
+ // Search in tool name
766
+ if (msg.toolName && msg.toolName.toLowerCase().includes(lowerKeyword)) {
767
+ const inputPreview = msg.toolInput ? JSON.stringify(msg.toolInput).substring(0, 200) : '';
768
+ results.push({ type: msg.type, text: `[${msg.toolName}: ${inputPreview}]`, timestamp: msg.timestamp });
769
+ continue;
770
+ }
771
+ // Search in tool input
772
+ if (msg.toolInput) {
773
+ const inputStr = JSON.stringify(msg.toolInput);
774
+ if (inputStr.toLowerCase().includes(lowerKeyword)) {
775
+ results.push({ type: msg.type, text: `[${msg.toolName || 'tool'}: ${inputStr.substring(0, 500)}]`, timestamp: msg.timestamp });
776
+ }
777
+ }
778
+ }
779
+ return results;
780
+ }
781
+ /**
782
+ * Get session stats: message counts, tool usage breakdown, data sizes.
783
+ * Helps the fast brain decide how much data to read and which tools to query.
784
+ */
785
+ export function getSessionStats(sessionId, projectDir, opts) {
786
+ const paths = getSessionPaths(sessionId, projectDir, opts);
787
+ if (!paths.exists)
788
+ return null;
789
+ const messages = readJsonl(paths.conversation);
790
+ const toolBreakdown = {};
791
+ let userMessages = 0;
792
+ let assistantMessages = 0;
793
+ let toolUseCount = 0;
794
+ let toolResultCount = 0;
795
+ for (const msg of messages) {
796
+ if (msg.type === 'user')
797
+ userMessages++;
798
+ else if (msg.type === 'assistant')
799
+ assistantMessages++;
800
+ else if (msg.type === 'tool_use') {
801
+ toolUseCount++;
802
+ const name = msg.toolName || 'unknown';
803
+ toolBreakdown[name] = (toolBreakdown[name] || 0) + 1;
804
+ }
805
+ else if (msg.type === 'tool_result')
806
+ toolResultCount++;
807
+ }
808
+ return {
809
+ totalMessages: messages.length,
810
+ userMessages,
811
+ assistantMessages,
812
+ toolUseCount,
813
+ toolResultCount,
814
+ toolBreakdown,
815
+ subagentCount: paths.subagents.length,
816
+ fileSizeBytes: existsSync(paths.conversation) ? statSync(paths.conversation).size : 0,
817
+ firstTimestamp: messages.find(m => m.timestamp)?.timestamp,
818
+ lastTimestamp: [...messages].reverse().find(m => m.timestamp)?.timestamp,
819
+ };
820
+ }
651
821
  /**
652
822
  * Get a quick summary of a session: slug, message count, timestamps, tools used.
653
823
  */
@@ -0,0 +1,87 @@
1
+ /**
2
+ * summary-index.ts — Builds a compact searchable summary of Claude JSONL sessions
3
+ *
4
+ * Instead of ripgrepping 80MB raw JSONL, we extract one-line summaries per message
5
+ * into a ~1MB plain text file. Ripgrep searches this in <5ms.
6
+ *
7
+ * Format: {lineNum}|{timestamp}|{source}|{msgType}|{summary}
8
+ *
9
+ * No LLM calls — pure heuristic extraction:
10
+ * tool_use → tool name + key params (file path, command, query)
11
+ * tool_result → tool name + first 80 chars of output
12
+ * user → raw text (already short from voice)
13
+ * assistant → first 500 chars of text
14
+ *
15
+ * Per-session index stored at: .osborn/sessions/{sessionId}/.index/search-index.txt
16
+ */
17
+ export interface IndexEntry {
18
+ lineNum: number;
19
+ byteOffset: number;
20
+ timestamp: string;
21
+ source: string;
22
+ msgType: string;
23
+ summary: string;
24
+ }
25
+ export interface SummaryIndexState {
26
+ indexPath: string;
27
+ metaPath: string;
28
+ main: {
29
+ jsonlPath: string;
30
+ byteOffset: number;
31
+ lineCount: number;
32
+ indexLineCount: number;
33
+ };
34
+ subAgents: Map<string, {
35
+ jsonlPath: string;
36
+ byteOffset: number;
37
+ lineCount: number;
38
+ indexLineCount: number;
39
+ }>;
40
+ toolUseIdMap: Map<string, string>;
41
+ }
42
+ export interface SummaryIndexMeta {
43
+ version: 1;
44
+ sessionId: string;
45
+ createdAt: string;
46
+ updatedAt: string;
47
+ main: {
48
+ jsonlPath: string;
49
+ byteOffset: number;
50
+ lineCount: number;
51
+ indexLineCount: number;
52
+ };
53
+ subAgents: Record<string, {
54
+ jsonlPath: string;
55
+ byteOffset: number;
56
+ lineCount: number;
57
+ indexLineCount: number;
58
+ }>;
59
+ toolUseIdMap: Record<string, string>;
60
+ }
61
+ export interface IndexWatcher {
62
+ stop(): void;
63
+ state: SummaryIndexState;
64
+ }
65
+ /**
66
+ * Build or resume a summary index for a session.
67
+ * Reads main JSONL + all sub-agent JSONLs, extracts summaries.
68
+ * If an existing index with metadata exists, resumes from last byte offset.
69
+ */
70
+ export declare function buildSummaryIndex(sessionId: string, workingDir: string, sessionBaseDir: string, onProgress?: (msg: string) => void): SummaryIndexState;
71
+ /**
72
+ * Poll-based incremental index updater (10s interval).
73
+ * Checks main JSONL + sub-agents for new content, indexes in one batch.
74
+ * No fs.watch — avoids race conditions with concurrent writers.
75
+ */
76
+ export declare function startIndexWatcher(sessionId: string, workingDir: string, sessionBaseDir: string, state: SummaryIndexState): IndexWatcher;
77
+ export declare function getIndexPath(sessionId: string, sessionBaseDir: string): string | null;
78
+ /**
79
+ * Given index search results (with byte offsets + source tags),
80
+ * read FULL content from raw JSONL via targeted reads — 0.5ms per result.
81
+ * No readFileSync of the whole file. Strips JSON noise, returns clean text.
82
+ */
83
+ export declare function readFullContent(results: {
84
+ lineNum: number;
85
+ byteOffset: number;
86
+ source: string;
87
+ }[], sessionId: string, workingDir: string, sessionBaseDir: string, maxCharsPerResult?: number): string[];