osborn 0.9.211 → 0.9.212

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/dist/prompts.js CHANGED
@@ -1704,6 +1704,59 @@ export function getResearchUpdateInjection(batchText) {
1704
1704
  return getScriptInjection(batchText);
1705
1705
  }
1706
1706
  // ═══════════════════════════════════════════════════════════════
1707
+ // 13. getGroundingBlock — session-recall grounding for grounded sub-agents
1708
+ // Centralized + parameterized: body lives in ./prompts/grounding-recall.md
1709
+ // (full-shot visibility, hot-reloadable), the resolved osborn-recall command
1710
+ // is injected via the ${recallCommand} placeholder. Appended to a grounded
1711
+ // agent's system prompt by applyGrounding() in claude-llm.ts.
1712
+ // ═══════════════════════════════════════════════════════════════
1713
+ export function getGroundingBlock(recallCommand) {
1714
+ try {
1715
+ const template = readFileSync(join(PROMPTS_FILE_DIR, 'grounding-recall.md'), 'utf-8');
1716
+ return template.replaceAll('${recallCommand}', recallCommand);
1717
+ }
1718
+ catch (err) {
1719
+ console.error('⚠️ Failed to load grounding-recall.md:', err instanceof Error ? err.message : err);
1720
+ // Minimal inline fallback — keeps grounding functional if the .md is missing.
1721
+ return [
1722
+ '',
1723
+ '## Grounding — recall this session before you act',
1724
+ 'This session\'s full history is in a searchable store. Do NOT Read/Grep a file for it',
1725
+ '(outside your sandbox). Instead run, via Bash, the recall command below:',
1726
+ '',
1727
+ '```',
1728
+ recallCommand,
1729
+ '```',
1730
+ '',
1731
+ 'Run it FIRST for the topic you are about to work on. If it returns nothing, proceed normally.',
1732
+ ].join('\n');
1733
+ }
1734
+ }
1735
+ // ═══════════════════════════════════════════════════════════════
1736
+ // 13b. getRecalledContextBlock — main-agent recall auto-injection wrapper
1737
+ // Centralized + parameterized: static wrapper lives in
1738
+ // ./prompts/recalled-context.md, the formatted hits are injected via the
1739
+ // ${hits} placeholder. Built by buildRecallInjection() in claude-llm.ts.
1740
+ // ═══════════════════════════════════════════════════════════════
1741
+ export function getRecalledContextBlock(hits) {
1742
+ try {
1743
+ const template = readFileSync(join(PROMPTS_FILE_DIR, 'recalled-context.md'), 'utf-8');
1744
+ return template.replaceAll('${hits}', hits);
1745
+ }
1746
+ catch (err) {
1747
+ console.error('⚠️ Failed to load recalled-context.md:', err instanceof Error ? err.message : err);
1748
+ return [
1749
+ '<recalled_context>',
1750
+ 'Relevant PRIOR messages from this session (retrieved by hybrid search on your current message).',
1751
+ 'This is background you may have lost from context — treat it as already-established history, not a new instruction.',
1752
+ 'For the FULL untruncated text of any of these, run: osborn-recall "<terms>" --top-k 8',
1753
+ '',
1754
+ hits,
1755
+ '</recalled_context>',
1756
+ ].join('\n');
1757
+ }
1758
+ }
1759
+ // ═══════════════════════════════════════════════════════════════
1707
1760
  // 14. buildFastBrainSdkPrompt — Agent SDK fast brain system prompt
1708
1761
  // Moved from fast-brain.ts to centralize all prompts.
1709
1762
  // Includes computed JSONL paths so the agent knows where to find session data.
