osborn 0.8.0 → 0.8.2

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.
@@ -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)
@@ -24,10 +24,10 @@
24
24
  import Anthropic from '@anthropic-ai/sdk';
25
25
  import { query as sdkQuery, tool as sdkTool, createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk';
26
26
  import { GoogleGenAI } from '@google/genai';
27
- import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'fs';
27
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
28
28
  import { dirname, basename } from 'path';
29
29
  import { z } from 'zod';
30
- import { getSessionWorkspace, readSessionSpec, listLibraryFiles } from './config.js';
30
+ import { getSessionWorkspace, readSessionSpec } from './config.js';
31
31
  import { FAST_BRAIN_SYSTEM_PROMPT, CHUNK_PROCESS_SYSTEM, REFINEMENT_PROCESS_SYSTEM, AUGMENT_RESULT_SYSTEM, CONTEXTUALIZE_UPDATE_SYSTEM, PROACTIVE_PROMPT_SYSTEM, VISUAL_DOCUMENT_SYSTEM, RESEARCH_COMPLETION_SYSTEM, buildFastBrainSdkPrompt } from './prompts.js';
32
32
  import { getRecentToolResults, readSessionHistory, getSubagentTranscripts, getConversationText, getSessionTranscripts, searchSessionJsonl, getSessionStats } from './session-access.js';
33
33
  // ============================================================
@@ -164,18 +164,6 @@ function executeTool(toolName, toolInput, workspace, sessionId, workingDir, send
164
164
  console.log(`📝 Fast brain wrote ${relPath} (${content.length} chars)`);
165
165
  return `Written: ${relPath} (${content.length} chars)`;
166
166
  }
167
- case 'list_library': {
168
- const libraryDir = `${workspace}/library`;
169
- if (!existsSync(libraryDir))
170
- return 'Library is empty — no research files yet.';
171
- try {
172
- const items = readdirSync(libraryDir);
173
- return items.length > 0 ? items.join('\n') : 'Library is empty — no research files yet.';
174
- }
175
- catch {
176
- return 'Library is empty — no research files yet.';
177
- }
178
- }
179
167
  case 'read_agent_results': {
180
168
  if (!sessionId || !workingDir)
181
169
  return 'Error: no active research session';
@@ -328,7 +316,7 @@ function buildAnthropicTools() {
328
316
  return [
329
317
  {
330
318
  name: 'read_file',
331
- description: 'Read a file from the session workspace. Use relative paths like "spec.md" or "library/react-guide.md".',
319
+ description: 'Read a file from the session workspace. Use "spec.md" to read the session spec.',
332
320
  input_schema: {
333
321
  type: 'object',
334
322
  properties: {
@@ -349,11 +337,6 @@ function buildAnthropicTools() {
349
337
  required: ['path', 'content']
350
338
  }
351
339
  },
352
- {
353
- name: 'list_library',
354
- description: 'List all files in the research library directory.',
355
- input_schema: { type: 'object', properties: {} }
356
- },
357
340
  {
358
341
  name: 'read_agent_results',
359
342
  description: 'Read the research agent\'s FULL memory — complete untruncated tool outputs including entire file contents the agent read, full bash command outputs, web search results, and web page fetches. This is the agent\'s raw data. Use this FIRST when asked about anything the agent just researched. Default: last 40 results.',
@@ -460,7 +443,7 @@ function buildGeminiTools() {
460
443
  functionDeclarations: [
461
444
  {
462
445
  name: 'read_file',
463
- description: 'Read a file from the session workspace. Use relative paths like "spec.md" or "library/react-guide.md".',
446
+ description: 'Read a file from the session workspace. Use "spec.md" to read the session spec.',
464
447
  parameters: {
465
448
  type: 'object',
466
449
  properties: {
@@ -481,11 +464,6 @@ function buildGeminiTools() {
481
464
  required: ['path', 'content']
482
465
  }
483
466
  },
484
- {
485
- name: 'list_library',
486
- description: 'List all files in the research library directory.',
487
- parameters: { type: 'object', properties: {} }
488
- },
489
467
  {
490
468
  name: 'web_search',
491
469
  description: 'Search the web for current information. Use for factual questions like "current version of X", "what is X", definitions, etc.',
@@ -886,7 +864,7 @@ async function askViaGemini(question, workspace, researchContext, sessionId, wor
886
864
  */
887
865
  export async function askHaiku(workingDir, sessionId, question, researchContext, chatHistory, sendToChat, sessionBaseDir) {
888
866
  initProvider();
889
- // workspace uses sessionBaseDir (Osborn install dir) for spec.md/library
867
+ // workspace uses workingDir for spec.md (under ~/.claude/projects/{slug}/osb/)
890
868
  // workingDir is for JSONL access (matches Claude SDK cwd)
891
869
  const wsDir = sessionBaseDir || workingDir;
892
870
  const workspace = getSessionWorkspace(wsDir, sessionId);
@@ -917,7 +895,7 @@ export async function askFastBrain(workingDir, sessionId, question, opts) {
917
895
  try {
918
896
  const result = await generateVisualDocument(workingDir, sessionId, question, docMatch, wsDir);
919
897
  if (result) {
920
- const fullPath = `${wsDir}/.osborn/sessions/${sessionId}/library/${result.fileName}`;
898
+ const fullPath = `${getSessionWorkspace(wsDir, sessionId)}/${result.fileName}`;
921
899
  callbacks.sendToFrontend({
922
900
  type: 'research_artifact_updated',
923
901
  filePath: fullPath,
@@ -1064,7 +1042,7 @@ function generateResearchAck(question, chatHistory) {
1064
1042
  // ============================================================
1065
1043
  /**
1066
1044
  * Process a batch of research content chunks through the fast brain.
1067
- * Updates spec.md and library/ files incrementally during research.
1045
+ * Updates spec.md incrementally during research.
1068
1046
  *
1069
1047
  * @param isRefinement - true for the final post-research consolidation pass (higher token budget)
1070
1048
  */
@@ -1089,30 +1067,8 @@ export async function processResearchChunk(workingDir, sessionId, task, contentC
1089
1067
  return null;
1090
1068
  }
1091
1069
  const currentSpec = readFileSync(specPath, 'utf-8');
1092
- const libraryDir = `${workspace}/library`;
1093
- // Only read library files during refinement pass (final consolidation)
1094
- // Mid-research: skip library entirely to stay fast and avoid file proliferation
1095
- let existingSection = '';
1096
- if (isRefinement) {
1097
- const existingFiles = listLibraryFiles(wsDir, sessionId);
1098
- const existingContents = [];
1099
- for (const file of existingFiles) {
1100
- const filePath = `${libraryDir}/${file}`;
1101
- if (existsSync(filePath)) {
1102
- try {
1103
- const content = readFileSync(filePath, 'utf-8');
1104
- existingContents.push(`--- ${file} ---\n${content}`);
1105
- }
1106
- catch { /* skip */ }
1107
- }
1108
- }
1109
- existingSection = existingContents.length > 0
1110
- ? `\n\nExisting library/ files:\n${existingContents.join('\n\n')}`
1111
- : '';
1112
- }
1113
1070
  // No content capping — models handle 200K+ tokens (Haiku) / 1M+ (Gemini Flash)
1114
1071
  const chunksText = contentChunks.join('\n\n---\n\n');
1115
- // Use different prompts: mid-research = spec only, refinement = spec + library
1116
1072
  const systemPrompt = isRefinement ? REFINEMENT_PROCESS_SYSTEM : CHUNK_PROCESS_SYSTEM;
1117
1073
  const userMessage = `Research task: "${task}"
1118
1074
 
@@ -1120,7 +1076,6 @@ Current spec.md:
1120
1076
  \`\`\`markdown
1121
1077
  ${currentSpec}
1122
1078
  \`\`\`
1123
- ${existingSection}
1124
1079
 
1125
1080
  Content chunks from research:
1126
1081
  ${chunksText}
@@ -1151,7 +1106,6 @@ Return ONLY valid JSON — no code fences, no explanation.`;
1151
1106
  if (!parsed)
1152
1107
  return null;
1153
1108
  let updatedSpec = null;
1154
- const writtenFiles = [];
1155
1109
  // Write spec.md
1156
1110
  if (parsed.spec && typeof parsed.spec === 'string' && parsed.spec.length > 50) {
1157
1111
  writeFileSync(specPath, parsed.spec, 'utf-8');
@@ -1159,22 +1113,9 @@ Return ONLY valid JSON — no code fences, no explanation.`;
1159
1113
  const label = isRefinement ? 'refinement pass' : 'chunk';
1160
1114
  console.log(`📋 Fast brain processed research ${label} — spec.md updated (${parsed.spec.length} chars)`);
1161
1115
  }
1162
- // Write library files — ONLY during refinement pass (prevents file proliferation)
1163
- if (isRefinement && parsed.library && Array.isArray(parsed.library) && parsed.library.length > 0) {
1164
- mkdirSync(libraryDir, { recursive: true });
1165
- for (const file of parsed.library) {
1166
- if (!file.filename || !file.content)
1167
- continue;
1168
- const safeName = file.filename.replace(/[^a-zA-Z0-9._-]/g, '-');
1169
- const filePath = `${libraryDir}/${safeName}`;
1170
- writeFileSync(filePath, file.content, 'utf-8');
1171
- console.log(`📝 Fast brain wrote library/${safeName} (${file.content.length} chars)`);
1172
- writtenFiles.push(safeName);
1173
- }
1174
- }
1175
1116
  const label = isRefinement ? 'refinement' : `${contentChunks.length} content items`;
1176
1117
  console.log(`📋 Fast brain processed research chunk (${label})`);
1177
- return { spec: updatedSpec, libraryFiles: writtenFiles };
1118
+ return { spec: updatedSpec, libraryFiles: [] };
1178
1119
  }
1179
1120
  catch (err) {
1180
1121
  console.error('❌ processResearchChunk failed:', err);
@@ -1254,18 +1195,14 @@ export async function augmentResearchResult(workingDir, sessionId, task, agentRe
1254
1195
  try {
1255
1196
  // Read spec for context
1256
1197
  const specContent = readSessionSpec(workingDir, sessionId);
1257
- const libraryFiles = listLibraryFiles(workingDir, sessionId);
1258
1198
  const specSection = specContent
1259
1199
  ? `\n\nCurrent spec.md:\n${specContent}`
1260
1200
  : '';
1261
- const libSection = libraryFiles.length > 0
1262
- ? `\n\nLibrary files available: ${libraryFiles.join(', ')}`
1263
- : '';
1264
1201
  const userMessage = `Research task: "${task}"
1265
1202
 
1266
1203
  Agent findings:
1267
1204
  ${agentResult}
1268
- ${specSection}${libSection}
1205
+ ${specSection}
1269
1206
 
1270
1207
  Augment the agent's findings with relevant context from the spec. Pass ALL details through verbatim.`;
1271
1208
  let responseText = null;
@@ -1303,7 +1240,7 @@ Augment the agent's findings with relevant context from the spec. Pass ALL detai
1303
1240
  // updateSpecFromJSONL — Post-research spec consolidation via JSONL
1304
1241
  // ============================================================
1305
1242
  /**
1306
- * Update spec.md and library/ files after research completes.
1243
+ * Update spec.md after research completes.
1307
1244
  * Reads FULL untruncated data directly from Claude Agent SDK JSONL files
1308
1245
  * instead of receiving pre-truncated content chunks.
1309
1246
  *
@@ -1312,7 +1249,7 @@ Augment the agent's findings with relevant context from the spec. Pass ALL detai
1312
1249
  * - readSessionHistory() — last 50 assistant messages (agent reasoning/analysis)
1313
1250
  * - getSubagentTranscripts() — all sub-agent findings
1314
1251
  *
1315
- * Returns { spec, libraryFiles } or null if update failed.
1252
+ * Returns { spec } or null if update failed.
1316
1253
  */
1317
1254
  export async function updateSpecFromJSONL(workingDir, sessionId, task, researchLog, sessionBaseDir) {
1318
1255
  initProvider();
@@ -1692,8 +1629,8 @@ ${previousPrompts.length > 0 ? previousPrompts.join('\n') : '(none yet)'}`;
1692
1629
  * Generate a structured visual document (comparison table, Mermaid diagram,
1693
1630
  * analysis, or summary) from research findings.
1694
1631
  *
1695
- * Reads spec.md, JSONL results, and library for context.
1696
- * Writes the result to library/ and returns the filename + content.
1632
+ * Reads spec.md and JSONL results for context.
1633
+ * Writes the result to workspace and returns the filename + content.
1697
1634
  */
1698
1635
  export async function generateVisualDocument(workingDir, sessionId, request, documentType, sessionBaseDir) {
1699
1636
  initProvider();
@@ -1703,20 +1640,6 @@ export async function generateVisualDocument(workingDir, sessionId, request, doc
1703
1640
  try {
1704
1641
  const workspace = getSessionWorkspace(wsDir, sessionId);
1705
1642
  const specContent = readSessionSpec(wsDir, sessionId) || '';
1706
- const libraryFiles = listLibraryFiles(wsDir, sessionId);
1707
- // Read library contents for context
1708
- const libraryDir = `${workspace}/library`;
1709
- const libraryContents = [];
1710
- for (const file of libraryFiles.slice(0, 5)) {
1711
- const filePath = `${libraryDir}/${file}`;
1712
- if (existsSync(filePath)) {
1713
- try {
1714
- const content = readFileSync(filePath, 'utf-8');
1715
- libraryContents.push(`--- ${file} ---\n${content.substring(0, 3000)}`);
1716
- }
1717
- catch { /* skip */ }
1718
- }
1719
- }
1720
1643
  // Read recent JSONL results for raw data
1721
1644
  const toolResults = getRecentToolResults(sessionId, workingDir, 20);
1722
1645
  const toolResultsSummary = toolResults.map(tr => {
@@ -1729,8 +1652,6 @@ Document type: ${documentType}
1729
1652
  Session spec:
1730
1653
  ${specContent}
1731
1654
 
1732
- ${libraryContents.length > 0 ? `Library files:\n${libraryContents.join('\n\n')}` : ''}
1733
-
1734
1655
  Recent research data:
1735
1656
  ${toolResultsSummary}
1736
1657
 
@@ -1780,11 +1701,10 @@ Return JSON: {"fileName": "descriptive-name.md", "content": "full markdown conte
1780
1701
  }
1781
1702
  if (!parsed.fileName || !parsed.content)
1782
1703
  return null;
1783
- // Write to library
1704
+ // Write to workspace
1784
1705
  const safeName = parsed.fileName.replace(/[^a-zA-Z0-9._-]/g, '-');
1785
- const libraryPath = `${workspace}/library`;
1786
- mkdirSync(libraryPath, { recursive: true });
1787
- const filePath = `${libraryPath}/${safeName}`;
1706
+ mkdirSync(workspace, { recursive: true });
1707
+ const filePath = `${workspace}/${safeName}`;
1788
1708
  writeFileSync(filePath, parsed.content, 'utf-8');
1789
1709
  console.log(`📊 generateVisualDocument: wrote ${safeName} (${parsed.content.length} chars)`);
1790
1710
  return { fileName: safeName, content: parsed.content };
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ import { createServer } from 'http';
13
13
  import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
14
14
  import { join } from 'node:path';
15
15
  import { createPatch } from 'diff';
16
- import { loadConfig, getMcpServers, getEnabledMcpServerNames, getVoiceMode, getRealtimeConfig, getDirectConfig, listSessions, getMostRecentSessionId, sessionExists, cleanupOrphanedMetadata, getSessionSummary, getConversationHistory, ensureSessionWorkspace, getMcpServerStatusList, buildMcpServersForKeys, listWorkspaceArtifacts } from './config.js';
16
+ import { loadConfig, getMcpServers, getEnabledMcpServerNames, getVoiceMode, getRealtimeConfig, getDirectConfig, listAllClaudeSessions, getMostRecentSessionId, sessionExists, getSessionSummary, getConversationHistory, ensureSessionWorkspace, getSessionWorkspace, getMcpServerStatusList, buildMcpServersForKeys, listWorkspaceArtifacts } from './config.js';
17
17
  import { createSTT, createTTS, createRealtimeModelFromConfig, DIRECT_MODE_STT, DIRECT_MODE_TTS } from './voice-io.js';
18
18
  import { createClaudeLLM } from './claude-llm.js';
19
19
  import { clearPipelineFastBrainSession, prewarmBM25Index } from './pipeline-fastbrain.js';
@@ -150,14 +150,18 @@ function startApiServer(workingDir, port) {
150
150
  const url = new URL(req.url || '/', `http://localhost:${port}`);
151
151
  if (req.method === 'GET' && url.pathname === '/sessions') {
152
152
  try {
153
- await cleanupOrphanedMetadata(workingDir);
154
- const sessions = await listSessions(workingDir);
153
+ const limit = parseInt(url.searchParams.get('limit') || '100', 10);
154
+ const sessions = await listAllClaudeSessions(limit);
155
155
  const payload = {
156
156
  sessions: sessions.map(s => ({
157
157
  sessionId: s.sessionId,
158
+ projectSlug: s.projectSlug,
159
+ projectPath: s.projectPath,
160
+ cwd: s.cwd,
158
161
  timestamp: s.timestamp.toISOString(),
159
162
  lastMessage: s.lastMessage,
160
163
  messageCount: s.messageCount,
164
+ fileSize: s.fileSize,
161
165
  })),
162
166
  total: sessions.length,
163
167
  };
@@ -215,6 +219,14 @@ function startApiServer(workingDir, port) {
215
219
  res.end(JSON.stringify({ roomCode: currentRoomCode }));
216
220
  return;
217
221
  }
222
+ // POST /restart — graceful process restart (process manager will restart)
223
+ if (req.method === 'POST' && url.pathname === '/restart') {
224
+ res.writeHead(200, { 'Content-Type': 'application/json' });
225
+ res.end(JSON.stringify({ success: true, message: 'Agent restarting...' }));
226
+ console.log('🔄 Restart requested via HTTP — exiting for process manager restart');
227
+ setTimeout(() => process.exit(0), 150);
228
+ return;
229
+ }
218
230
  res.writeHead(404, { 'Content-Type': 'application/json' });
219
231
  res.end(JSON.stringify({ error: 'Not found' }));
220
232
  });
@@ -655,7 +667,7 @@ async function main() {
655
667
  const voiceSid = currentLLM?.sessionId;
656
668
  if (voiceSid) {
657
669
  const chatHistory = getChatHistory(10);
658
- handleResearchBatch(workingDir, voiceSid, lastTaskRequest || '', updates, activeResearch.researchLog, chatHistory, sessionBaseDir)
670
+ handleResearchBatch(workingDir, voiceSid, lastTaskRequest || '', updates, activeResearch.researchLog, chatHistory, workingDir)
659
671
  .then(script => {
660
672
  if (script && activeResearch) {
661
673
  activeResearch.voiceUpdateCount++;
@@ -776,12 +788,12 @@ async function main() {
776
788
  sessionAlwaysAllowPaths = new Set();
777
789
  // For resumed sessions, eagerly create workspace (we know the real ID)
778
790
  if (resumeSessionId) {
779
- const workspace = ensureSessionWorkspace(sessionBaseDir, resumeSessionId);
791
+ const workspace = ensureSessionWorkspace(workingDir, resumeSessionId);
780
792
  console.log(`📁 Session workspace (resumed): ${workspace}`);
781
793
  }
782
794
  // For new sessions, create workspace when SDK assigns real session ID
783
795
  directLLM.events.once('session_id', ({ sessionId }) => {
784
- const workspace = ensureSessionWorkspace(sessionBaseDir, sessionId);
796
+ const workspace = ensureSessionWorkspace(workingDir, sessionId);
785
797
  console.log(`📁 Session workspace created: ${workspace}`);
786
798
  // Pipeline mode: pre-warm BM25 index so first fast brain query is fast
787
799
  if (currentVoiceMode === 'pipeline') {
@@ -812,7 +824,7 @@ async function main() {
812
824
  // Detect research artifact writes (session workspace or legacy research dir)
813
825
  if ((data.name === 'Write' || data.name === 'Edit') && data.input?.file_path) {
814
826
  const fp = data.input.file_path;
815
- if (fp.includes('.osborn/sessions/') || fp.includes('.osborn/research/')) {
827
+ if (fp.includes('/osb/') || fp.includes('.osborn/sessions/') || fp.includes('.osborn/research/')) {
816
828
  sendToFrontend({
817
829
  type: 'research_artifact_updated',
818
830
  filePath: fp,
@@ -938,10 +950,13 @@ async function main() {
938
950
  diff: diffString,
939
951
  });
940
952
  // Speak the descriptive request so user knows to respond
941
- if (currentSession) {
942
- const ttsMessage = `${description} Say yes, no, or always.`;
943
- currentSession.say?.(ttsMessage);
944
- }
953
+ //do not delete!! Leave this section commented out
954
+ // say permission, request permission, ask for permission with session.say
955
+ // if (currentSession) {
956
+ // const ttsMessage = `${description} Say yes, no, or always.`
957
+ // // ;(currentSession as any).say?.(ttsMessage).catch(() => {})
958
+ // ;(currentSession as any).say?.(ttsMessage)
959
+ // }
945
960
  });
946
961
  // Wire up TTS say — bypass LiveKit's BufferedTokenStream, speak directly via session.say()
947
962
  // Each text block from Claude gets spoken immediately as it arrives, no internal buffering
@@ -1046,12 +1061,12 @@ async function main() {
1046
1061
  currentLLM = realtimeClaudeHandler;
1047
1062
  // For resumed sessions, eagerly create workspace (we know the real ID)
1048
1063
  if (resumeSessionId) {
1049
- const workspace = ensureSessionWorkspace(sessionBaseDir, resumeSessionId);
1064
+ const workspace = ensureSessionWorkspace(workingDir, resumeSessionId);
1050
1065
  console.log(`📁 Session workspace (resumed): ${workspace}`);
1051
1066
  }
1052
1067
  // For new sessions, create workspace when SDK assigns real session ID
1053
1068
  realtimeClaudeHandler.events.once('session_id', ({ sessionId }) => {
1054
- const workspace = ensureSessionWorkspace(sessionBaseDir, sessionId);
1069
+ const workspace = ensureSessionWorkspace(workingDir, sessionId);
1055
1070
  console.log(`📁 Session workspace created: ${workspace}`);
1056
1071
  });
1057
1072
  // Wire up MCP server changes to frontend
@@ -1074,7 +1089,7 @@ async function main() {
1074
1089
  // Detect research artifact writes (session workspace or legacy research dir)
1075
1090
  if ((data.name === 'Write' || data.name === 'Edit') && data.input?.file_path) {
1076
1091
  const fp = data.input.file_path;
1077
- if (fp.includes('.osborn/sessions/') || fp.includes('.osborn/research/')) {
1092
+ if (fp.includes('/osb/') || fp.includes('.osborn/sessions/') || fp.includes('.osborn/research/')) {
1078
1093
  sendToFrontend({
1079
1094
  type: 'research_artifact_updated',
1080
1095
  filePath: fp,
@@ -1157,7 +1172,7 @@ async function main() {
1157
1172
  // Fire-and-forget: write user question to spec.md BEFORE agent starts
1158
1173
  const questionSid = currentLLM?.sessionId || resumeSessionId;
1159
1174
  if (questionSid) {
1160
- writeQuestionToSpec(sessionBaseDir, questionSid, task).catch(err => console.error('❌ writeQuestionToSpec failed:', err));
1175
+ writeQuestionToSpec(workingDir, questionSid, task).catch(err => console.error('❌ writeQuestionToSpec failed:', err));
1161
1176
  }
1162
1177
  // Clean up previous research UI tracking — but let the SDK query complete in background.
1163
1178
  // The SDK has an internal queue: new query() calls enqueue behind running ones.
@@ -1229,7 +1244,7 @@ async function main() {
1229
1244
  if (resultText.length > ANSWER_CHECK_THRESHOLD) {
1230
1245
  const sid = currentLLM?.sessionId || resumeSessionId;
1231
1246
  if (sid)
1232
- checkOutputAgainstQuestions(sessionBaseDir, sid, resultText, 'tool_result').catch(() => { });
1247
+ checkOutputAgainstQuestions(workingDir, sid, resultText, 'tool_result').catch(() => { });
1233
1248
  }
1234
1249
  // When AskUserQuestion completes, the user's answer is a decision — track it in spec
1235
1250
  if (data.name === 'AskUserQuestion' && data.response) {
@@ -1238,7 +1253,7 @@ async function main() {
1238
1253
  const questionText = JSON.stringify(data.input?.questions || data.input || {});
1239
1254
  const answerText = typeof data.response === 'string' ? data.response : JSON.stringify(data.response);
1240
1255
  const specUpdate = `User answered a clarifying question during research.\nQuestion: ${questionText}\nAnswer: ${answerText}\nRecord this as a user decision in spec.md.`;
1241
- askHaiku(workingDir, sid, specUpdate, undefined, undefined, undefined, sessionBaseDir).catch(err => console.error('❌ Failed to record AskUserQuestion answer in spec:', err));
1256
+ askHaiku(workingDir, sid, specUpdate, undefined, undefined, undefined, workingDir).catch(err => console.error('❌ Failed to record AskUserQuestion answer in spec:', err));
1242
1257
  console.log(`📝 AskUserQuestion answer forwarded to fast brain for spec tracking`);
1243
1258
  }
1244
1259
  }
@@ -1255,7 +1270,7 @@ async function main() {
1255
1270
  if (text.length > ANSWER_CHECK_THRESHOLD) {
1256
1271
  const sid = currentLLM?.sessionId || resumeSessionId;
1257
1272
  if (sid)
1258
- checkOutputAgainstQuestions(sessionBaseDir, sid, text, 'assistant_text').catch(() => { });
1273
+ checkOutputAgainstQuestions(workingDir, sid, text, 'assistant_text').catch(() => { });
1259
1274
  }
1260
1275
  }
1261
1276
  };
@@ -1368,7 +1383,7 @@ async function main() {
1368
1383
  sendToFrontend({ type: 'assistant_response', text });
1369
1384
  };
1370
1385
  if (voiceSid) {
1371
- processResearchCompletion(workingDir, voiceSid, task, result, chatHistory, completionSendToChat, sessionBaseDir)
1386
+ processResearchCompletion(workingDir, voiceSid, task, result, chatHistory, completionSendToChat, workingDir)
1372
1387
  .then(script => {
1373
1388
  queueVoiceInjection(getScriptInjection(script));
1374
1389
  })
@@ -1384,28 +1399,18 @@ async function main() {
1384
1399
  // Reads FULL untruncated data from JSONL — no content buffer, no truncation
1385
1400
  const postResearchSessionId = currentLLM?.sessionId || resumeSessionId;
1386
1401
  if (postResearchSessionId) {
1387
- updateSpecFromJSONL(workingDir, postResearchSessionId, task, researchLog, sessionBaseDir)
1402
+ updateSpecFromJSONL(workingDir, postResearchSessionId, task, researchLog, workingDir)
1388
1403
  .then(updateResult => {
1389
1404
  if (!updateResult)
1390
1405
  return;
1391
1406
  // Notify frontend about spec.md update
1392
1407
  if (updateResult.spec) {
1393
- const specPath = `${sessionBaseDir}/.osborn/sessions/${postResearchSessionId}/spec.md`;
1408
+ const specPath = join(getSessionWorkspace(workingDir, postResearchSessionId), 'spec.md');
1394
1409
  sendToFrontend({
1395
1410
  type: 'research_artifact_updated',
1396
1411
  filePath: specPath,
1397
1412
  fileName: 'spec.md',
1398
1413
  });
1399
- // Voice model is a teleprompter — fast brain reads spec directly, no ChatCtx injection needed
1400
- }
1401
- // Notify frontend about each library file written by the fast brain
1402
- for (const libFile of updateResult.libraryFiles) {
1403
- const libPath = `${sessionBaseDir}/.osborn/sessions/${postResearchSessionId}/library/${libFile}`;
1404
- sendToFrontend({
1405
- type: 'research_artifact_updated',
1406
- filePath: libPath,
1407
- fileName: libFile,
1408
- });
1409
1414
  }
1410
1415
  });
1411
1416
  }
@@ -2091,16 +2096,20 @@ async function main() {
2091
2096
  console.log('💓 Sending agent_ready signal...');
2092
2097
  let readySent = false;
2093
2098
  const provider = sessionVoiceMode === 'realtime' ? realtimeConfig.provider : 'claude';
2094
- // Fetch full session list for startup session browser
2095
- const allSessions = await listSessions(workingDir);
2099
+ // Fetch full session list for startup session browser (all Claude projects)
2100
+ const allSessions = await listAllClaudeSessions(50);
2096
2101
  const recentSessionId = allSessions.length > 0 ? allSessions[0].sessionId : null;
2097
2102
  const hasRecentSession = allSessions.length > 0;
2098
2103
  // Prepare sessions for frontend (up to 50)
2099
2104
  const sessionsForFrontend = allSessions.slice(0, 50).map(s => ({
2100
2105
  sessionId: s.sessionId,
2106
+ projectSlug: s.projectSlug,
2107
+ projectPath: s.projectPath,
2108
+ cwd: s.cwd,
2101
2109
  timestamp: s.timestamp.toISOString(),
2102
2110
  lastMessage: s.lastMessage,
2103
2111
  messageCount: s.messageCount,
2112
+ fileSize: s.fileSize,
2104
2113
  }));
2105
2114
  const sendReady = async () => {
2106
2115
  if (readySent)
@@ -2157,7 +2166,7 @@ async function main() {
2157
2166
  success: true,
2158
2167
  });
2159
2168
  // Send existing workspace artifacts to frontend (session-scoped)
2160
- const preArtifacts = listWorkspaceArtifacts(sessionBaseDir, preSelectedSessionId);
2169
+ const preArtifacts = listWorkspaceArtifacts(workingDir, preSelectedSessionId);
2161
2170
  if (preArtifacts.length > 0) {
2162
2171
  console.log(`📁 Sending ${preArtifacts.length} workspace artifacts to frontend`);
2163
2172
  await sendToFrontend({
@@ -2177,7 +2186,7 @@ async function main() {
2177
2186
  try {
2178
2187
  if (sessionVoiceMode === 'realtime') {
2179
2188
  const historyForScript = conversationHistory.map(e => ({ role: e.role, text: e.content }));
2180
- const script = await prepareBriefingScript(sessionBaseDir, preSelectedSessionId, historyForScript);
2189
+ const script = await prepareBriefingScript(workingDir, preSelectedSessionId, historyForScript);
2181
2190
  await session.generateReply({ instructions: getScriptInjection(script) });
2182
2191
  }
2183
2192
  else {
@@ -2274,30 +2283,51 @@ async function main() {
2274
2283
  }
2275
2284
  }
2276
2285
  else if (data.type === 'user_text' && currentSession) {
2277
- console.log(`📝 Text: "${data.content}"`);
2286
+ // Build message content — include attached files (URLs, text content)
2287
+ let fullContent = String(data.content || '');
2288
+ const files = data.files;
2289
+ if (files && files.length > 0) {
2290
+ for (const f of files) {
2291
+ if (f.url) {
2292
+ fullContent += `\n\n[${f.type === 'image' ? 'Image' : 'File'}: ${f.name}](${f.url})`;
2293
+ }
2294
+ else if (f.type === 'text' && f.content) {
2295
+ fullContent += `\n\n[File: ${f.name}]\n${f.content}`;
2296
+ }
2297
+ else if (f.type === 'image' && f.content) {
2298
+ fullContent += `\n\n[Image attached: ${f.name}]`;
2299
+ }
2300
+ }
2301
+ console.log(`📝 Text + ${files.length} file(s): "${fullContent.substring(0, 100)}"`);
2302
+ }
2303
+ else {
2304
+ console.log(`📝 Text: "${fullContent.substring(0, 100)}"`);
2305
+ }
2278
2306
  // Skip interrupt for Gemini — disrupts state machine (hangs in speaking state)
2279
2307
  if (currentProvider !== 'gemini') {
2280
2308
  currentSession.interrupt();
2281
2309
  }
2282
- await currentSession.generateReply({ userInput: data.content });
2310
+ await currentSession.generateReply({ userInput: fullContent });
2283
2311
  }
2284
2312
  // ============================================================
2285
2313
  // SESSION MANAGEMENT HANDLERS
2286
2314
  // ============================================================
2287
2315
  else if (data.type === 'list_sessions') {
2288
- // List available sessions for this project
2289
- console.log('📋 Listing available sessions...');
2316
+ // List available sessions across all Claude projects
2317
+ console.log('📋 Listing available sessions (all projects)...');
2290
2318
  try {
2291
- // Clean up orphaned metadata entries before listing
2292
- await cleanupOrphanedMetadata(workingDir);
2293
- const sessions = await listSessions(workingDir);
2319
+ const sessions = await listAllClaudeSessions(100);
2294
2320
  await sendToFrontend({
2295
2321
  type: 'sessions_list',
2296
2322
  sessions: sessions.map(s => ({
2297
2323
  sessionId: s.sessionId,
2324
+ projectSlug: s.projectSlug,
2325
+ projectPath: s.projectPath,
2326
+ cwd: s.cwd,
2298
2327
  timestamp: s.timestamp.toISOString(),
2299
2328
  lastMessage: s.lastMessage,
2300
2329
  messageCount: s.messageCount,
2330
+ fileSize: s.fileSize,
2301
2331
  })),
2302
2332
  count: sessions.length,
2303
2333
  });
@@ -2327,7 +2357,7 @@ async function main() {
2327
2357
  success: true,
2328
2358
  });
2329
2359
  // Send existing session artifacts to frontend (session-scoped)
2330
- const artifacts = listWorkspaceArtifacts(sessionBaseDir, sessionId);
2360
+ const artifacts = listWorkspaceArtifacts(workingDir, sessionId);
2331
2361
  if (artifacts.length > 0) {
2332
2362
  console.log(`📁 Sending ${artifacts.length} session artifacts to frontend`);
2333
2363
  await sendToFrontend({
@@ -2366,7 +2396,7 @@ async function main() {
2366
2396
  success: true,
2367
2397
  });
2368
2398
  // Send existing session artifacts to frontend (session-scoped)
2369
- const artifacts = listWorkspaceArtifacts(sessionBaseDir, recentId);
2399
+ const artifacts = listWorkspaceArtifacts(workingDir, recentId);
2370
2400
  if (artifacts.length > 0) {
2371
2401
  console.log(`📁 Sending ${artifacts.length} session artifacts to frontend`);
2372
2402
  await sendToFrontend({
@@ -2386,7 +2416,7 @@ async function main() {
2386
2416
  try {
2387
2417
  if (currentVoiceMode === 'realtime') {
2388
2418
  const historyForScript = conversationHistory.map(e => ({ role: e.role, text: e.content }));
2389
- const script = await prepareBriefingScript(sessionBaseDir, recentId, historyForScript);
2419
+ const script = await prepareBriefingScript(workingDir, recentId, historyForScript);
2390
2420
  await currentSession.generateReply({ instructions: getScriptInjection(script) });
2391
2421
  }
2392
2422
  else {
@@ -2431,7 +2461,7 @@ async function main() {
2431
2461
  conversationHistory,
2432
2462
  });
2433
2463
  // Step 3.5: Send existing session artifacts to frontend (session-scoped)
2434
- const switchArtifacts = listWorkspaceArtifacts(sessionBaseDir, sessionId);
2464
+ const switchArtifacts = listWorkspaceArtifacts(workingDir, sessionId);
2435
2465
  if (switchArtifacts.length > 0) {
2436
2466
  console.log(`📁 Sending ${switchArtifacts.length} session artifacts to frontend`);
2437
2467
  await sendToFrontend({
@@ -2451,7 +2481,7 @@ async function main() {
2451
2481
  try {
2452
2482
  if (currentVoiceMode === 'realtime') {
2453
2483
  const historyForScript = conversationHistory.map(e => ({ role: e.role, text: e.content }));
2454
- const briefingScript = await prepareBriefingScript(sessionBaseDir, sessionId, historyForScript, 'switch');
2484
+ const briefingScript = await prepareBriefingScript(workingDir, sessionId, historyForScript, 'switch');
2455
2485
  queueVoiceInjection(getScriptInjection(briefingScript));
2456
2486
  }
2457
2487
  else {
@@ -2486,7 +2516,7 @@ async function main() {
2486
2516
  else if (data.type === 'get_session_artifacts') {
2487
2517
  const sessionId = data.sessionId;
2488
2518
  if (sessionId) {
2489
- const artifacts = listWorkspaceArtifacts(sessionBaseDir, sessionId);
2519
+ const artifacts = listWorkspaceArtifacts(workingDir, sessionId);
2490
2520
  console.log(`📁 Sending ${artifacts.length} session artifacts for ${sessionId.substring(0, 8)}`);
2491
2521
  await sendToFrontend({
2492
2522
  type: 'session_artifacts',
@@ -2518,7 +2548,7 @@ async function main() {
2518
2548
  }
2519
2549
  else if (data.type === 'get_research_artifact') {
2520
2550
  const filePath = data.filePath;
2521
- if (filePath && (filePath.includes('.osborn/sessions/') || filePath.includes('.osborn/research/'))) {
2551
+ if (filePath && (filePath.includes('/osb/') || filePath.includes('.osborn/sessions/') || filePath.includes('.osborn/research/'))) {
2522
2552
  try {
2523
2553
  const fs = await import('fs');
2524
2554
  const fileName = filePath.split('/').pop() || '';
@@ -2705,7 +2735,7 @@ async function main() {
2705
2735
  success: true,
2706
2736
  });
2707
2737
  // Send existing session artifacts to frontend (session-scoped)
2708
- const gateArtifacts = listWorkspaceArtifacts(sessionBaseDir, sessionId);
2738
+ const gateArtifacts = listWorkspaceArtifacts(workingDir, sessionId);
2709
2739
  if (gateArtifacts.length > 0) {
2710
2740
  console.log(`📁 Sending ${gateArtifacts.length} session artifacts to frontend`);
2711
2741
  await sendToFrontend({
@@ -2725,7 +2755,7 @@ async function main() {
2725
2755
  try {
2726
2756
  if (currentVoiceMode === 'realtime') {
2727
2757
  const historyForScript = conversationHistory.map(e => ({ role: e.role, text: e.content }));
2728
- const briefingScript = await prepareBriefingScript(sessionBaseDir, sessionId, historyForScript, 'resume');
2758
+ const briefingScript = await prepareBriefingScript(workingDir, sessionId, historyForScript, 'resume');
2729
2759
  queueVoiceInjection(getScriptInjection(briefingScript));
2730
2760
  }
2731
2761
  else {