osborn 0.9.15 → 0.9.17

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.
@@ -3,7 +3,7 @@
3
3
  Export Markdown documents as formatted PDF files.
4
4
 
5
5
  ## When to use
6
- When the user wants to create a PDF from a Markdown file, spec, or research findings.
6
+ When the user /wants to create a PDF from a Markdown file, spec, or research findings.
7
7
 
8
8
  ## How to execute
9
9
 
@@ -3,7 +3,7 @@
3
3
  Automate web browser interactions — navigate pages, click buttons, fill forms, take screenshots, and extract content.
4
4
 
5
5
  ## When to use
6
- - Navigate to a URL and interact with it
6
+ - Navigate to a - URL and interact with it
7
7
  - Click buttons or links by their text or role
8
8
  - Fill form fields and submit data
9
9
  - Take screenshots of web pages
@@ -7,7 +7,7 @@ When the user wants to add UI components (buttons, dialogs, cards, forms, tables
7
7
 
8
8
  ## Setup (first time only)
9
9
 
10
- Initialize shadcn in the project root (where package.json lives):
10
+ Initialize shadcn in the work/project root (where package.json lives):
11
11
  ```bash
12
12
  npx shadcn@latest init
13
13
  ```
@@ -78,6 +78,11 @@ export declare class ClaudeLLM extends llm.LLM {
78
78
  * Call this before sending the first message to resume from a previous session
79
79
  */
80
80
  setResumeSessionId(sessionId: string | null): void;
81
+ /**
82
+ * Set the working directory for the current session
83
+ * Call this when resuming a session from a different project slug
84
+ */
85
+ setWorkingDirectory(path: string): void;
81
86
  /**
82
87
  * Reset state for mid-conversation session switch
83
88
  * Clears pending permissions and resets conversation tracking
@@ -14,6 +14,7 @@ import { getResearchSystemPrompt, getDirectModeResearchPrompt } from './prompts.
14
14
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
15
15
  import { join, dirname } from 'node:path';
16
16
  import { fileURLToPath } from 'node:url';
17
+ import { homedir } from 'node:os';
17
18
  // Directory of this module — used to locate co-located prompt files (e.g., turn-shape reminder).
18
19
  const __claudeLlmDir = dirname(fileURLToPath(import.meta.url));
19
20
  const TURN_SHAPE_REMINDER_PATH = join(__claudeLlmDir, 'prompts', 'turn-shape-reminder.md');
@@ -85,6 +86,44 @@ function loadSkillsFromDir(agentDir) {
85
86
  console.log(`📚 Loaded ${skills.length} skill(s) from ${skillsDir}`);
86
87
  return `<available-skills>\n${skills.join('\n\n---\n\n')}\n</available-skills>`;
87
88
  }
89
+ /**
90
+ * Loads skills from both ~/.claude/skills/ (home dir) and {workingDir}/.claude/skills/ (project dir).
91
+ * Merges results, deduplicating by skill directory name — home dir wins on conflicts.
92
+ * Returns a combined <available-skills> XML block, or '' if no skills found.
93
+ */
94
+ function loadAllSkills(workingDir) {
95
+ const homeSkillsDir = join(homedir(), '.claude', 'skills');
96
+ const projectSkillsDir = join(workingDir, '.claude', 'skills');
97
+ // skill name → content; home dir loaded first so it wins on conflicts
98
+ const skillMap = new Map();
99
+ const loadFromDir = (dir) => {
100
+ if (!existsSync(dir))
101
+ return;
102
+ try {
103
+ for (const skillName of readdirSync(dir)) {
104
+ if (skillMap.has(skillName))
105
+ continue; // home dir already set this one
106
+ const skillFile = join(dir, skillName, 'SKILL.md');
107
+ if (existsSync(skillFile)) {
108
+ skillMap.set(skillName, readFileSync(skillFile, 'utf-8').trim());
109
+ }
110
+ }
111
+ }
112
+ catch (err) {
113
+ console.warn('⚠️ Failed to load skills from', dir, ':', err);
114
+ }
115
+ };
116
+ loadFromDir(homeSkillsDir);
117
+ loadFromDir(projectSkillsDir);
118
+ if (skillMap.size === 0)
119
+ return '';
120
+ const sources = [
121
+ existsSync(homeSkillsDir) ? homeSkillsDir : null,
122
+ existsSync(projectSkillsDir) ? projectSkillsDir : null,
123
+ ].filter(Boolean).join(', ');
124
+ console.log(`📚 Loaded ${skillMap.size} skill(s) from ${sources}`);
125
+ return `<available-skills>\n${[...skillMap.values()].join('\n\n---\n\n')}\n</available-skills>`;
126
+ }
88
127
  // Research mode tools — full research capabilities
89
128
  const RESEARCH_TOOLS = [
90
129
  'Read', 'Write', 'Edit', 'Glob', 'Grep',
@@ -301,6 +340,13 @@ export class ClaudeLLM extends llm.LLM {
301
340
  console.log(`🔄 Will resume session: ${sessionId}`);
302
341
  }
303
342
  }
343
+ /**
344
+ * Set the working directory for the current session
345
+ * Call this when resuming a session from a different project slug
346
+ */
347
+ setWorkingDirectory(path) {
348
+ this.#opts.workingDirectory = path;
349
+ }
304
350
  /**
305
351
  * Reset state for mid-conversation session switch
306
352
  * Clears pending permissions and resets conversation tracking
@@ -547,8 +593,11 @@ export class ClaudeLLM extends llm.LLM {
547
593
  * @param callbacks - Event callbacks for the background consumer
548
594
  */
549
595
  pushMessage(userText, sdkOptions, callbacks) {
550
- // Lower compaction threshold to 65% so PreCompact fires earlier and context is preserved
551
- process.env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE = '75';
596
+ // Auto-compact threshold fires PreCompact when context fills to this %.
597
+ // Higher = uses more of the window before compacting (more context retained
598
+ // per turn, but less headroom for the next reply). 85% is the sweet spot
599
+ // before Claude starts hard-capping replies near the limit.
600
+ process.env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE = '85';
552
601
  const userMessage = {
553
602
  type: 'user',
554
603
  message: { role: 'user', content: [{ type: 'text', text: userText }] },
@@ -762,6 +811,7 @@ class ClaudeLLMStream extends llm.LLMStream {
762
811
  // model: this.#opts.model || 'haiku', // haiku for speed with limited tools, sonnet for full research capabilities (including tool use trace in response)
763
812
  model: this.#opts.model || 'claude-sonnet-4-6', // Sonnet orchestrator with named sub-agents (Haiku tested but ignored delegation rules)
764
813
  enableFileCheckpointing: true,
814
+ settingSources: ['project', 'user'],
765
815
  extraArgs: { 'replay-user-messages': null },
766
816
  ...(this.#abortController && { abortController: this.#abortController }),
767
817
  ...(resumeSessionId && { resume: resumeSessionId }),
@@ -773,7 +823,7 @@ class ClaudeLLMStream extends llm.LLMStream {
773
823
  this.#opts.voiceMode === 'direct'
774
824
  ? getDirectModeResearchPrompt(workspacePath)
775
825
  : getResearchSystemPrompt(workspacePath),
776
- loadSkillsFromDir(this.#opts.sessionBaseDir || this.#opts.workingDirectory || process.cwd()),
826
+ loadAllSkills(this.#opts.sessionBaseDir || this.#opts.workingDirectory || process.cwd()),
777
827
  ].filter(Boolean).join('\n\n'),
778
828
  canUseTool: async (toolName, input, _options) => {
779
829
  // Auto-approve writes to session workspace (but block spec.md and library/ — fast brain manages those)
@@ -943,7 +993,7 @@ class ClaudeLLMStream extends llm.LLMStream {
943
993
  try {
944
994
  const summary = input?.compact_summary || '';
945
995
  const { mkdirSync, writeFileSync: writeSyncFs, readFileSync: readSyncFs, existsSync: existsSyncFs } = await import('node:fs');
946
- const skillDir = this.#opts.sessionBaseDir || this.#opts.workingDirectory || process.cwd();
996
+ const skillDir = homedir();
947
997
  const today = new Date().toISOString().split('T')[0];
948
998
  const sessionId = this.#sessionId || 'unknown';
949
999
  let skillsWritten = 0;
package/dist/index.js CHANGED
@@ -457,6 +457,10 @@ function startApiServer(workingDir, port) {
457
457
  }
458
458
  }
459
459
  filesWritten++;
460
+ const recoveredPath = effectiveSlug.replace(/^-/, '/').replace(/--/g, '/.').replace(/-/g, '/');
461
+ if (recoveredPath && recoveredPath !== '/') {
462
+ mkdirSync(recoveredPath, { recursive: true });
463
+ }
460
464
  }
461
465
  res.writeHead(200, { 'Content-Type': 'application/json' });
462
466
  res.end(JSON.stringify({ ok: true, filesWritten, remapped }));
@@ -579,24 +583,11 @@ function startApiServer(workingDir, port) {
579
583
  // The archive should contain a 'projects' subdirectory
580
584
  const extractedProjects = join(tmpExtractDir, 'projects');
581
585
  const sourceDir = existsSync(extractedProjects) ? extractedProjects : tmpExtractDir;
582
- // Optionally remap slug: if targetWorkDir is provided, find slug(s)
583
- // that don't match the target and rename them
584
- const remapped = {};
585
- if (targetWorkDir) {
586
- const targetSlug = targetWorkDir.replace(/\//g, '-');
587
- const sourceSlugs = readdirSync(sourceDir);
588
- for (const slug of sourceSlugs) {
589
- if (slug !== targetSlug && !slug.startsWith('.')) {
590
- remapped[slug] = targetSlug;
591
- }
592
- }
593
- }
594
586
  // Copy subdirectories into ~/.claude/projects/, merging and updating existing files
595
587
  let filesWritten = 0;
596
588
  const slugsInSource = readdirSync(sourceDir);
597
589
  for (const slug of slugsInSource) {
598
- const effectiveSlug = remapped[slug] ?? slug;
599
- const destSlug = join(projectsDir, effectiveSlug);
590
+ const destSlug = join(projectsDir, slug);
600
591
  mkdirSync(destSlug, { recursive: true });
601
592
  try {
602
593
  renameSync(join(sourceDir, slug), destSlug);
@@ -610,10 +601,15 @@ function startApiServer(workingDir, port) {
610
601
  throw e;
611
602
  }
612
603
  }
604
+ // Also create the corresponding workspace directory so Claude can resume
605
+ const recoveredPath = slug.replace(/^-/, '/').replace(/--/g, '/.').replace(/-/g, '/');
606
+ if (recoveredPath && recoveredPath !== '/') {
607
+ mkdirSync(recoveredPath, { recursive: true });
608
+ }
613
609
  filesWritten++;
614
610
  }
615
611
  res.writeHead(200, { 'Content-Type': 'application/json' });
616
- res.end(JSON.stringify({ ok: true, filesWritten, remapped }));
612
+ res.end(JSON.stringify({ ok: true, filesWritten }));
617
613
  }
618
614
  catch (err) {
619
615
  console.error('[import-finalize] merge error:', err);
@@ -2993,13 +2989,59 @@ async function main() {
2993
2989
  }
2994
2990
  }
2995
2991
  else {
2996
- console.error(`❌ Session not found: ${sessionId}`);
2997
- await sendToFrontend({
2998
- type: 'session_resume_set',
2999
- sessionId,
3000
- success: false,
3001
- error: 'Session not found',
3002
- });
2992
+ // Try to find the session in any slug directory
2993
+ let found = false;
2994
+ const projectsDir = join(homedir(), '.claude', 'projects');
2995
+ if (existsSync(projectsDir)) {
2996
+ const slugDirs = readdirSync(projectsDir);
2997
+ for (const slug of slugDirs) {
2998
+ const candidate = join(projectsDir, slug, `${sessionId}.jsonl`);
2999
+ if (existsSync(candidate)) {
3000
+ // Recover the original path from the slug
3001
+ const recoveredPath = slug.replace(/^-/, '/').replace(/--/g, '/.').replace(/-/g, '/');
3002
+ if (recoveredPath && recoveredPath !== '/') {
3003
+ mkdirSync(recoveredPath, { recursive: true });
3004
+ workingDir = recoveredPath;
3005
+ currentLLM.setWorkingDirectory(recoveredPath);
3006
+ console.log(`🔄 Found session in slug ${slug}, using path: ${recoveredPath}`);
3007
+ found = true;
3008
+ // Proceed with the same success path
3009
+ currentLLM.setResumeSessionId(sessionId);
3010
+ currentResumeSessionId = sessionId;
3011
+ console.log(`🔄 Will resume session: ${sessionId}`);
3012
+ await sendToFrontend({
3013
+ type: 'session_resume_set',
3014
+ sessionId,
3015
+ success: true,
3016
+ });
3017
+ const artifacts = listWorkspaceArtifacts(workingDir, sessionId);
3018
+ if (artifacts.length > 0) {
3019
+ console.log(`📁 Sending ${artifacts.length} session artifacts to frontend`);
3020
+ await sendToFrontend({
3021
+ type: 'session_artifacts',
3022
+ sessionId,
3023
+ artifacts: artifacts.map(a => ({
3024
+ filePath: a.filePath,
3025
+ fileName: a.fileName,
3026
+ type: a.type,
3027
+ updatedAt: a.updatedAt,
3028
+ }))
3029
+ });
3030
+ }
3031
+ break;
3032
+ }
3033
+ }
3034
+ }
3035
+ }
3036
+ if (!found) {
3037
+ console.error(`❌ Session not found: ${sessionId}`);
3038
+ await sendToFrontend({
3039
+ type: 'session_resume_set',
3040
+ sessionId,
3041
+ success: false,
3042
+ error: 'Session not found',
3043
+ });
3044
+ }
3003
3045
  }
3004
3046
  }
3005
3047
  else if (data.type === 'continue_session' && currentLLM) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.9.15",
3
+ "version": "0.9.17",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {