osborn 0.8.6 → 0.8.8
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/dist/claude-auth.js +40 -0
- package/dist/config.js +65 -13
- package/dist/index.js +19 -3
- package/package.json +3 -1
- package/scripts/dev-logged.ts +81 -0
- package/scripts/review.ts +425 -0
- package/.claude/settings.local.json +0 -9
- package/.claude/skills/markdown-to-pdf/SKILL.md +0 -29
- package/.claude/skills/pdf-to-markdown/SKILL.md +0 -28
- package/.claude/skills/playwright-browser/SKILL.md +0 -90
- package/.claude/skills/shadcn/SKILL.md +0 -232
- package/.claude/skills/shadcn/image.png +0 -0
- package/.claude/skills/youtube-transcript/SKILL.md +0 -24
- package/dist/conversation-brain.d.ts +0 -92
- package/dist/conversation-brain.js +0 -360
- package/dist/fast-llm.d.ts +0 -15
- package/dist/fast-llm.js +0 -81
package/dist/claude-auth.js
CHANGED
|
@@ -150,6 +150,13 @@ export async function checkClaudeAuthStatus() {
|
|
|
150
150
|
* Strips ALL whitespace first (like vutran1710/claudebox) to handle
|
|
151
151
|
* Ink UI wrapping the URL across multiple lines.
|
|
152
152
|
* Also cleans trailing "Pastecodehereifprompted" that Ink appends.
|
|
153
|
+
*
|
|
154
|
+
* IMPORTANT: strips the `redirect_uri` query parameter (which points to a
|
|
155
|
+
* localhost callback server on the *sprite*, not the user's machine). With
|
|
156
|
+
* no redirect_uri, claude.com falls back to showing the auth code in-page,
|
|
157
|
+
* which the user pastes back into the modal. This is the only flow that
|
|
158
|
+
* works for cloud sandboxes — the localhost redirect breaks both on phones
|
|
159
|
+
* (no listener) AND on desktops (sprite's localhost is unreachable).
|
|
153
160
|
*/
|
|
154
161
|
function extractOAuthUrl(text) {
|
|
155
162
|
// Strip ANSI codes
|
|
@@ -168,6 +175,39 @@ function extractOAuthUrl(text) {
|
|
|
168
175
|
if (idx > 0)
|
|
169
176
|
url = url.substring(0, idx);
|
|
170
177
|
}
|
|
178
|
+
// Strip the localhost redirect_uri so claude.com shows a pasteable code
|
|
179
|
+
// instead of trying to redirect. URL() can't be used here because it
|
|
180
|
+
// re-encodes the path, so we surgically delete the redirect_uri param.
|
|
181
|
+
url = stripRedirectUri(url);
|
|
182
|
+
return url;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Strip the `redirect_uri` query param from an OAuth URL.
|
|
186
|
+
*
|
|
187
|
+
* Background: `claude setup-token` spawns a one-shot localhost HTTP server on
|
|
188
|
+
* a random port and registers it as the redirect_uri. That works fine when the
|
|
189
|
+
* user is on the same machine as the CLI, but on a sprite the URL points to
|
|
190
|
+
* the *sprite's* localhost — unreachable from the user's browser regardless
|
|
191
|
+
* of whether they open the auth link on their PC or their phone. With no
|
|
192
|
+
* redirect_uri at all, claude.ai falls back to its in-page code display
|
|
193
|
+
* (the same flow that `claude setup-token`'s "Paste code here if prompted"
|
|
194
|
+
* Ink input is built to consume), and the user can paste the code back into
|
|
195
|
+
* our modal — which works whether they signed in on phone or desktop.
|
|
196
|
+
*
|
|
197
|
+
* Done with regex rather than `new URL()` because the URL constructor
|
|
198
|
+
* normalizes the path (which can break Claude's strict redirect check)
|
|
199
|
+
* and re-encodes spaces/special chars in other params.
|
|
200
|
+
*/
|
|
201
|
+
function stripRedirectUri(url) {
|
|
202
|
+
const before = url;
|
|
203
|
+
// Three cases: leading param (?redirect_uri=...&), middle/trailing (&redirect_uri=...),
|
|
204
|
+
// and only param (?redirect_uri=...). Order matters so cleanup leaves the URL well-formed.
|
|
205
|
+
url = url.replace(/&redirect_uri=[^&]*/g, '');
|
|
206
|
+
url = url.replace(/\?redirect_uri=[^&]*&/g, '?');
|
|
207
|
+
url = url.replace(/\?redirect_uri=[^&]*$/g, '');
|
|
208
|
+
if (before !== url) {
|
|
209
|
+
console.log('🔑 Stripped localhost redirect_uri from OAuth URL — claude.ai will show a pasteable code instead of redirecting');
|
|
210
|
+
}
|
|
171
211
|
return url;
|
|
172
212
|
}
|
|
173
213
|
// ─────────────────────────────────────────
|
package/dist/config.js
CHANGED
|
@@ -37,8 +37,13 @@ export const MCP_CATALOG = [
|
|
|
37
37
|
},
|
|
38
38
|
];
|
|
39
39
|
// Default config template
|
|
40
|
+
// Note: workingDirectory is intentionally OMITTED here. Baking process.cwd() into the
|
|
41
|
+
// default config at module-load time freezes whatever directory osborn happened to be
|
|
42
|
+
// invoked from on first boot — which on cloud sandboxes can be the npm install dir
|
|
43
|
+
// (`/usr/local/nvm/.../osborn`) and gets persisted to ~/.osborn/config.yaml forever.
|
|
44
|
+
// Leaving it undefined lets the runtime self-heal in index.ts resolve it on every boot
|
|
45
|
+
// from OSBORN_CWD → process.cwd() at the actual time the agent starts.
|
|
40
46
|
const DEFAULT_CONFIG = {
|
|
41
|
-
workingDirectory: process.cwd(),
|
|
42
47
|
defaultProvider: 'gemini',
|
|
43
48
|
defaultCodingAgent: 'claude',
|
|
44
49
|
// Voice mode: 'direct' (Claude Agent SDK) or 'realtime' (OpenAI/Gemini native)
|
|
@@ -504,36 +509,83 @@ export function sessionExists(sessionId, projectPath) {
|
|
|
504
509
|
return existsSync(sessionFile);
|
|
505
510
|
}
|
|
506
511
|
/**
|
|
507
|
-
* Reverse a project slug back to a path
|
|
508
|
-
*
|
|
512
|
+
* Reverse a project slug back to a path — LAST-RESORT fallback only.
|
|
513
|
+
*
|
|
514
|
+
* Claude's slug encoding (`/` → `-`, `.` → `-`) is LOSSY: you can't tell from a
|
|
515
|
+
* slug whether a given `-` was originally `/`, `.`, or a literal `-` inside a
|
|
516
|
+
* directory name like `pensive-bohr`. So this function cannot reliably
|
|
517
|
+
* round-trip an arbitrary path.
|
|
518
|
+
*
|
|
519
|
+
* Strategy: produce the naive guess (with a small `--` → `/.` improvement for
|
|
520
|
+
* dot-directories like `.claude`), then VALIDATE it with `existsSync`. If the
|
|
521
|
+
* guess doesn't exist, return empty string — that way the caller knows the
|
|
522
|
+
* reverse failed and can fall back cleanly instead of passing a broken path
|
|
523
|
+
* to `child_process.spawn` and crashing with ENOENT.
|
|
524
|
+
*
|
|
525
|
+
* The primary source of cwd is `extractCwd()` which reads the actual cwd from
|
|
526
|
+
* the JSONL file. This function is only reached when that fails.
|
|
509
527
|
*/
|
|
510
528
|
function slugToPath(slug) {
|
|
511
|
-
|
|
529
|
+
// Naive reverse: leading `-` → `/`, `--` → `/.`, remaining `-` → `/`.
|
|
530
|
+
// The `--` → `/.` pass handles dot-prefixed directories like `.claude`.
|
|
531
|
+
const guess = slug
|
|
532
|
+
.replace(/^-/, '/')
|
|
533
|
+
.replace(/--/g, '/.')
|
|
534
|
+
.replace(/-/g, '/');
|
|
535
|
+
// Validate — lossy encoding means we cannot trust the guess.
|
|
536
|
+
return existsSync(guess) ? guess : '';
|
|
512
537
|
}
|
|
513
538
|
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
539
|
/**
|
|
515
|
-
* Extract cwd from first
|
|
516
|
-
*
|
|
540
|
+
* Extract cwd from the first JSONL entry that carries a `cwd` field.
|
|
541
|
+
*
|
|
542
|
+
* Previously: read only the first 8KB and only accepted `type === 'user'`. That
|
|
543
|
+
* broke for sessions whose first JSONL entry was larger than 8KB — e.g. a
|
|
544
|
+
* `queue-operation` containing pasted email/page text. readline never emits the
|
|
545
|
+
* `line` event for an incomplete final chunk, so the scan finds nothing,
|
|
546
|
+
* `listAllClaudeSessions` falls through to the lossy `slugToPath` reverse, and
|
|
547
|
+
* the mangled path ends up as a `cwd` passed to `child_process.spawn`, producing
|
|
548
|
+
* the misleading "Claude Code executable not found" error (see MEMORY bug #11).
|
|
549
|
+
*
|
|
550
|
+
* Now: stream line-by-line with no byte cap, short-circuit on the first entry
|
|
551
|
+
* with a `cwd` field regardless of `type` (every `user` / `attachment` /
|
|
552
|
+
* `assistant` / `system` entry in a Claude JSONL session carries `cwd`, so the
|
|
553
|
+
* scan finishes in the first few KB of any normal session).
|
|
517
554
|
*/
|
|
518
555
|
async function extractCwd(filePath) {
|
|
519
556
|
return new Promise((resolve) => {
|
|
520
|
-
const fileStream = createReadStream(filePath
|
|
557
|
+
const fileStream = createReadStream(filePath);
|
|
521
558
|
const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
559
|
+
let resolved = false;
|
|
560
|
+
const done = (value) => {
|
|
561
|
+
if (resolved)
|
|
562
|
+
return;
|
|
563
|
+
resolved = true;
|
|
564
|
+
try {
|
|
565
|
+
rl.close();
|
|
566
|
+
}
|
|
567
|
+
catch { }
|
|
568
|
+
try {
|
|
569
|
+
fileStream.destroy();
|
|
570
|
+
}
|
|
571
|
+
catch { }
|
|
572
|
+
resolve(value);
|
|
573
|
+
};
|
|
522
574
|
rl.on('line', (line) => {
|
|
575
|
+
if (resolved)
|
|
576
|
+
return;
|
|
523
577
|
if (!line.trim())
|
|
524
578
|
return;
|
|
525
579
|
try {
|
|
526
580
|
const obj = JSON.parse(line);
|
|
527
|
-
if (obj
|
|
528
|
-
|
|
529
|
-
fileStream.destroy();
|
|
530
|
-
resolve(obj.cwd);
|
|
581
|
+
if (typeof obj?.cwd === 'string' && obj.cwd.length > 0) {
|
|
582
|
+
done(obj.cwd);
|
|
531
583
|
}
|
|
532
584
|
}
|
|
533
585
|
catch { }
|
|
534
586
|
});
|
|
535
|
-
rl.on('close', () =>
|
|
536
|
-
rl.on('error', () =>
|
|
587
|
+
rl.on('close', () => done(''));
|
|
588
|
+
rl.on('error', () => done(''));
|
|
537
589
|
});
|
|
538
590
|
}
|
|
539
591
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1680,10 +1680,26 @@ async function main() {
|
|
|
1680
1680
|
preSelectedSessionId = metadata.sessionId;
|
|
1681
1681
|
console.log(`📂 Pre-selected session from frontend: ${preSelectedSessionId}`);
|
|
1682
1682
|
}
|
|
1683
|
-
// Read working directory override from frontend
|
|
1683
|
+
// Read working directory override from frontend.
|
|
1684
|
+
//
|
|
1685
|
+
// Must validate with existsSync before accepting: a broken reverse-slug in
|
|
1686
|
+
// the frontend's session list (see `slugToPath` in config.ts — the encoding
|
|
1687
|
+
// is lossy), a deleted project, or a bad legacy client can all produce a
|
|
1688
|
+
// non-existent path here. Passing a non-existent cwd to
|
|
1689
|
+
// `child_process.spawn` in the Claude SDK errors with ENOENT, which the
|
|
1690
|
+
// SDK then reports as the misleading "Claude Code executable not found at
|
|
1691
|
+
// .../cli.js" error (see MEMORY bug fix #11). Fall back to defaultWorkingDir
|
|
1692
|
+
// (which is itself existsSync-verified at startup).
|
|
1684
1693
|
if (metadata.workingDirectory && typeof metadata.workingDirectory === 'string' && metadata.workingDirectory.length > 0) {
|
|
1685
|
-
|
|
1686
|
-
|
|
1694
|
+
if (existsSync(metadata.workingDirectory)) {
|
|
1695
|
+
workingDir = metadata.workingDirectory;
|
|
1696
|
+
console.log(`📂 Working directory from frontend: ${workingDir}`);
|
|
1697
|
+
}
|
|
1698
|
+
else {
|
|
1699
|
+
console.log(`⚠️ Frontend sent workingDirectory that does not exist: ${metadata.workingDirectory}`);
|
|
1700
|
+
console.log(` Falling back to default: ${defaultWorkingDir}`);
|
|
1701
|
+
workingDir = defaultWorkingDir;
|
|
1702
|
+
}
|
|
1687
1703
|
}
|
|
1688
1704
|
else {
|
|
1689
1705
|
// Reset to default for new connections (in case previous session changed it)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "osborn",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.8",
|
|
4
4
|
"description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"dev": "tsx src/index.ts",
|
|
11
|
+
"dev:logged": "tsx scripts/dev-logged.ts",
|
|
12
|
+
"review": "tsx scripts/review.ts",
|
|
11
13
|
"start": "tsx src/index.ts",
|
|
12
14
|
"build": "tsc",
|
|
13
15
|
"room": "tsx src/index.ts --room",
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev-logger wrapper: spawns `tsx src/index.ts` as a child, tees its stdout /
|
|
3
|
+
* stderr to BOTH the user's terminal AND a timestamped log file under
|
|
4
|
+
* `.osborn/dev-logs/`. Forwards SIGINT / SIGTERM so Ctrl-C cleanly shuts down
|
|
5
|
+
* the agent child before the wrapper exits.
|
|
6
|
+
*
|
|
7
|
+
* This is an OUT-OF-LOOP process — the agent itself (src/index.ts) is
|
|
8
|
+
* unmodified and unaware of this wrapper. Removing the dev-logger means
|
|
9
|
+
* deleting this file and the `"dev:logged"` script in package.json — zero
|
|
10
|
+
* impact on the agent's runtime behavior.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* npm run dev:logged # capture to .osborn/dev-logs/<ts>.log
|
|
14
|
+
*
|
|
15
|
+
* After shutdown, review with:
|
|
16
|
+
* npm run review
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { spawn } from 'node:child_process'
|
|
20
|
+
import { createWriteStream, mkdirSync } from 'node:fs'
|
|
21
|
+
import { join } from 'node:path'
|
|
22
|
+
|
|
23
|
+
// Invoked via `npm run dev:logged` from `agent/`, so process.cwd() === agent/.
|
|
24
|
+
// Log dir is co-located with the agent install — follows the existing
|
|
25
|
+
// `.osborn/` convention (already matched by the root .gitignore).
|
|
26
|
+
const logDir = join(process.cwd(), '.osborn', 'dev-logs')
|
|
27
|
+
mkdirSync(logDir, { recursive: true })
|
|
28
|
+
|
|
29
|
+
// YYYYMMDDHHMMSS timestamp — sortable, filesystem-safe on every OS.
|
|
30
|
+
const ts = new Date().toISOString().replace(/[-:T.]/g, '').slice(0, 14)
|
|
31
|
+
const logPath = join(logDir, `${ts}.log`)
|
|
32
|
+
const logStream = createWriteStream(logPath, { flags: 'a' })
|
|
33
|
+
|
|
34
|
+
console.log(`📝 [dev-logger] Capturing to ${logPath}`)
|
|
35
|
+
console.log(`📝 [dev-logger] Review later with: npm run review\n`)
|
|
36
|
+
logStream.write(`=== dev-logged session started at ${new Date().toISOString()} ===\n`)
|
|
37
|
+
|
|
38
|
+
// `tsx` resolves to `node_modules/.bin/tsx` because npm run <script> prepends
|
|
39
|
+
// the local node_modules/.bin to PATH. No need to hardcode the path.
|
|
40
|
+
const child = spawn('tsx', ['src/index.ts'], {
|
|
41
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
42
|
+
env: process.env,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
child.stdout.on('data', (chunk: Buffer) => {
|
|
46
|
+
process.stdout.write(chunk)
|
|
47
|
+
logStream.write(chunk)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
child.stderr.on('data', (chunk: Buffer) => {
|
|
51
|
+
process.stderr.write(chunk)
|
|
52
|
+
logStream.write(chunk)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// Forward termination signals exactly once — if the user hits Ctrl-C multiple
|
|
56
|
+
// times, only the first SIGINT goes to the child; subsequent ones are ignored
|
|
57
|
+
// to avoid racing the graceful shutdown.
|
|
58
|
+
let forwarded = false
|
|
59
|
+
const forward = (sig: NodeJS.Signals) => {
|
|
60
|
+
if (forwarded) return
|
|
61
|
+
forwarded = true
|
|
62
|
+
try { child.kill(sig) } catch {}
|
|
63
|
+
}
|
|
64
|
+
process.on('SIGINT', () => forward('SIGINT'))
|
|
65
|
+
process.on('SIGTERM', () => forward('SIGTERM'))
|
|
66
|
+
|
|
67
|
+
child.on('exit', (code) => {
|
|
68
|
+
logStream.write(`\n=== dev-logged session ended at ${new Date().toISOString()} (exit ${code}) ===\n`)
|
|
69
|
+
// Wait for the log file to finish flushing BEFORE process.exit — otherwise
|
|
70
|
+
// the final marker may be lost on disk.
|
|
71
|
+
logStream.end(() => {
|
|
72
|
+
console.log(`\n📝 [dev-logger] Log saved: ${logPath}`)
|
|
73
|
+
console.log(`📝 [dev-logger] Review: npm run review`)
|
|
74
|
+
process.exit(code ?? 0)
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
child.on('error', (err) => {
|
|
79
|
+
console.error(`❌ [dev-logger] Failed to spawn agent: ${err.message}`)
|
|
80
|
+
logStream.end(() => process.exit(1))
|
|
81
|
+
})
|