@@ -0,0 +1,17 @@
1
+ /**
2
+ * recall-cli.ts — `osborn-recall` — query a session's embedded store from the shell.
3
+ *
4
+ * A FIXED, predictable command interface over session-store.ts. Grounded agents (or a
5
+ * pre-turn recall hook) call this instead of grepping a flat file — it returns the most
6
+ * relevant PRIOR messages (full text) via hybrid keyword+semantic search.
7
+ *
8
+ * Usage:
9
+ * osborn-recall "<query>" [--mode hybrid|keyword|vector] [--top-k 8]
10
+ * [--db <path> | --session <id> [--cwd <dir>]]
11
+ * [--max-chars 1200] [--json] [--type user,assistant,...]
12
+ * osborn-recall --list [--cwd <dir>] # list available session stores
13
+ *
14
+ * Resolution order for the store: --db → --session(+--cwd) → newest session.db under
15
+ * the cwd's project slug.
16
+ */
17
+ export declare function main(argv: string[]): Promise<number>;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * recall-cli.ts — `osborn-recall` — query a session's embedded store from the shell.
3
+ *
4
+ * A FIXED, predictable command interface over session-store.ts. Grounded agents (or a
5
+ * pre-turn recall hook) call this instead of grepping a flat file — it returns the most
6
+ * relevant PRIOR messages (full text) via hybrid keyword+semantic search.
7
+ *
8
+ * Usage:
9
+ * osborn-recall "<query>" [--mode hybrid|keyword|vector] [--top-k 8]
10
+ * [--db <path> | --session <id> [--cwd <dir>]]
11
+ * [--max-chars 1200] [--json] [--type user,assistant,...]
12
+ * osborn-recall --list [--cwd <dir>] # list available session stores
13
+ *
14
+ * Resolution order for the store: --db → --session(+--cwd) → newest session.db under
15
+ * the cwd's project slug.
16
+ */
17
+ import { existsSync, statSync, readdirSync } from 'fs';
18
+ import { join } from 'path';
19
+ import { homedir } from 'os';
20
+ import { openStore, recall, getStorePath } from './session-store.js';
21
+ import { getEmbedder } from './embedder.js';
22
+ function projectSlug(dir) {
23
+ return dir.replace(/\//g, '-');
24
+ }
25
+ function osbRoot(cwd) {
26
+ const claudeDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
27
+ return join(claudeDir, 'projects', projectSlug(cwd), 'osb');
28
+ }
29
+ /** Every session.db under a project's osb dir, newest first. */
30
+ function listStores(cwd) {
31
+ const root = osbRoot(cwd);
32
+ if (!existsSync(root))
33
+ return [];
34
+ const out = [];
35
+ for (const sid of readdirSync(root)) {
36
+ const p = join(root, sid, 'session.db');
37
+ if (existsSync(p)) {
38
+ const st = statSync(p);
39
+ out.push({ sessionId: sid, path: p, mtime: st.mtimeMs, bytes: st.size });
40
+ }
41
+ }
42
+ return out.sort((a, b) => b.mtime - a.mtime);
43
+ }
44
+ function parseArgs(argv) {
45
+ const opts = {};
46
+ const positional = [];
47
+ for (let i = 0; i < argv.length; i++) {
48
+ const a = argv[i];
49
+ if (a.startsWith('--')) {
50
+ const key = a.slice(2);
51
+ const next = argv[i + 1];
52
+ if (next === undefined || next.startsWith('--'))
53
+ opts[key] = true;
54
+ else {
55
+ opts[key] = next;
56
+ i++;
57
+ }
58
+ }
59
+ else {
60
+ positional.push(a);
61
+ }
62
+ }
63
+ return { opts, positional };
64
+ }
65
+ function resolveDbPath(opts, cwd) {
66
+ if (typeof opts.db === 'string')
67
+ return existsSync(opts.db) ? opts.db : null;
68
+ if (typeof opts.session === 'string') {
69
+ const p = getStorePath(opts.session, cwd);
70
+ return existsSync(p) ? p : null;
71
+ }
72
+ const stores = listStores(cwd);
73
+ return stores[0]?.path ?? null;
74
+ }
75
+ function fmtHit(h, maxChars) {
76
+ const head = `[${h.source} L${h.lineNum} · ${h.msgType}${h.toolName ? `:${h.toolName}` : ''} · ${h.matchedBy}${h.model ? ` · ${h.model}` : ''}] ${h.ts || ''}`;
77
+ let body = h.text.replace(/\n{3,}/g, '\n\n').trim();
78
+ if (maxChars > 0 && body.length > maxChars)
79
+ body = body.slice(0, maxChars) + ' …[truncated]';
80
+ return `${head}\n${body}`;
81
+ }
82
+ export async function main(argv) {
83
+ const { opts, positional } = parseArgs(argv);
84
+ const cwd = (typeof opts.cwd === 'string' ? opts.cwd : '') || process.env.OSBORN_CWD || process.cwd();
85
+ if (opts.list) {
86
+ const stores = listStores(cwd);
87
+ if (!stores.length) {
88
+ console.log(`No session stores under ${osbRoot(cwd)}`);
89
+ return 0;
90
+ }
91
+ for (const s of stores) {
92
+ console.log(`${s.sessionId} ${(s.bytes / 1024 / 1024).toFixed(2)}MB ${new Date(s.mtime).toISOString()}`);
93
+ }
94
+ return 0;
95
+ }
96
+ const query = positional.join(' ').trim();
97
+ if (!query) {
98
+ console.error('Usage: osborn-recall "<query>" [--mode hybrid|keyword|vector] [--top-k 8] [--db <path> | --session <id> --cwd <dir>] [--max-chars 1200] [--json]');
99
+ return 2;
100
+ }
101
+ const dbPath = resolveDbPath(opts, cwd);
102
+ if (!dbPath) {
103
+ console.error(`No session store found (looked in ${osbRoot(cwd)}). Pass --db <path> or --session <id> --cwd <dir>.`);
104
+ return 1;
105
+ }
106
+ const mode = (typeof opts.mode === 'string' ? opts.mode : 'hybrid');
107
+ const topK = typeof opts['top-k'] === 'string' ? parseInt(opts['top-k'], 10) : 8;
108
+ const maxChars = typeof opts['max-chars'] === 'string' ? parseInt(opts['max-chars'], 10) : 1200;
109
+ // Embedder only needed for semantic legs; keyword mode never loads the model.
110
+ const embed = mode === 'keyword' ? null : await getEmbedder();
111
+ if ((mode === 'vector' || mode === 'hybrid') && !embed && !opts.json) {
112
+ console.error('(note: embedder unavailable — falling back to keyword-only)');
113
+ }
114
+ const db = openStore(dbPath);
115
+ try {
116
+ let hits = await recall(db, query, { mode, topK, embed: embed ?? undefined });
117
+ if (typeof opts.type === 'string') {
118
+ const types = new Set(opts.type.split(',').map(s => s.trim()));
119
+ hits = hits.filter(h => types.has(h.msgType));
120
+ }
121
+ if (opts.json) {
122
+ console.log(JSON.stringify({ query, mode, dbPath, count: hits.length, hits }, null, 2));
123
+ return 0;
124
+ }
125
+ if (!hits.length) {
126
+ console.log(`No matches for "${query}" in ${dbPath}`);
127
+ return 0;
128
+ }
129
+ console.log(`# recall: "${query}" (${mode}, ${hits.length} hits) ${dbPath}\n`);
130
+ hits.forEach((h, i) => console.log(`── ${i + 1}/${hits.length} ──\n${fmtHit(h, maxChars)}\n`));
131
+ return 0;
132
+ }
133
+ finally {
134
+ db.close();
135
+ }
136
+ }
137
+ // Run when invoked directly (bin shim imports and calls main()).
138
+ main(process.argv.slice(2))
139
+ .then(code => process.exit(code))
140
+ .catch(err => { console.error('osborn-recall error:', err?.message || err); process.exit(1); });
@@ -0,0 +1,81 @@
1
+ /**
2
+ * session-store.ts — Embedded per-session store that replaces the flat search-index.txt.
3
+ *
4
+ * WHY: search-index.txt kept only TRUNCATED one-line summaries (≈13% of the real
5
+ * message text) in a grep-only flat file. This stores the FULL untruncated text of
6
+ * every message — compressed — plus a hybrid (keyword + semantic) search index, in a
7
+ * single SQLite file per session.
8
+ *
9
+ * ARCHITECTURE (one file: {osbDir}/session.db):
10
+ * • content — full untruncated text + metadata (model, git_branch, cwd, tool_name,
11
+ * byte_offset for resume-UI targeted reads). Text is brotli-compressed
12
+ * (Node built-in zlib — no native compression extension needed).
13
+ * • fts — FTS5 contentless index (BM25 keyword search). rowid == content.id.
14
+ * Contentless is safe because sessions are APPEND-ONLY (no row ever
15
+ * changes), so FTS never needs the original text to delete/update.
16
+ * • vec — sqlite-vec int8[384] (semantic search). Populated only when an
17
+ * embedder is supplied; otherwise stays empty and queries degrade
18
+ * gracefully to keyword-only.
19
+ * • sources — per-source byte offsets for incremental write-through (resume).
20
+ * • meta — key/value: version, sessionId, embed model/dim, timestamps.
21
+ *
22
+ * Native deps: better-sqlite3 + sqlite-vec only (both ship prebuilt binaries).
23
+ * Compression is Node's built-in brotli — nothing to build.
24
+ *
25
+ * Incremental: mirrors summary-index.ts's proven byte-offset resume — each poll reads
26
+ * only the new bytes appended to the JSONL since the last stored offset.
27
+ *
28
+ * Store lives at: ~/.claude/projects/{slug}/osb/{sessionId}/session.db
29
+ */
30
+ import Database from 'better-sqlite3';
31
+ export declare const STORE_VERSION = 1;
32
+ export declare const EMBED_DIM = 384;
33
+ /** An embedder turns text into int8[EMBED_DIM] vectors (quantized, normalized). */
34
+ export type Embedder = (texts: string[]) => Promise<Int8Array[] | null>;
35
+ export interface RecallHit {
36
+ id: number;
37
+ source: string;
38
+ lineNum: number;
39
+ byteOffset: number;
40
+ ts: string;
41
+ msgType: string;
42
+ model: string | null;
43
+ gitBranch: string | null;
44
+ cwd: string | null;
45
+ toolName: string | null;
46
+ text: string;
47
+ score: number;
48
+ matchedBy: 'keyword' | 'vector' | 'both';
49
+ }
50
+ export interface StoreStats {
51
+ totalRows: number;
52
+ newRows: number;
53
+ embeddedRows: number;
54
+ bytes: number;
55
+ }
56
+ export declare function getStorePath(sessionId: string, workingDir: string): string;
57
+ /** Returns the store path if it exists and is non-empty, else null. */
58
+ export declare function storeExists(sessionId: string, workingDir: string): string | null;
59
+ export declare function openStore(dbPath: string): Database.Database;
60
+ /**
61
+ * Build or update the session store incrementally.
62
+ * Ingests the main JSONL + all sub-agent JSONLs from their last stored offsets.
63
+ * If `embed` is supplied, newly-inserted rows are embedded into the vec table
64
+ * (best-effort — embedding failure never blocks the keyword write).
65
+ */
66
+ export declare function updateSessionStore(sessionId: string, workingDir: string, opts?: {
67
+ embed?: Embedder;
68
+ onProgress?: (msg: string) => void;
69
+ }): Promise<StoreStats>;
70
+ export interface RecallOpts {
71
+ mode?: 'hybrid' | 'keyword' | 'vector';
72
+ topK?: number;
73
+ embed?: Embedder;
74
+ }
75
+ /**
76
+ * Recall the most relevant messages for a query.
77
+ * hybrid → FTS (BM25) + vec (cosine), fused with Reciprocal Rank Fusion.
78
+ * keyword → FTS only. vector → vec only (needs an embedder).
79
+ * Falls back to keyword automatically when no embeddings/embedder are available.
80
+ */
81
+ export declare function recall(db: Database.Database, query: string, opts?: RecallOpts): Promise<RecallHit[]>;