osborn 0.5.3 → 0.8.0

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.
Files changed (47) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/.claude/skills/markdown-to-pdf/SKILL.md +29 -0
  3. package/.claude/skills/pdf-to-markdown/SKILL.md +28 -0
  4. package/.claude/skills/playwright-browser/SKILL.md +90 -0
  5. package/.claude/skills/shadcn/SKILL.md +232 -0
  6. package/.claude/skills/shadcn/image.png +0 -0
  7. package/.claude/skills/youtube-transcript/SKILL.md +24 -0
  8. package/.dockerignore +13 -0
  9. package/Dockerfile +103 -0
  10. package/deploy.sh +70 -0
  11. package/dist/claude-auth.d.ts +60 -0
  12. package/dist/claude-auth.js +334 -0
  13. package/dist/claude-llm.d.ts +51 -2
  14. package/dist/claude-llm.js +619 -86
  15. package/dist/config.d.ts +5 -1
  16. package/dist/config.js +4 -1
  17. package/dist/fast-brain.d.ts +70 -16
  18. package/dist/fast-brain.js +662 -99
  19. package/dist/index-3-2-26-legacy.d.ts +1 -0
  20. package/dist/index-3-2-26-legacy.js +2233 -0
  21. package/dist/index.js +979 -429
  22. package/dist/jsonl-search.d.ts +66 -0
  23. package/dist/jsonl-search.js +274 -0
  24. package/dist/leagcyprompts2.d.ts +0 -0
  25. package/dist/leagcyprompts2.js +573 -0
  26. package/dist/pipeline-direct-llm.d.ts +77 -0
  27. package/dist/pipeline-direct-llm.js +221 -0
  28. package/dist/pipeline-fastbrain.d.ts +45 -0
  29. package/dist/pipeline-fastbrain.js +373 -0
  30. package/dist/prompts-2-25-26.d.ts +0 -0
  31. package/dist/prompts-2-25-26.js +518 -0
  32. package/dist/prompts-3-2-26.d.ts +78 -0
  33. package/dist/prompts-3-2-26.js +1319 -0
  34. package/dist/prompts.d.ts +83 -12
  35. package/dist/prompts.js +2064 -587
  36. package/dist/recall-client.d.ts +33 -0
  37. package/dist/recall-client.js +101 -0
  38. package/dist/session-access.d.ts +24 -0
  39. package/dist/session-access.js +74 -0
  40. package/dist/summary-index.d.ts +87 -0
  41. package/dist/summary-index.js +570 -0
  42. package/dist/turn-detector-shim.d.ts +24 -0
  43. package/dist/turn-detector-shim.js +83 -0
  44. package/dist/voice-io.d.ts +15 -5
  45. package/dist/voice-io.js +52 -20
  46. package/fly.toml +30 -0
  47. package/package.json +18 -13
