osborn 0.9.13 → 0.9.16
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
|
|
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
|
```
|
package/dist/claude-llm.d.ts
CHANGED
|
@@ -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
|
package/dist/claude-llm.js
CHANGED
|
@@ -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
|
|
@@ -762,6 +808,7 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
762
808
|
// model: this.#opts.model || 'haiku', // haiku for speed with limited tools, sonnet for full research capabilities (including tool use trace in response)
|
|
763
809
|
model: this.#opts.model || 'claude-sonnet-4-6', // Sonnet orchestrator with named sub-agents (Haiku tested but ignored delegation rules)
|
|
764
810
|
enableFileCheckpointing: true,
|
|
811
|
+
settingSources: ['project', 'user'],
|
|
765
812
|
extraArgs: { 'replay-user-messages': null },
|
|
766
813
|
...(this.#abortController && { abortController: this.#abortController }),
|
|
767
814
|
...(resumeSessionId && { resume: resumeSessionId }),
|
|
@@ -773,7 +820,7 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
773
820
|
this.#opts.voiceMode === 'direct'
|
|
774
821
|
? getDirectModeResearchPrompt(workspacePath)
|
|
775
822
|
: getResearchSystemPrompt(workspacePath),
|
|
776
|
-
|
|
823
|
+
loadAllSkills(this.#opts.sessionBaseDir || this.#opts.workingDirectory || process.cwd()),
|
|
777
824
|
].filter(Boolean).join('\n\n'),
|
|
778
825
|
canUseTool: async (toolName, input, _options) => {
|
|
779
826
|
// Auto-approve writes to session workspace (but block spec.md and library/ — fast brain manages those)
|
|
@@ -943,7 +990,7 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
943
990
|
try {
|
|
944
991
|
const summary = input?.compact_summary || '';
|
|
945
992
|
const { mkdirSync, writeFileSync: writeSyncFs, readFileSync: readSyncFs, existsSync: existsSyncFs } = await import('node:fs');
|
|
946
|
-
const skillDir =
|
|
993
|
+
const skillDir = homedir();
|
|
947
994
|
const today = new Date().toISOString().split('T')[0];
|
|
948
995
|
const sessionId = this.#sessionId || 'unknown';
|
|
949
996
|
let skillsWritten = 0;
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,8 @@ import { dirname, join } from 'node:path';
|
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
16
|
import { spawn } from 'node:child_process';
|
|
17
17
|
import { homedir, tmpdir } from 'node:os';
|
|
18
|
+
import { PassThrough } from 'node:stream';
|
|
19
|
+
import { createGunzip } from 'node:zlib';
|
|
18
20
|
// Resolve __dirname for this ESM module so we can find sibling files (e.g.
|
|
19
21
|
// meeting-output.html) relative to the compiled JS location, NOT process.cwd().
|
|
20
22
|
// In production cwd is the user's workspace; the static file lives next to dist/index.js.
|
|
@@ -376,8 +378,26 @@ function startApiServer(workingDir, port) {
|
|
|
376
378
|
const targetWorkDir = url.searchParams.get('targetWorkDir');
|
|
377
379
|
const tmpDir = mkdtempSync(join(tmpdir(), 'osborn-import-'));
|
|
378
380
|
const tarProc = spawn('tar', ['-xf', '-', '-C', tmpDir]);
|
|
379
|
-
//
|
|
380
|
-
|
|
381
|
+
// Stream-sniff the first chunk to detect gzip magic bytes (0x1f 0x8b).
|
|
382
|
+
// Then route through createGunzip() if gzip, otherwise pipe raw to tar.
|
|
383
|
+
// This avoids any reliance on Content-Type or Content-Encoding headers.
|
|
384
|
+
const passthrough = new PassThrough();
|
|
385
|
+
let sniffDone = false;
|
|
386
|
+
req.once('data', (firstChunk) => {
|
|
387
|
+
sniffDone = true;
|
|
388
|
+
const isGzip = firstChunk[0] === 0x1f && firstChunk[1] === 0x8b;
|
|
389
|
+
passthrough.write(firstChunk);
|
|
390
|
+
req.pipe(passthrough);
|
|
391
|
+
const source = isGzip ? passthrough.pipe(createGunzip()) : passthrough;
|
|
392
|
+
source.pipe(tarProc.stdin);
|
|
393
|
+
});
|
|
394
|
+
req.once('end', () => {
|
|
395
|
+
if (!sniffDone) {
|
|
396
|
+
// Empty body — just end tar stdin
|
|
397
|
+
passthrough.end();
|
|
398
|
+
tarProc.stdin.end();
|
|
399
|
+
}
|
|
400
|
+
});
|
|
381
401
|
tarProc.stdin.on('error', (err) => {
|
|
382
402
|
console.error('[import] tar stdin error', err);
|
|
383
403
|
tarProc.kill('SIGTERM');
|
|
@@ -437,6 +457,10 @@ function startApiServer(workingDir, port) {
|
|
|
437
457
|
}
|
|
438
458
|
}
|
|
439
459
|
filesWritten++;
|
|
460
|
+
const recoveredPath = effectiveSlug.replace(/^-/, '/').replace(/--/g, '/.').replace(/-/g, '/');
|
|
461
|
+
if (recoveredPath && recoveredPath !== '/') {
|
|
462
|
+
mkdirSync(recoveredPath, { recursive: true });
|
|
463
|
+
}
|
|
440
464
|
}
|
|
441
465
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
442
466
|
res.end(JSON.stringify({ ok: true, filesWritten, remapped }));
|
|
@@ -522,22 +546,24 @@ function startApiServer(workingDir, port) {
|
|
|
522
546
|
}
|
|
523
547
|
const tmpExtractDir = mkdtempSync(join(tmpdir(), 'osborn-import-'));
|
|
524
548
|
try {
|
|
525
|
-
// Reassemble chunks into a
|
|
549
|
+
// Reassemble all chunks into a combined buffer, then sniff first 2 bytes
|
|
550
|
+
// to detect gzip magic (0x1f 0x8b). Route through createGunzip() if gzip,
|
|
551
|
+
// otherwise pass raw bytes — always using tar -xf (no -z flag).
|
|
552
|
+
const chunkBuffers = [];
|
|
553
|
+
for (const chunkFile of expectedChunks) {
|
|
554
|
+
chunkBuffers.push(readFileSync(join(uploadDir, chunkFile)));
|
|
555
|
+
}
|
|
556
|
+
const combined = Buffer.concat(chunkBuffers);
|
|
557
|
+
const isGzip = combined[0] === 0x1f && combined[1] === 0x8b;
|
|
526
558
|
const tarProc = spawn('tar', ['-xf', '-', '-C', tmpExtractDir]);
|
|
527
|
-
//
|
|
559
|
+
// Feed combined buffer through gunzip (if needed) then into tar stdin
|
|
560
|
+
const feedStream = new PassThrough();
|
|
561
|
+
const tarInput = isGzip ? feedStream.pipe(createGunzip()) : feedStream;
|
|
562
|
+
tarInput.pipe(tarProc.stdin);
|
|
563
|
+
feedStream.end(combined);
|
|
528
564
|
const streamChunks = async () => {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
await new Promise((resolve, reject) => {
|
|
532
|
-
tarProc.stdin.write(chunkData, (err) => {
|
|
533
|
-
if (err)
|
|
534
|
-
reject(err);
|
|
535
|
-
else
|
|
536
|
-
resolve();
|
|
537
|
-
});
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
tarProc.stdin.end();
|
|
565
|
+
// feeding is already initiated above; just return a resolved promise
|
|
566
|
+
await Promise.resolve();
|
|
541
567
|
};
|
|
542
568
|
streamChunks().catch(err => {
|
|
543
569
|
console.error('[import-finalize] chunk stream error', err);
|
|
@@ -557,24 +583,11 @@ function startApiServer(workingDir, port) {
|
|
|
557
583
|
// The archive should contain a 'projects' subdirectory
|
|
558
584
|
const extractedProjects = join(tmpExtractDir, 'projects');
|
|
559
585
|
const sourceDir = existsSync(extractedProjects) ? extractedProjects : tmpExtractDir;
|
|
560
|
-
// Optionally remap slug: if targetWorkDir is provided, find slug(s)
|
|
561
|
-
// that don't match the target and rename them
|
|
562
|
-
const remapped = {};
|
|
563
|
-
if (targetWorkDir) {
|
|
564
|
-
const targetSlug = targetWorkDir.replace(/\//g, '-');
|
|
565
|
-
const sourceSlugs = readdirSync(sourceDir);
|
|
566
|
-
for (const slug of sourceSlugs) {
|
|
567
|
-
if (slug !== targetSlug && !slug.startsWith('.')) {
|
|
568
|
-
remapped[slug] = targetSlug;
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
586
|
// Copy subdirectories into ~/.claude/projects/, merging and updating existing files
|
|
573
587
|
let filesWritten = 0;
|
|
574
588
|
const slugsInSource = readdirSync(sourceDir);
|
|
575
589
|
for (const slug of slugsInSource) {
|
|
576
|
-
const
|
|
577
|
-
const destSlug = join(projectsDir, effectiveSlug);
|
|
590
|
+
const destSlug = join(projectsDir, slug);
|
|
578
591
|
mkdirSync(destSlug, { recursive: true });
|
|
579
592
|
try {
|
|
580
593
|
renameSync(join(sourceDir, slug), destSlug);
|
|
@@ -588,10 +601,15 @@ function startApiServer(workingDir, port) {
|
|
|
588
601
|
throw e;
|
|
589
602
|
}
|
|
590
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
|
+
}
|
|
591
609
|
filesWritten++;
|
|
592
610
|
}
|
|
593
611
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
594
|
-
res.end(JSON.stringify({ ok: true, filesWritten
|
|
612
|
+
res.end(JSON.stringify({ ok: true, filesWritten }));
|
|
595
613
|
}
|
|
596
614
|
catch (err) {
|
|
597
615
|
console.error('[import-finalize] merge error:', err);
|
|
@@ -2971,13 +2989,59 @@ async function main() {
|
|
|
2971
2989
|
}
|
|
2972
2990
|
}
|
|
2973
2991
|
else {
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
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
|
+
}
|
|
2981
3045
|
}
|
|
2982
3046
|
}
|
|
2983
3047
|
else if (data.type === 'continue_session' && currentLLM) {
|