osborn 0.8.0 → 0.8.1

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/bin/cli.js CHANGED
@@ -41,10 +41,6 @@ Example:
41
41
  process.exit(0)
42
42
  }
43
43
 
44
- // Run the agent using tsx
45
- const agentPath = join(__dirname, '..', 'src', 'index.ts')
46
- const tsxPath = join(__dirname, '..', 'node_modules', '.bin', 'tsx')
47
-
48
44
  // Determine mode (default to 'dev' if no mode specified)
49
45
  let mode = 'dev'
50
46
  if (args.includes('start')) {
@@ -54,11 +50,31 @@ if (args.includes('start')) {
54
50
  args.splice(args.indexOf('dev'), 1)
55
51
  }
56
52
 
57
- const child = spawn(tsxPath, [agentPath, mode, ...args], {
58
- stdio: 'inherit',
59
- cwd: join(__dirname, '..'),
60
- env: process.env,
61
- })
53
+ // Use src/index.ts (dev) if available, otherwise dist/index.js (npm install)
54
+ import { existsSync } from 'fs'
55
+ const srcPath = join(__dirname, '..', 'src', 'index.ts')
56
+ const distPath = join(__dirname, '..', 'dist', 'index.js')
57
+
58
+ let child
59
+ if (existsSync(srcPath)) {
60
+ // Dev mode: run via tsx
61
+ const tsxPath = join(__dirname, '..', 'node_modules', '.bin', 'tsx')
62
+ child = spawn(tsxPath, [srcPath, mode, ...args], {
63
+ stdio: 'inherit',
64
+ cwd: join(__dirname, '..'),
65
+ env: process.env,
66
+ })
67
+ } else if (existsSync(distPath)) {
68
+ // Production: run compiled JS directly
69
+ child = spawn('node', [distPath, mode, ...args], {
70
+ stdio: 'inherit',
71
+ cwd: join(__dirname, '..'),
72
+ env: process.env,
73
+ })
74
+ } else {
75
+ console.error('Error: Neither src/index.ts nor dist/index.js found')
76
+ process.exit(1)
77
+ }
62
78
 
63
79
  child.on('error', (err) => {
64
80
  console.error('Failed to start agent:', err.message)
@@ -29,8 +29,8 @@ export interface ClaudeAuthHandle {
29
29
  */
30
30
  export declare function isClaudeAuthenticated(): boolean;
31
31
  /**
32
- * Check auth via `claude auth status --json` (most reliable).
33
- * Uses execSync to avoid node-pty PATH issues on macOS.
32
+ * Check auth via `claude auth status` (most reliable).
33
+ * Uses resolved path to avoid posix_spawnp PATH issues.
34
34
  */
35
35
  export declare function checkClaudeAuthStatus(): Promise<boolean>;
36
36
  /**
@@ -15,8 +15,61 @@
15
15
  */
16
16
  import * as pty from 'node-pty';
17
17
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
18
+ import { execSync } from 'child_process';
18
19
  import { homedir } from 'os';
19
20
  import { join } from 'path';
21
+ /**
22
+ * Resolve the full path to the `claude` binary.
23
+ * node-pty uses posix_spawnp which may not find binaries in nvm/homebrew paths.
24
+ * Shell-based `which` resolves the full PATH including .zshrc/.bashrc additions.
25
+ * Also checks Docker/Linux global npm paths for Fly.io/container deployments.
26
+ */
27
+ let _cachedClaudePath = null;
28
+ function resolveClaudePath() {
29
+ if (_cachedClaudePath)
30
+ return _cachedClaudePath;
31
+ // 1. Shell-based resolution — picks up nvm, homebrew, etc.
32
+ try {
33
+ const resolved = execSync('which claude', { encoding: 'utf-8', timeout: 5000 }).trim();
34
+ if (resolved && existsSync(resolved)) {
35
+ _cachedClaudePath = resolved;
36
+ return resolved;
37
+ }
38
+ }
39
+ catch { }
40
+ // 2. Fallback: check common locations (macOS, Linux, Docker, nvm)
41
+ const candidates = [
42
+ // Linux / Docker / Fly.io (npm install -g @anthropic-ai/claude-code)
43
+ '/usr/local/bin/claude',
44
+ '/usr/bin/claude',
45
+ // macOS Homebrew (Apple Silicon)
46
+ '/opt/homebrew/bin/claude',
47
+ // nvm (current node version — macOS/Linux)
48
+ join(homedir(), '.nvm/versions/node', process.version, 'bin/claude'),
49
+ // macOS Homebrew cask (Intel)
50
+ '/usr/local/Caskroom/claude-code/latest/claude',
51
+ ];
52
+ for (const p of candidates) {
53
+ if (existsSync(p)) {
54
+ console.log(`🔑 Found claude at: ${p}`);
55
+ _cachedClaudePath = p;
56
+ return p;
57
+ }
58
+ }
59
+ // 3. Try npm global bin directory (covers custom npm prefix, Docker variants)
60
+ try {
61
+ const npmBin = execSync('npm bin -g', { encoding: 'utf-8', timeout: 5000 }).trim();
62
+ const npmClaudePath = join(npmBin, 'claude');
63
+ if (existsSync(npmClaudePath)) {
64
+ console.log(`🔑 Found claude at: ${npmClaudePath} (via npm bin -g)`);
65
+ _cachedClaudePath = npmClaudePath;
66
+ return npmClaudePath;
67
+ }
68
+ }
69
+ catch { }
70
+ console.warn('⚠️ Could not resolve claude binary path — falling back to "claude"');
71
+ return 'claude'; // last resort — let posix_spawnp try
72
+ }
20
73
  // ─────────────────────────────────────────
21
74
  // Constants
22
75
  // ─────────────────────────────────────────
@@ -68,13 +121,14 @@ export function isClaudeAuthenticated() {
68
121
  }
69
122
  }
70
123
  /**
71
- * Check auth via `claude auth status --json` (most reliable).
72
- * Uses execSync to avoid node-pty PATH issues on macOS.
124
+ * Check auth via `claude auth status` (most reliable).
125
+ * Uses resolved path to avoid posix_spawnp PATH issues.
73
126
  */
74
127
  export async function checkClaudeAuthStatus() {
75
128
  try {
76
- const { execSync } = await import('child_process');
77
- const output = execSync('claude auth status', {
129
+ const claudePath = resolveClaudePath();
130
+ console.log(`🔑 Checking auth via: ${claudePath} auth status`);
131
+ const output = execSync(`"${claudePath}" auth status`, {
78
132
  encoding: 'utf-8',
79
133
  timeout: 10_000,
80
134
  env: { ...process.env },
@@ -157,8 +211,9 @@ export function runClaudeAuthFlow(callbacks) {
157
211
  },
158
212
  };
159
213
  const done = new Promise((resolve, reject) => {
160
- console.log('🔑 Starting Claude Code authentication flow (setup-token)...');
161
- const proc = pty.spawn('claude', ['setup-token'], {
214
+ const claudePath = resolveClaudePath();
215
+ console.log(`🔑 Starting Claude Code authentication flow: ${claudePath} setup-token`);
216
+ const proc = pty.spawn(claudePath, ['setup-token'], {
162
217
  name: 'xterm-color',
163
218
  cols: 500, // Wide to prevent Ink URL wrapping
164
219
  rows: 30,
@@ -9,7 +9,7 @@
9
9
  import { llm, shortuuid, DEFAULT_API_CONNECT_OPTIONS } from '@livekit/agents';
10
10
  import { query } from '@anthropic-ai/claude-agent-sdk';
11
11
  import { EventEmitter } from 'events';
12
- import { saveSessionMetadata } from './config.js';
12
+ import { saveSessionMetadata, getSessionWorkspace } from './config.js';
13
13
  import { getResearchSystemPrompt, getDirectModeResearchPrompt } from './prompts.js';
14
14
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
15
15
  import { join } from 'node:path';
@@ -736,14 +736,10 @@ class ClaudeLLMStream extends llm.LLMStream {
736
736
  // Build Claude Agent SDK options
737
737
  const resumeSessionId = this.#opts.resumeSessionId;
738
738
  const continueSession = this.#opts.continueSession;
739
- // Session workspace path for system prompt — uses sessionBaseDir (not cwd) so
740
- // workspace always lives in the Osborn install dir regardless of cwd setting
739
+ // Session workspace path for system prompt — lives under ~/.claude/projects/{slug}/osb/{sessionId}/
741
740
  const sessionId = this.#sessionId || this.#opts.resumeSessionId || null;
742
- const baseDir = this.#opts.sessionBaseDir || this.#opts.workingDirectory;
743
- const workspacePath = sessionId
744
- ? (baseDir
745
- ? `${baseDir}/.osborn/sessions/${sessionId}/`
746
- : `.osborn/sessions/${sessionId}/`)
741
+ const workspacePath = sessionId && this.#opts.workingDirectory
742
+ ? getSessionWorkspace(this.#opts.workingDirectory, sessionId)
747
743
  : null;
748
744
  const allowedTools = this.#opts.allowedTools || [];
749
745
  const sdkOptions = {
@@ -775,12 +771,12 @@ class ClaudeLLMStream extends llm.LLMStream {
775
771
  console.log('input,', input, 'input.file_path', filePath, 'agent_type', agentType);
776
772
  console.log(`🔍 canUseTool: ${toolName} filePath="${filePath}" keys=${Object.keys(input || {}).join(',')}`);
777
773
  console.log(`🔍 canUseTool _options keys=[${Object.keys(_options || {}).join(', ')}] title="${_options?.title || ''}" decisionReason="${_options?.decisionReason || ''}" blockedPath="${_options?.blockedPath || ''}"`);
778
- if (filePath.includes('.osborn/sessions/') || filePath.includes('.osborn/research/')) {
779
- // Block writes to spec.md and library/ — the fast brain manages these
774
+ if (filePath.includes('/osb/') || filePath.includes('.osborn/sessions/') || filePath.includes('.osborn/research/')) {
775
+ // Block writes to spec.md — the fast brain manages it
780
776
  const fileName = filePath.split('/').pop() || '';
781
- if (fileName === 'spec.md' || filePath.includes('/library/')) {
782
- console.log(`🚫 Blocked research agent write to managed file: ${filePath} (fast brain handles spec.md and library/)`);
783
- return { behavior: 'deny', message: 'spec.md and library/ are managed by the fast brain sub-agent. Do NOT write to them. Return your findings in your response text — the fast brain will organize them into spec.md and library/ automatically.' };
777
+ if (fileName === 'spec.md') {
778
+ console.log(`🚫 Blocked research agent write to managed file: ${filePath} (fast brain handles spec.md)`);
779
+ return { behavior: 'deny', message: 'spec.md is managed by the fast brain. Do NOT write to it. Return your findings in your response text — the fast brain will organize them into spec.md automatically.' };
784
780
  }
785
781
  console.log(`✅ Auto-approved ${toolName} to workspace: ${filePath}`);
786
782
  return { behavior: 'allow', updatedInput: input };
@@ -824,10 +820,10 @@ class ClaudeLLMStream extends llm.LLMStream {
824
820
  }
825
821
  // All other agents (main, researcher, reasoner, etc.): workspace only
826
822
  const filePath = String(toolInput.file_path || '');
827
- if (filePath && !filePath.includes('.osborn/sessions/') && !filePath.includes('.osborn/research/')) {
823
+ if (filePath && !filePath.includes('/osb/') && !filePath.includes('.osborn/sessions/') && !filePath.includes('.osborn/research/')) {
828
824
  console.log(`🚫 Research mode: blocked write to ${filePath} (agent_type: ${agentType ?? 'main'})`);
829
825
  this.#eventEmitter.emit('tool_blocked', { name: toolName, reason: 'Research mode: writes restricted to session workspace' });
830
- return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny' }, reason: 'Research mode: write to .osborn/sessions/ only.' };
826
+ return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny' }, reason: 'Research mode: writes restricted to session workspace.' };
831
827
  }
832
828
  }
833
829
  console.log(`🔧 Claude: ${toolName}`);
package/dist/config.d.ts CHANGED
@@ -168,6 +168,25 @@ export declare function getMostRecentSessionId(projectPath?: string): Promise<st
168
168
  * Check if a session exists
169
169
  */
170
170
  export declare function sessionExists(sessionId: string, projectPath?: string): boolean;
171
+ export interface ClaudeSessionEntry {
172
+ sessionId: string;
173
+ projectSlug: string;
174
+ projectPath: string;
175
+ cwd: string;
176
+ timestamp: Date;
177
+ lastMessage?: string;
178
+ messageCount: number;
179
+ filePath: string;
180
+ fileSize: number;
181
+ }
182
+ /**
183
+ * Scan ALL Claude Code projects for sessions.
184
+ * Returns lightweight metadata for every main session JSONL across all projects.
185
+ * Uses existing getSessionPreview() for last message + message count.
186
+ *
187
+ * @param limit - Max sessions to return (default 100, sorted by recency)
188
+ */
189
+ export declare function listAllClaudeSessions(limit?: number): Promise<ClaudeSessionEntry[]>;
171
190
  /**
172
191
  * Session summary for context briefing when switching sessions
173
192
  */
@@ -229,23 +248,28 @@ export declare function deleteSessionMetadata(projectPath: string, sessionId: st
229
248
  * Clean up metadata for sessions that no longer exist
230
249
  */
231
250
  export declare function cleanupOrphanedMetadata(projectPath: string): Promise<number>;
232
- export declare function getSessionWorkspace(projectPath: string, sessionId: string): string;
233
- export declare function ensureSessionWorkspace(projectPath: string, sessionId: string): string;
251
+ /**
252
+ * Get the session workspace path.
253
+ * @param workingDir - The project working directory (used to compute project slug)
254
+ * @param sessionId - The session UUID
255
+ */
256
+ export declare function getSessionWorkspace(workingDir: string, sessionId: string): string;
257
+ export declare function ensureSessionWorkspace(workingDir: string, sessionId: string): string;
234
258
  /**
235
259
  * Rename a session workspace folder to match the SDK session ID.
236
260
  * Returns the new path, or null if rename was not needed/possible.
237
261
  */
238
- export declare function renameSessionWorkspace(projectPath: string, oldSessionId: string, newSessionId: string): string | null;
239
- export declare function getResearchDir(projectPath: string, sessionId: string): string;
240
- export declare function ensureResearchDir(projectPath: string, sessionId: string): string;
262
+ export declare function renameSessionWorkspace(workingDir: string, oldSessionId: string, newSessionId: string): string | null;
263
+ export declare function getResearchDir(workingDir: string, sessionId: string): string;
264
+ export declare function ensureResearchDir(workingDir: string, sessionId: string): string;
241
265
  /**
242
266
  * Read the session spec document (spec.md) if it exists
243
267
  */
244
- export declare function readSessionSpec(projectPath: string, sessionId: string): string | null;
268
+ export declare function readSessionSpec(workingDir: string, sessionId: string): string | null;
245
269
  /**
246
- * List files in the session library directory
270
+ * List files in the session library directory (deprecated — library removed)
247
271
  */
248
- export declare function listLibraryFiles(projectPath: string, sessionId: string): string[];
272
+ export declare function listLibraryFiles(_workingDir: string, _sessionId: string): string[];
249
273
  export interface ResearchArtifact {
250
274
  fileName: string;
251
275
  filePath: string;
@@ -253,11 +277,9 @@ export interface ResearchArtifact {
253
277
  size: number;
254
278
  updatedAt: string;
255
279
  }
256
- export declare function listResearchArtifacts(projectPath: string, sessionId: string): ResearchArtifact[];
280
+ export declare function listResearchArtifacts(workingDir: string, sessionId: string): ResearchArtifact[];
257
281
  /**
258
- * List artifacts in a session workspace.
259
- * When sessionId is provided, scans the per-session folder (.osborn/sessions/{sessionId}/).
260
- * Without sessionId, falls back to the flat .osborn/sessions/ directory (legacy).
282
+ * List artifacts in a session workspace (osb/{sessionId}/ under Claude's project dir).
261
283
  */
262
- export declare function listWorkspaceArtifacts(projectPath: string, sessionId?: string): ResearchArtifact[];
284
+ export declare function listWorkspaceArtifacts(workingDir: string, sessionId?: string): ResearchArtifact[];
263
285
  export {};
package/dist/config.js CHANGED
@@ -503,6 +503,116 @@ export function sessionExists(sessionId, projectPath) {
503
503
  const sessionFile = join(sessionDir, `${sessionId}.jsonl`);
504
504
  return existsSync(sessionFile);
505
505
  }
506
+ /**
507
+ * Reverse a project slug back to a path (best-effort — replace leading dash, then dashes→slashes).
508
+ * "-Users-foo-bar" → "/Users/foo/bar"
509
+ */
510
+ function slugToPath(slug) {
511
+ return slug.replace(/^-/, '/').replace(/-/g, '/');
512
+ }
513
+ const UUID_JSONL_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.jsonl$/i;
514
+ /**
515
+ * Extract cwd from first user message in a JSONL file.
516
+ * Reuses the existing readline-based parsing pattern.
517
+ */
518
+ async function extractCwd(filePath) {
519
+ return new Promise((resolve) => {
520
+ const fileStream = createReadStream(filePath, { end: 8192 }); // first 8KB
521
+ const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
522
+ rl.on('line', (line) => {
523
+ if (!line.trim())
524
+ return;
525
+ try {
526
+ const obj = JSON.parse(line);
527
+ if (obj.type === 'user' && obj.cwd) {
528
+ rl.close();
529
+ fileStream.destroy();
530
+ resolve(obj.cwd);
531
+ }
532
+ }
533
+ catch { }
534
+ });
535
+ rl.on('close', () => resolve(''));
536
+ rl.on('error', () => resolve(''));
537
+ });
538
+ }
539
+ /**
540
+ * Scan ALL Claude Code projects for sessions.
541
+ * Returns lightweight metadata for every main session JSONL across all projects.
542
+ * Uses existing getSessionPreview() for last message + message count.
543
+ *
544
+ * @param limit - Max sessions to return (default 100, sorted by recency)
545
+ */
546
+ export async function listAllClaudeSessions(limit = 100) {
547
+ const projectsDir = getClaudeProjectsDir();
548
+ if (!existsSync(projectsDir))
549
+ return [];
550
+ // 1. Discover all project folders
551
+ const projectFolders = readdirSync(projectsDir).filter(name => {
552
+ const fullPath = join(projectsDir, name);
553
+ try {
554
+ return statSync(fullPath).isDirectory();
555
+ }
556
+ catch {
557
+ return false;
558
+ }
559
+ });
560
+ // 2. Collect all JSONL files with stat info (fast — no file reads yet)
561
+ const candidates = [];
562
+ for (const slug of projectFolders) {
563
+ const projectDir = join(projectsDir, slug);
564
+ try {
565
+ const files = readdirSync(projectDir);
566
+ for (const file of files) {
567
+ if (!UUID_JSONL_RE.test(file))
568
+ continue;
569
+ const fullPath = join(projectDir, file);
570
+ try {
571
+ const stats = statSync(fullPath);
572
+ if (stats.size > 500) { // Skip trivially small files
573
+ candidates.push({
574
+ filePath: fullPath,
575
+ slug,
576
+ sessionId: file.replace('.jsonl', ''),
577
+ mtime: stats.mtime,
578
+ size: stats.size,
579
+ });
580
+ }
581
+ }
582
+ catch { }
583
+ }
584
+ }
585
+ catch { }
586
+ }
587
+ // 3. Sort by mtime descending, take top N for detailed reads
588
+ candidates.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
589
+ const topCandidates = candidates.slice(0, limit);
590
+ // 4. Read metadata using existing getSessionPreview + cwd extraction
591
+ const sessions = [];
592
+ for (const c of topCandidates) {
593
+ try {
594
+ const [preview, cwd] = await Promise.all([
595
+ getSessionPreview(c.filePath),
596
+ extractCwd(c.filePath),
597
+ ]);
598
+ if (preview.messageCount < 2)
599
+ continue;
600
+ sessions.push({
601
+ sessionId: c.sessionId,
602
+ projectSlug: c.slug,
603
+ projectPath: cwd || slugToPath(c.slug),
604
+ cwd: cwd || slugToPath(c.slug),
605
+ timestamp: c.mtime,
606
+ lastMessage: preview.lastMessage,
607
+ messageCount: preview.messageCount,
608
+ filePath: c.filePath,
609
+ fileSize: c.size,
610
+ });
611
+ }
612
+ catch { }
613
+ }
614
+ return sessions;
615
+ }
506
616
  /**
507
617
  * Get a summary of a session for context briefing
508
618
  * Extracts last few messages and mode info for realtime agent
@@ -729,15 +839,22 @@ export async function cleanupOrphanedMetadata(projectPath) {
729
839
  return cleanedCount;
730
840
  }
731
841
  // ============================================================
732
- // SESSION WORKSPACE - For research artifacts and session library
842
+ // SESSION WORKSPACE — Co-located with Claude's native JSONL
843
+ // ~/.claude/projects/{slug}/osb/{sessionId}/
733
844
  // ============================================================
734
- export function getSessionWorkspace(projectPath, sessionId) {
735
- return join(projectPath, '.osborn', 'sessions', sessionId);
845
+ /**
846
+ * Get the session workspace path.
847
+ * @param workingDir - The project working directory (used to compute project slug)
848
+ * @param sessionId - The session UUID
849
+ */
850
+ export function getSessionWorkspace(workingDir, sessionId) {
851
+ const claudeDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
852
+ const slug = projectPathToClaudeFolderName(workingDir);
853
+ return join(claudeDir, 'projects', slug, 'osb', sessionId);
736
854
  }
737
- export function ensureSessionWorkspace(projectPath, sessionId) {
738
- const dir = getSessionWorkspace(projectPath, sessionId);
739
- const libraryDir = join(dir, 'library');
740
- mkdirSync(libraryDir, { recursive: true });
855
+ export function ensureSessionWorkspace(workingDir, sessionId) {
856
+ const dir = getSessionWorkspace(workingDir, sessionId);
857
+ mkdirSync(dir, { recursive: true });
741
858
  // Create default spec.md if it doesn't exist (won't overwrite on resumed sessions)
742
859
  const specPath = join(dir, 'spec.md');
743
860
  if (!existsSync(specPath)) {
@@ -783,11 +900,11 @@ export function ensureSessionWorkspace(projectPath, sessionId) {
783
900
  * Rename a session workspace folder to match the SDK session ID.
784
901
  * Returns the new path, or null if rename was not needed/possible.
785
902
  */
786
- export function renameSessionWorkspace(projectPath, oldSessionId, newSessionId) {
903
+ export function renameSessionWorkspace(workingDir, oldSessionId, newSessionId) {
787
904
  if (oldSessionId === newSessionId)
788
905
  return null;
789
- const oldDir = getSessionWorkspace(projectPath, oldSessionId);
790
- const newDir = getSessionWorkspace(projectPath, newSessionId);
906
+ const oldDir = getSessionWorkspace(workingDir, oldSessionId);
907
+ const newDir = getSessionWorkspace(workingDir, newSessionId);
791
908
  if (!existsSync(oldDir))
792
909
  return null;
793
910
  if (existsSync(newDir))
@@ -797,22 +914,22 @@ export function renameSessionWorkspace(projectPath, oldSessionId, newSessionId)
797
914
  return newDir;
798
915
  }
799
916
  catch (err) {
800
- console.error(`⚠️ Failed to rename workspace ${oldSessionId} → ${newSessionId}:`, err);
917
+ console.error(`Failed to rename workspace ${oldSessionId} → ${newSessionId}:`, err);
801
918
  return null;
802
919
  }
803
920
  }
804
921
  // Deprecated aliases for backward compatibility
805
- export function getResearchDir(projectPath, sessionId) {
806
- return getSessionWorkspace(projectPath, sessionId);
922
+ export function getResearchDir(workingDir, sessionId) {
923
+ return getSessionWorkspace(workingDir, sessionId);
807
924
  }
808
- export function ensureResearchDir(projectPath, sessionId) {
809
- return ensureSessionWorkspace(projectPath, sessionId);
925
+ export function ensureResearchDir(workingDir, sessionId) {
926
+ return ensureSessionWorkspace(workingDir, sessionId);
810
927
  }
811
928
  /**
812
929
  * Read the session spec document (spec.md) if it exists
813
930
  */
814
- export function readSessionSpec(projectPath, sessionId) {
815
- const specPath = join(getSessionWorkspace(projectPath, sessionId), 'spec.md');
931
+ export function readSessionSpec(workingDir, sessionId) {
932
+ const specPath = join(getSessionWorkspace(workingDir, sessionId), 'spec.md');
816
933
  if (!existsSync(specPath))
817
934
  return null;
818
935
  try {
@@ -823,18 +940,10 @@ export function readSessionSpec(projectPath, sessionId) {
823
940
  }
824
941
  }
825
942
  /**
826
- * List files in the session library directory
943
+ * List files in the session library directory (deprecated — library removed)
827
944
  */
828
- export function listLibraryFiles(projectPath, sessionId) {
829
- const libraryDir = join(getSessionWorkspace(projectPath, sessionId), 'library');
830
- if (!existsSync(libraryDir))
831
- return [];
832
- try {
833
- return readdirSync(libraryDir);
834
- }
835
- catch {
836
- return [];
837
- }
945
+ export function listLibraryFiles(_workingDir, _sessionId) {
946
+ return [];
838
947
  }
839
948
  function classifyFile(fileName) {
840
949
  const ext = fileName.split('.').pop()?.toLowerCase() || '';
@@ -863,8 +972,8 @@ function scanDirForArtifacts(dir) {
863
972
  scan(fullPath);
864
973
  }
865
974
  else {
866
- // Skip internal index files and .index/ folder
867
- if (entry.startsWith('search-index') || entry === '.index')
975
+ // Skip internal index files
976
+ if (entry.startsWith('search-index'))
868
977
  continue;
869
978
  results.push({
870
979
  fileName: entry,
@@ -881,17 +990,15 @@ function scanDirForArtifacts(dir) {
881
990
  scan(dir);
882
991
  return results;
883
992
  }
884
- export function listResearchArtifacts(projectPath, sessionId) {
885
- return scanDirForArtifacts(getSessionWorkspace(projectPath, sessionId));
993
+ export function listResearchArtifacts(workingDir, sessionId) {
994
+ return scanDirForArtifacts(getSessionWorkspace(workingDir, sessionId));
886
995
  }
887
996
  /**
888
- * List artifacts in a session workspace.
889
- * When sessionId is provided, scans the per-session folder (.osborn/sessions/{sessionId}/).
890
- * Without sessionId, falls back to the flat .osborn/sessions/ directory (legacy).
997
+ * List artifacts in a session workspace (osb/{sessionId}/ under Claude's project dir).
891
998
  */
892
- export function listWorkspaceArtifacts(projectPath, sessionId) {
893
- const dir = sessionId
894
- ? join(projectPath, '.osborn', 'sessions', sessionId)
895
- : join(projectPath, '.osborn', 'sessions');
999
+ export function listWorkspaceArtifacts(workingDir, sessionId) {
1000
+ if (!sessionId)
1001
+ return [];
1002
+ const dir = getSessionWorkspace(workingDir, sessionId);
896
1003
  return scanDirForArtifacts(dir);
897
1004
  }
@@ -5,7 +5,7 @@
5
5
  * The realtime voice model is a thin teleprompter — it speaks what this module returns.
6
6
  *
7
7
  * Capabilities:
8
- * - Read/write session files (spec.md + library/)
8
+ * - Read/write session files (spec.md)
9
9
  * - Web search for quick factual lookups
10
10
  * - Record user decisions and preferences into spec.md
11
11
  * - Trigger deep research (via callbacks to index.ts)
@@ -71,7 +71,7 @@ export declare function askFastBrain(workingDir: string, sessionId: string, ques
71
71
  }): Promise<FastBrainResponse>;
72
72
  /**
73
73
  * Process a batch of research content chunks through the fast brain.
74
- * Updates spec.md and library/ files incrementally during research.
74
+ * Updates spec.md incrementally during research.
75
75
  *
76
76
  * @param isRefinement - true for the final post-research consolidation pass (higher token budget)
77
77
  */
@@ -88,7 +88,7 @@ export declare function processResearchChunk(workingDir: string, sessionId: stri
88
88
  */
89
89
  export declare function augmentResearchResult(workingDir: string, sessionId: string, task: string, agentResult: string): Promise<string>;
90
90
  /**
91
- * Update spec.md and library/ files after research completes.
91
+ * Update spec.md after research completes.
92
92
  * Reads FULL untruncated data directly from Claude Agent SDK JSONL files
93
93
  * instead of receiving pre-truncated content chunks.
94
94
  *
@@ -97,7 +97,7 @@ export declare function augmentResearchResult(workingDir: string, sessionId: str
97
97
  * - readSessionHistory() — last 50 assistant messages (agent reasoning/analysis)
98
98
  * - getSubagentTranscripts() — all sub-agent findings
99
99
  *
100
- * Returns { spec, libraryFiles } or null if update failed.
100
+ * Returns { spec } or null if update failed.
101
101
  */
102
102
  export declare function updateSpecFromJSONL(workingDir: string, sessionId: string, task: string, researchLog: string[], sessionBaseDir?: string): Promise<{
103
103
  spec: string | null;
@@ -139,8 +139,8 @@ export declare function generateProactivePrompt(workingDir: string, sessionId: s
139
139
  * Generate a structured visual document (comparison table, Mermaid diagram,
140
140
  * analysis, or summary) from research findings.
141
141
  *
142
- * Reads spec.md, JSONL results, and library for context.
143
- * Writes the result to library/ and returns the filename + content.
142
+ * Reads spec.md and JSONL results for context.
143
+ * Writes the result to workspace and returns the filename + content.
144
144
  */
145
145
  export declare function generateVisualDocument(workingDir: string, sessionId: string, request: string, documentType: 'comparison' | 'diagram' | 'analysis' | 'summary', sessionBaseDir?: string): Promise<{
146
146
  fileName: string;