@@ -0,0 +1,334 @@
1
+ /**
2
+ * claude-auth.ts — Claude Code CLI OAuth flow
3
+ *
4
+ * Handles authentication for Claude Code in headless/cloud environments.
5
+ * Learned from claudebox (etokarev/claude-code-docker, vutran1710/claudebox).
6
+ *
7
+ * Auth priority:
8
+ * 1. CLAUDE_CODE_OAUTH_TOKEN env var (set via `claude setup-token` on local machine)
9
+ * 2. ~/.claude/.credentials.json file (persisted on Fly.io volume)
10
+ * 3. `claude auth status --json` CLI check
11
+ * 4. Interactive OAuth flow via `claude setup-token` + pty
12
+ *
13
+ * On Linux/Docker, credentials go to ~/.claude/.credentials.json (file-based, no keyring).
14
+ * The Fly.io volume at /workspace/.claude is symlinked to ~/.claude for persistence.
15
+ */
16
+ import * as pty from 'node-pty';
17
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
18
+ import { homedir } from 'os';
19
+ import { join } from 'path';
20
+ // ─────────────────────────────────────────
21
+ // Constants
22
+ // ─────────────────────────────────────────
23
+ const CREDENTIALS_PATH = join(homedir(), '.claude', '.credentials.json');
24
+ // URL matching: strip all whitespace first (like claudebox), then match
25
+ // Handles Ink UI wrapping URLs across multiple lines
26
+ const URL_REGEX = /https:\/\/claude\.(com|ai)\/cai\/oauth\/authorize[^\s]*/;
27
+ // Matches successful login/token creation
28
+ const SUCCESS_PATTERN = /Long-lived authentication token created|Login successful|Logged in as|Successfully authenticated|auth.*success/i;
29
+ // How long to wait for auth before timing out (5 minutes)
30
+ const AUTH_TIMEOUT_MS = 5 * 60 * 1000;
31
+ // ─────────────────────────────────────────
32
+ // Auth Check
33
+ // ─────────────────────────────────────────
34
+ /**
35
+ * Check if CLAUDE_CODE_OAUTH_TOKEN env var is set (highest priority).
36
+ * This is the recommended approach for cloud deployments.
37
+ * Token generated via `claude setup-token` on local machine.
38
+ */
39
+ function hasOAuthTokenEnv() {
40
+ return !!process.env.CLAUDE_CODE_OAUTH_TOKEN;
41
+ }
42
+ /**
43
+ * Check if credentials file exists with a valid access token.
44
+ * On Linux/Docker, Claude Code stores OAuth creds at ~/.claude/.credentials.json
45
+ */
46
+ export function isClaudeAuthenticated() {
47
+ // Env var takes highest priority
48
+ if (hasOAuthTokenEnv())
49
+ return true;
50
+ if (!existsSync(CREDENTIALS_PATH))
51
+ return false;
52
+ try {
53
+ const raw = readFileSync(CREDENTIALS_PATH, 'utf-8');
54
+ const creds = JSON.parse(raw);
55
+ const oauth = creds?.claudeAiOauth;
56
+ if (!oauth?.accessToken)
57
+ return false;
58
+ // Check expiry with 60s buffer
59
+ if (oauth.expiresAt && Date.now() > oauth.expiresAt - 60_000) {
60
+ console.log('⚠️ Claude credentials exist but access token is expired');
61
+ return false;
62
+ }
63
+ return true;
64
+ }
65
+ catch (err) {
66
+ console.warn('⚠️ Failed to read Claude credentials:', err);
67
+ return false;
68
+ }
69
+ }
70
+ /**
71
+ * Check auth via `claude auth status --json` (most reliable).
72
+ * Uses execSync to avoid node-pty PATH issues on macOS.
73
+ */
74
+ export async function checkClaudeAuthStatus() {
75
+ try {
76
+ const { execSync } = await import('child_process');
77
+ const output = execSync('claude auth status', {
78
+ encoding: 'utf-8',
79
+ timeout: 10_000,
80
+ env: { ...process.env },
81
+ });
82
+ if (output.includes('"loggedIn": true') || output.includes('"loggedIn":true')) {
83
+ return true;
84
+ }
85
+ return /logged in|authenticated|active/i.test(output);
86
+ }
87
+ catch {
88
+ return false;
89
+ }
90
+ }
91
+ // ─────────────────────────────────────────
92
+ // URL Extraction (claudebox pattern)
93
+ // ─────────────────────────────────────────
94
+ /**
95
+ * Extract OAuth URL from CLI output.
96
+ * Strips ALL whitespace first (like vutran1710/claudebox) to handle
97
+ * Ink UI wrapping the URL across multiple lines.
98
+ * Also cleans trailing "Pastecodehereifprompted" that Ink appends.
99
+ */
100
+ function extractOAuthUrl(text) {
101
+ // Strip ANSI codes
102
+ const noAnsi = text.replace(/\x1B\[[0-9;]*[A-Za-z]/g, '')
103
+ .replace(/\x1B\][^\x07]*\x07/g, '');
104
+ // Strip all whitespace (claudebox pattern: strings.Join(strings.Fields(pane), ""))
105
+ const stripped = noAnsi.replace(/\s+/g, '');
106
+ const match = stripped.match(URL_REGEX);
107
+ if (!match)
108
+ return null;
109
+ let url = match[0];
110
+ // Clean trailing Ink artifacts (claudebox pattern)
111
+ const trailingJunk = ['Pastecodehereifprompted', 'Pastecodehereifprompted>'];
112
+ for (const junk of trailingJunk) {
113
+ const idx = url.indexOf(junk);
114
+ if (idx > 0)
115
+ url = url.substring(0, idx);
116
+ }
117
+ return url;
118
+ }
119
+ // ─────────────────────────────────────────
120
+ // Auth Flow
121
+ // ─────────────────────────────────────────
122
+ /**
123
+ * Run the Claude CLI OAuth flow via `claude setup-token` in a pseudo-terminal.
124
+ *
125
+ * setup-token provides the Ink UI with a "Paste code here if prompted >" input.
126
+ * Unlike `auth login` which ignores pty stdin, setup-token accepts typed input.
127
+ *
128
+ * Code is written in chunks to simulate real typing (Ink reads raw keypresses).
129
+ */
130
+ export function runClaudeAuthFlow(callbacks) {
131
+ let procRef = null;
132
+ const handle = {
133
+ submitCode: (code) => {
134
+ if (procRef) {
135
+ const trimmed = code.trim();
136
+ console.log(`🔑 Submitting auth code to Claude CLI (${trimmed.length} chars)`);
137
+ // Ink reads raw keypresses. Write in chunks to simulate typing.
138
+ const CHUNK_SIZE = 10;
139
+ let offset = 0;
140
+ const writeChunk = () => {
141
+ if (!procRef || offset >= trimmed.length) {
142
+ if (procRef) {
143
+ console.log('🔑 Auth code fully written, sending Enter');
144
+ procRef.write('\r');
145
+ }
146
+ return;
147
+ }
148
+ procRef.write(trimmed.slice(offset, offset + CHUNK_SIZE));
149
+ offset += CHUNK_SIZE;
150
+ setTimeout(writeChunk, 50);
151
+ };
152
+ writeChunk();
153
+ }
154
+ else {
155
+ console.error('❌ Cannot submit code — Claude CLI process not running');
156
+ }
157
+ },
158
+ };
159
+ const done = new Promise((resolve, reject) => {
160
+ console.log('🔑 Starting Claude Code authentication flow (setup-token)...');
161
+ const proc = pty.spawn('claude', ['setup-token'], {
162
+ name: 'xterm-color',
163
+ cols: 500, // Wide to prevent Ink URL wrapping
164
+ rows: 30,
165
+ cwd: homedir(),
166
+ env: { ...process.env, TERM: 'xterm-color' },
167
+ });
168
+ procRef = proc;
169
+ let fullBuffer = ''; // Accumulates ALL output for URL extraction
170
+ let recentBuffer = ''; // Recent output for pattern matching
171
+ let urlSent = false;
172
+ let codeSolicited = false;
173
+ let completed = false;
174
+ const timeout = setTimeout(() => {
175
+ if (!completed) {
176
+ proc.kill();
177
+ procRef = null;
178
+ const msg = 'Claude authentication timed out after 5 minutes';
179
+ console.error('❌', msg);
180
+ callbacks.onError(msg);
181
+ reject(new Error(msg));
182
+ }
183
+ }, AUTH_TIMEOUT_MS);
184
+ proc.onData((data) => {
185
+ const clean = data.replace(/\x1B\[[0-9;]*[A-Za-z]/g, '')
186
+ .replace(/\x1B\][^\x07]*\x07/g, '');
187
+ fullBuffer += clean;
188
+ recentBuffer += clean;
189
+ // Debug log (truncated)
190
+ const logLine = clean.trim();
191
+ if (logLine)
192
+ console.log(`🔑 [claude-cli] ${logLine.substring(0, 200)}`);
193
+ callbacks.onOutput?.(data);
194
+ // Extract OAuth URL from accumulated output (claudebox whitespace-strip pattern)
195
+ if (!urlSent) {
196
+ const url = extractOAuthUrl(fullBuffer);
197
+ if (url && url.length > 100) { // Valid URLs are >100 chars
198
+ console.log(`🔗 Claude auth URL captured (${url.length} chars)`);
199
+ urlSent = true;
200
+ callbacks.onUrl(url);
201
+ recentBuffer = '';
202
+ }
203
+ }
204
+ // Detect code prompt
205
+ if (urlSent && !codeSolicited && /paste|enter.*code|Paste code/i.test(recentBuffer)) {
206
+ codeSolicited = true;
207
+ console.log('🔑 Claude CLI waiting for auth code');
208
+ callbacks.onWaitingForCode();
209
+ recentBuffer = '';
210
+ }
211
+ // Detect the OAuth token in output (sk-ant-oat01-...)
212
+ // setup-token prints it AFTER "created successfully!" — we must NOT kill before capturing it
213
+ const tokenMatch = fullBuffer.match(/sk-ant-oat01-[A-Za-z0-9_-]+/);
214
+ if (tokenMatch && !completed) {
215
+ completed = true;
216
+ clearTimeout(timeout);
217
+ const token = tokenMatch[0];
218
+ console.log(`✅ OAuth token captured (${token.length} chars, starts with ${token.substring(0, 20)}...)`);
219
+ // Set as env var so Claude Agent SDK picks it up immediately
220
+ process.env.CLAUDE_CODE_OAUTH_TOKEN = token;
221
+ // Persist to volume for future restarts
222
+ try {
223
+ const tokenDir = join(homedir(), '.claude');
224
+ mkdirSync(tokenDir, { recursive: true });
225
+ // Write as credentials file
226
+ writeFileSync(CREDENTIALS_PATH, JSON.stringify({
227
+ claudeAiOauth: { accessToken: token }
228
+ }), { mode: 0o600 });
229
+ // Also write token to a simple file for easy env var restore on restart
230
+ writeFileSync(join(tokenDir, '.oauth-token'), token, { mode: 0o600 });
231
+ console.log('✅ Token persisted to volume');
232
+ }
233
+ catch (err) {
234
+ console.warn('⚠️ Failed to persist token to file:', err);
235
+ }
236
+ procRef = null;
237
+ callbacks.onComplete();
238
+ proc.kill();
239
+ resolve();
240
+ }
241
+ // Detect errors
242
+ if (/OAuth error|Invalid code|expired/i.test(recentBuffer)) {
243
+ const errMsg = recentBuffer.replace(/\x1B\[[0-9;]*[A-Za-z]/g, '').trim().substring(0, 200);
244
+ console.log('⚠️ Claude auth error:', errMsg);
245
+ callbacks.onError(errMsg);
246
+ recentBuffer = '';
247
+ }
248
+ // Keep recentBuffer from growing unbounded
249
+ if (recentBuffer.length > 5000)
250
+ recentBuffer = recentBuffer.slice(-2000);
251
+ });
252
+ proc.onExit(({ exitCode }) => {
253
+ clearTimeout(timeout);
254
+ procRef = null;
255
+ if (!completed) {
256
+ const msg = `Claude CLI exited with code ${exitCode} before auth completed`;
257
+ console.error('❌', msg);
258
+ callbacks.onError(msg);
259
+ reject(new Error(msg));
260
+ }
261
+ });
262
+ });
263
+ return { handle, done };
264
+ }
265
+ // ─────────────────────────────────────────
266
+ // Startup Gate
267
+ // ─────────────────────────────────────────
268
+ /**
269
+ * Ensure Claude is authenticated before proceeding.
270
+ *
271
+ * Check order:
272
+ * 1. CLAUDE_CODE_OAUTH_TOKEN env var
273
+ * 2. ~/.claude/.credentials.json file
274
+ * 3. `claude auth status --json`
275
+ * 4. Interactive OAuth flow (setup-token)
276
+ */
277
+ export async function ensureClaudeAuth(sendToFrontend) {
278
+ // Check 0: Restore token from volume if previously persisted
279
+ if (!hasOAuthTokenEnv()) {
280
+ try {
281
+ const tokenFile = join(homedir(), '.claude', '.oauth-token');
282
+ if (existsSync(tokenFile)) {
283
+ const token = readFileSync(tokenFile, 'utf-8').trim();
284
+ if (token.startsWith('sk-ant-')) {
285
+ process.env.CLAUDE_CODE_OAUTH_TOKEN = token;
286
+ console.log('✅ Restored CLAUDE_CODE_OAUTH_TOKEN from persisted volume');
287
+ }
288
+ }
289
+ }
290
+ catch { }
291
+ }
292
+ // Check 1: Env var (cloud best practice — set via Fly secrets or persisted from setup-token)
293
+ if (hasOAuthTokenEnv()) {
294
+ console.log('✅ Claude authenticated via CLAUDE_CODE_OAUTH_TOKEN env var');
295
+ return {};
296
+ }
297
+ // Check 2: Credentials file
298
+ if (isClaudeAuthenticated()) {
299
+ console.log('✅ Claude authenticated via credentials file');
300
+ return {};
301
+ }
302
+ // Check 3: CLI status (handles Keychain on macOS, other storage backends)
303
+ const cliStatus = await checkClaudeAuthStatus();
304
+ if (cliStatus) {
305
+ console.log('✅ Claude authenticated (CLI status confirmed)');
306
+ return {};
307
+ }
308
+ // Check 4: Need interactive OAuth flow
309
+ console.log('🔑 Claude not authenticated — starting OAuth flow');
310
+ sendToFrontend('claude_auth_required', {
311
+ message: 'Claude authentication required. A login URL will appear shortly.',
312
+ });
313
+ const { handle, done } = runClaudeAuthFlow({
314
+ onUrl: (url) => {
315
+ console.log('📤 Sending Claude auth URL to frontend');
316
+ sendToFrontend('claude_auth_url', { url });
317
+ },
318
+ onWaitingForCode: () => {
319
+ console.log('📤 Sending code prompt to frontend');
320
+ sendToFrontend('claude_auth_waiting_code', {
321
+ message: 'Paste the authentication code from the browser.',
322
+ });
323
+ },
324
+ onComplete: () => {
325
+ sendToFrontend('claude_auth_complete', {
326
+ message: 'Claude authenticated successfully. Starting voice session...',
327
+ });
328
+ },
329
+ onError: (message) => {
330
+ sendToFrontend('claude_auth_error', { message });
331
+ },
332
+ });
333
+ return { submitCode: handle.submitCode, done };
334
+ }
@@ -7,10 +7,11 @@
7
7
  * Flow: User speaks → STT → ClaudeLLM (Agent SDK) → TTS → User hears
8
8
  */
9
9
  import { llm, type APIConnectOptions } from '@livekit/agents';
10
- import { type McpServerConfig } from '@anthropic-ai/claude-agent-sdk';
10
+ import { type Options, type McpServerConfig } from '@anthropic-ai/claude-agent-sdk';
11
11
  import { EventEmitter } from 'events';
12
12
  export interface ClaudeLLMOptions {
13
13
  workingDirectory?: string;
14
+ sessionBaseDir?: string;
14
15
  permissionMode?: 'default' | 'acceptEdits' | 'bypassPermissions';
15
16
  allowedTools?: string[];
16
17
  eventEmitter?: EventEmitter;
@@ -18,6 +19,8 @@ export interface ClaudeLLMOptions {
18
19
  continueSession?: boolean;
19
20
  mcpServers?: Record<string, McpServerConfig>;
20
21
  model?: string;
22
+ voiceMode?: 'direct' | 'realtime';
23
+ skipTTSQueue?: boolean;
21
24
  }
22
25
  /**
23
26
  * Claude LLM - Wraps Claude Agent SDK for LiveKit
@@ -113,13 +116,59 @@ export declare class ClaudeLLM extends llm.LLM {
113
116
  * Check if checkpoints are available
114
117
  */
115
118
  hasCheckpoints(): boolean;
116
- chat({ chatCtx, toolCtx, connOptions, }: {
119
+ /**
120
+ * Interrupt the current Claude query gracefully (like pressing Esc).
121
+ * Stops current tool execution but keeps the process alive.
122
+ * Returns true if interrupted, false if no active query.
123
+ */
124
+ interruptQuery(): Promise<boolean>;
125
+ /**
126
+ * Hard abort all active queries (like Ctrl+C).
127
+ * Kills subprocesses. Next message will spawn new processes.
128
+ */
129
+ abortQuery(): void;
130
+ /**
131
+ * Rewind file changes to a specific checkpoint.
132
+ * Uses the most recently added query (most likely to have the rewind capability).
133
+ */
134
+ rewindToCheckpoint(checkpointId?: string): Promise<boolean>;
135
+ /**
136
+ * Check if there are active queries that can be interrupted
137
+ */
138
+ hasActiveQuery(): boolean;
139
+ /** Add an active query (called from ClaudeLLMStream when query starts) */
140
+ setActiveQuery(q: any): void;
141
+ /** Remove an active query (called from ClaudeLLMStream when query completes) */
142
+ removeActiveQuery(q: any): void;
143
+ /** Whether a persistent session is alive and consuming messages */
144
+ hasSession(): boolean;
145
+ /**
146
+ * Close the persistent session (kills subprocess).
147
+ * Call on disconnect, session switch, or recovery.
148
+ */
149
+ closeSession(): void;
150
+ /**
151
+ * Push a user message into the persistent session.
152
+ * If no session exists yet, creates one (cold start with JSONL replay).
153
+ * If a session exists, instantly delivers the message (no replay).
154
+ *
155
+ * @param userText - The user's message text
156
+ * @param sdkOptions - Full V1 Options (only used on first call to create the query)
157
+ * @param callbacks - Event callbacks for the background consumer
158
+ */
159
+ pushMessage(userText: string, sdkOptions: Options, callbacks: {
160
+ onSessionId: (id: string) => void;
161
+ onCheckpoint: (checkpointId: string) => void;
162
+ eventEmitter: EventEmitter;
163
+ }): void;
164
+ chat({ chatCtx, toolCtx, connOptions, abortController, }: {
117
165
  chatCtx: llm.ChatContext;
118
166
  toolCtx?: llm.ToolContext;
119
167
  connOptions?: APIConnectOptions;
120
168
  parallelToolCalls?: boolean;
121
169
  toolChoice?: llm.ToolChoice;
122
170
  extraKwargs?: Record<string, unknown>;
171
+ abortController?: AbortController;
123
172
  }): llm.LLMStream;
124
173
  }
125
174
  /**