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 +25 -9
- package/dist/claude-auth.d.ts +2 -2
- package/dist/claude-auth.js +61 -6
- package/dist/claude-llm.js +11 -15
- package/dist/config.d.ts +35 -13
- package/dist/config.js +146 -39
- package/dist/fast-brain.d.ts +6 -6
- package/dist/fast-brain.js +17 -97
- package/dist/index.js +81 -51
- package/dist/pipeline-direct-llm.js +2 -2
- package/dist/pipeline-fastbrain.js +10 -9
- package/dist/prompts.d.ts +4 -4
- package/dist/prompts.js +28 -57
- package/dist/session-access.d.ts +1 -0
- package/dist/session-access.js +1 -1
- package/dist/summary-index.d.ts +8 -5
- package/dist/summary-index.js +28 -13
- package/package.json +1 -1
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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)
|
package/dist/claude-auth.d.ts
CHANGED
|
@@ -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
|
|
33
|
-
* Uses
|
|
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
|
/**
|
package/dist/claude-auth.js
CHANGED
|
@@ -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
|
|
72
|
-
* Uses
|
|
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
|
|
77
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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,
|
package/dist/claude-llm.js
CHANGED
|
@@ -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 —
|
|
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
|
|
743
|
-
|
|
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
|
|
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'
|
|
782
|
-
console.log(`🚫 Blocked research agent write to managed file: ${filePath} (fast brain handles spec.md
|
|
783
|
-
return { behavior: 'deny', message: 'spec.md
|
|
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:
|
|
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
|
-
|
|
233
|
-
|
|
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(
|
|
239
|
-
export declare function getResearchDir(
|
|
240
|
-
export declare function ensureResearchDir(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 -
|
|
842
|
+
// SESSION WORKSPACE — Co-located with Claude's native JSONL
|
|
843
|
+
// ~/.claude/projects/{slug}/osb/{sessionId}/
|
|
733
844
|
// ============================================================
|
|
734
|
-
|
|
735
|
-
|
|
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(
|
|
738
|
-
const dir = getSessionWorkspace(
|
|
739
|
-
|
|
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(
|
|
903
|
+
export function renameSessionWorkspace(workingDir, oldSessionId, newSessionId) {
|
|
787
904
|
if (oldSessionId === newSessionId)
|
|
788
905
|
return null;
|
|
789
|
-
const oldDir = getSessionWorkspace(
|
|
790
|
-
const newDir = getSessionWorkspace(
|
|
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(
|
|
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(
|
|
806
|
-
return getSessionWorkspace(
|
|
922
|
+
export function getResearchDir(workingDir, sessionId) {
|
|
923
|
+
return getSessionWorkspace(workingDir, sessionId);
|
|
807
924
|
}
|
|
808
|
-
export function ensureResearchDir(
|
|
809
|
-
return ensureSessionWorkspace(
|
|
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(
|
|
815
|
-
const specPath = join(getSessionWorkspace(
|
|
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(
|
|
829
|
-
|
|
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
|
|
867
|
-
if (entry.startsWith('search-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(
|
|
885
|
-
return scanDirForArtifacts(getSessionWorkspace(
|
|
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(
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
999
|
+
export function listWorkspaceArtifacts(workingDir, sessionId) {
|
|
1000
|
+
if (!sessionId)
|
|
1001
|
+
return [];
|
|
1002
|
+
const dir = getSessionWorkspace(workingDir, sessionId);
|
|
896
1003
|
return scanDirForArtifacts(dir);
|
|
897
1004
|
}
|
package/dist/fast-brain.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
143
|
-
* Writes the result to
|
|
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;
|