osborn 0.8.11 → 0.8.12

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.
@@ -0,0 +1,9 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(ps:*)",
5
+ "Bash(osascript:*)",
6
+ "Bash(curl -s http://localhost:3000)"
7
+ ]
8
+ }
9
+ }
@@ -0,0 +1,29 @@
1
+ # Skill: Markdown to PDF
2
+
3
+ Export Markdown documents as formatted PDF files.
4
+
5
+ ## When to use
6
+ When the user wants to create a PDF from a Markdown file, spec, or research findings.
7
+
8
+ ## How to execute
9
+
10
+ Option 1 — Using md-to-pdf (best quality):
11
+ ```bash
12
+ npx --yes md-to-pdf "<MARKDOWN_PATH>"
13
+ ```
14
+ This creates a PDF alongside the source file with the same name.
15
+
16
+ Option 2 — Using pandoc (if available):
17
+ ```bash
18
+ pandoc "<MARKDOWN_PATH>" -o "<OUTPUT_PATH>.pdf" --pdf-engine=wkhtmltopdf
19
+ ```
20
+
21
+ Option 3 — Using markdown-pdf:
22
+ ```bash
23
+ npx --yes markdown-pdf "<MARKDOWN_PATH>" -o "<OUTPUT_PATH>.pdf"
24
+ ```
25
+
26
+ ## Output
27
+ - Save the PDF to the session workspace (e.g., `library/{name}.pdf`)
28
+ - Confirm the output path and file size to the user
29
+ - If the source is spec.md, name the output `spec-export.pdf`
@@ -0,0 +1,28 @@
1
+ # Skill: PDF to Markdown
2
+
3
+ Convert PDF documents to readable Markdown text.
4
+
5
+ ## When to use
6
+ When the user provides a PDF file path and wants to read, search, or work with its contents.
7
+
8
+ ## How to execute
9
+
10
+ Option 1 — Using the built-in Read tool:
11
+ The Read tool can directly read PDF files. Use `pages` parameter for large PDFs (max 20 pages per request).
12
+
13
+ Option 2 — Full extraction via CLI (for better formatting or batch processing):
14
+ ```bash
15
+ npx --yes pdf-parse-cli "<PDF_PATH>"
16
+ ```
17
+
18
+ Option 3 — Using pdftotext (if available):
19
+ ```bash
20
+ pdftotext -layout "<PDF_PATH>" -
21
+ ```
22
+
23
+ ## Output
24
+ Save the converted content to the session workspace as `library/{filename}.md` with:
25
+ - Document title and source path at the top
26
+ - Preserved heading structure where detectable
27
+ - Tables converted to Markdown tables where possible
28
+ - Page numbers as section markers
@@ -0,0 +1,90 @@
1
+ # Skill: Playwright Browser Automation
2
+
3
+ Automate web browser interactions — navigate pages, click buttons, fill forms, take screenshots, and extract content.
4
+
5
+ ## When to use
6
+ - Navigate to a URL and interact with it
7
+ - Click buttons or links by their text or role
8
+ - Fill form fields and submit data
9
+ - Take screenshots of web pages
10
+ - Extract text or structured data from pages
11
+ - Automate multi-step web workflows (e.g. join a room, test a UI flow)
12
+
13
+ ## How to execute
14
+
15
+ Uses `@playwright/cli` via npx — no global install needed. Token-efficient: uses element references (e.g. `e15`) instead of pixel coordinates.
16
+
17
+ ### First time only — install browser binaries
18
+ ```bash
19
+ npx playwright install chromium
20
+ ```
21
+
22
+ ### Step 1 — Open a URL
23
+ ```bash
24
+ npx @playwright/cli open https://localhost:3000
25
+ ```
26
+
27
+ ### Step 2 — Get page structure and element references
28
+ ```bash
29
+ npx @playwright/cli snapshot
30
+ ```
31
+ Returns an accessibility tree with element IDs like e1, e2, e15. Use these in subsequent commands.
32
+
33
+ ### Step 3 — Interact with elements
34
+ ```bash
35
+ npx @playwright/cli click e15
36
+ npx @playwright/cli fill e3 "some text"
37
+ npx @playwright/cli press e3 Enter
38
+ npx @playwright/cli select e7 "optionValue"
39
+ npx @playwright/cli check e9
40
+ npx @playwright/cli hover e12
41
+ ```
42
+
43
+ ### Take a screenshot
44
+ ```bash
45
+ npx @playwright/cli screenshot --path=/tmp/page.png
46
+ ```
47
+
48
+ ### Take a screenshot at a specific viewport size (mobile check)
49
+ ```bash
50
+ npx @playwright/cli screenshot --viewport-size=375,812 --path=/tmp/page-mobile.png
51
+ ```
52
+ Common mobile sizes: `375,812` (iPhone 14), `390,844` (iPhone 14 Pro), `412,915` (Pixel 7), `768,1024` (iPad).
53
+
54
+ ### Close the browser
55
+ ```bash
56
+ npx @playwright/cli close
57
+ ```
58
+
59
+ ### Named sessions (persistent state across commands)
60
+ ```bash
61
+ npx @playwright/cli -s=myflow open https://localhost:3000
62
+ npx @playwright/cli -s=myflow snapshot
63
+ npx @playwright/cli -s=myflow fill e3 "abc123"
64
+ npx @playwright/cli -s=myflow click e5
65
+ npx @playwright/cli -s=myflow close
66
+ ```
67
+
68
+ ## Complete example — join Osborn voice room
69
+ ```bash
70
+ npx @playwright/cli open http://localhost:3000
71
+ npx @playwright/cli snapshot
72
+ npx @playwright/cli fill e3 "abc123"
73
+ npx @playwright/cli click e4
74
+ npx @playwright/cli screenshot --path=/tmp/osborn-joined.png
75
+ npx @playwright/cli close
76
+ ```
77
+
78
+ ## Complete example — check mobile layout
79
+ ```bash
80
+ npx @playwright/cli open http://localhost:3000
81
+ npx @playwright/cli screenshot --viewport-size=375,812 --path=/tmp/mobile-375.png
82
+ npx @playwright/cli close
83
+ ```
84
+
85
+ ## Notes
86
+ - Runs headless by default. Add --headed to see the browser window.
87
+ - Install browsers first if needed: npx playwright install chromium
88
+ - Element IDs are session-scoped — run snapshot again after page changes
89
+ - Use `--viewport-size=WIDTH,HEIGHT` to simulate mobile screen sizes (e.g. `375,812` for iPhone 14)
90
+ - Use `--storage-state=/tmp/state.json` to save and restore session state (cookies, localStorage) across runs
@@ -0,0 +1,232 @@
1
+ # Skill: shadcn/ui Components
2
+
3
+ Add and configure shadcn/ui components in a Next.js or React project.
4
+
5
+ ## When to use
6
+ When the user wants to add UI components (buttons, dialogs, cards, forms, tables, etc.) using shadcn/ui — the copy-paste component library built on Radix UI and Tailwind CSS.
7
+
8
+ ## Setup (first time only)
9
+
10
+ Initialize shadcn in the project root (where package.json lives):
11
+ ```bash
12
+ npx shadcn@latest init
13
+ ```
14
+
15
+ This creates `components.json` and sets up `src/components/ui/`. Answer the prompts to match your project's style preferences.
16
+
17
+ ## Add a component
18
+
19
+ ```bash
20
+ npx shadcn@latest add <component-name>
21
+ ```
22
+
23
+ ## Add multiple components at once
24
+ ```bash
25
+ npx shadcn@latest add button card dialog input form
26
+ ```
27
+
28
+ ## Commonly used components
29
+
30
+ | Component | Install name | Description |
31
+ |-----------|-------------|-------------|
32
+ | Button | `button` | Clickable button with variants |
33
+ | Card | `card` | Container with header/content/footer |
34
+ | Input | `input` | Text input field |
35
+ | Label | `label` | Form label |
36
+ | Textarea | `textarea` | Multi-line text input |
37
+ | Select | `select` | Dropdown select |
38
+ | Checkbox | `checkbox` | Checkbox with label |
39
+ | Switch | `switch` | Toggle switch |
40
+ | Dialog | `dialog` | Modal dialog |
41
+ | Sheet | `sheet` | Slide-in panel (drawer) |
42
+ | Popover | `popover` | Floating content panel |
43
+ | Tooltip | `tooltip` | Hover tooltip |
44
+ | Dropdown Menu | `dropdown-menu` | Contextual dropdown |
45
+ | Command | `command` | Command palette / search |
46
+ | Badge | `badge` | Small status indicator |
47
+ | Avatar | `avatar` | User avatar with fallback |
48
+ | Separator | `separator` | Visual divider |
49
+ | Table | `table` | Data table |
50
+ | Tabs | `tabs` | Tabbed interface |
51
+ | Accordion | `accordion` | Collapsible sections |
52
+ | Sonner | `sonner` | Toast notifications (preferred over toast) |
53
+ | Alert | `alert` | Inline alert message |
54
+ | Alert Dialog | `alert-dialog` | Confirmation dialog |
55
+ | Progress | `progress` | Progress bar |
56
+ | Skeleton | `skeleton` | Loading placeholder |
57
+ | Scroll Area | `scroll-area` | Custom scrollbar container |
58
+ | Calendar | `calendar` | Date picker calendar |
59
+ | Form | `form` | React Hook Form integration |
60
+ | Slider | `slider` | Range slider |
61
+ | Toggle | `toggle` | Toggle button |
62
+ | Navigation Menu | `navigation-menu` | Site navigation |
63
+ | Breadcrumb | `breadcrumb` | Page navigation breadcrumbs |
64
+ | Collapsible | `collapsible` | Expandable/collapsible section |
65
+ | Context Menu | `context-menu` | Right-click context menu |
66
+ | Menubar | `menubar` | Application menu bar |
67
+ | Resizable | `resizable` | Resizable panel groups |
68
+
69
+ ## Import pattern
70
+
71
+ ```tsx
72
+ import { Button } from "@/components/ui/button"
73
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card"
74
+ import { Input } from "@/components/ui/input"
75
+ import { Label } from "@/components/ui/label"
76
+ import {
77
+ Dialog,
78
+ DialogContent,
79
+ DialogDescription,
80
+ DialogHeader,
81
+ DialogTitle,
82
+ DialogTrigger,
83
+ } from "@/components/ui/dialog"
84
+ ```
85
+
86
+ ## Example — Button variants
87
+
88
+ ```tsx
89
+ <Button>Default</Button>
90
+ <Button variant="destructive">Delete</Button>
91
+ <Button variant="outline">Cancel</Button>
92
+ <Button variant="ghost">Ghost</Button>
93
+ <Button variant="link">Link</Button>
94
+ <Button size="sm">Small</Button>
95
+ <Button size="lg">Large</Button>
96
+ <Button disabled>Disabled</Button>
97
+ ```
98
+
99
+ ## Example — Card
100
+
101
+ ```tsx
102
+ <Card>
103
+ <CardHeader>
104
+ <CardTitle>Card Title</CardTitle>
105
+ <CardDescription>Card description goes here</CardDescription>
106
+ </CardHeader>
107
+ <CardContent>
108
+ <p>Card content here</p>
109
+ </CardContent>
110
+ <CardFooter>
111
+ <Button>Action</Button>
112
+ </CardFooter>
113
+ </Card>
114
+ ```
115
+
116
+ ## Example — Form with validation (React Hook Form + Zod)
117
+
118
+ ```bash
119
+ npx shadcn@latest add form input
120
+ npm install zod react-hook-form @hookform/resolvers
121
+ ```
122
+
123
+ ```tsx
124
+ import { useForm } from "react-hook-form"
125
+ import { zodResolver } from "@hookform/resolvers/zod"
126
+ import * as z from "zod"
127
+ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
128
+ import { Input } from "@/components/ui/input"
129
+ import { Button } from "@/components/ui/button"
130
+
131
+ const formSchema = z.object({
132
+ email: z.string().email("Invalid email address"),
133
+ password: z.string().min(8, "Password must be at least 8 characters"),
134
+ })
135
+
136
+ export function LoginForm() {
137
+ const form = useForm<z.infer<typeof formSchema>>({
138
+ resolver: zodResolver(formSchema),
139
+ defaultValues: { email: "", password: "" },
140
+ })
141
+
142
+ function onSubmit(values: z.infer<typeof formSchema>) {
143
+ console.log(values)
144
+ }
145
+
146
+ return (
147
+ <Form {...form}>
148
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
149
+ <FormField
150
+ control={form.control}
151
+ name="email"
152
+ render={({ field }) => (
153
+ <FormItem>
154
+ <FormLabel>Email</FormLabel>
155
+ <FormControl>
156
+ <Input placeholder="you@example.com" {...field} />
157
+ </FormControl>
158
+ <FormMessage />
159
+ </FormItem>
160
+ )}
161
+ />
162
+ <FormField
163
+ control={form.control}
164
+ name="password"
165
+ render={({ field }) => (
166
+ <FormItem>
167
+ <FormLabel>Password</FormLabel>
168
+ <FormControl>
169
+ <Input type="password" {...field} />
170
+ </FormControl>
171
+ <FormMessage />
172
+ </FormItem>
173
+ )}
174
+ />
175
+ <Button type="submit" className="w-full">Sign in</Button>
176
+ </form>
177
+ </Form>
178
+ )
179
+ }
180
+ ```
181
+
182
+ ## Example — Toast notifications (Sonner)
183
+
184
+ ```bash
185
+ npx shadcn@latest add sonner
186
+ ```
187
+
188
+ ```tsx
189
+ // In your root layout — add the Toaster once
190
+ import { Toaster } from "@/components/ui/sonner"
191
+ export default function RootLayout({ children }) {
192
+ return (
193
+ <html>
194
+ <body>
195
+ {children}
196
+ <Toaster />
197
+ </body>
198
+ </html>
199
+ )
200
+ }
201
+
202
+ // Then anywhere in your app
203
+ import { toast } from "sonner"
204
+
205
+ toast("Event created")
206
+ toast.success("Profile saved")
207
+ toast.error("Something went wrong")
208
+ toast.promise(saveData(), {
209
+ loading: "Saving...",
210
+ success: "Saved!",
211
+ error: "Failed to save",
212
+ })
213
+ ```
214
+
215
+ ## The cn() utility
216
+
217
+ shadcn uses `clsx` + `tailwind-merge` via a `cn()` helper for conditional classes:
218
+
219
+ ```tsx
220
+ import { cn } from "@/lib/utils"
221
+
222
+ <div className={cn("base-class", isActive && "active-class", className)} />
223
+ ```
224
+
225
+ ## Notes
226
+ - Components are **copied into your project** (not a node_modules dependency) — edit them freely
227
+ - Requires **Tailwind CSS** configured in the project
228
+ - Uses **Radix UI** primitives for accessibility
229
+ - Components land in `src/components/ui/` by default
230
+ - Run `npx shadcn@latest diff` to see if upstream components have changed
231
+ - Check `components.json` for path and style config
232
+ - Use Sonner (`sonner`) instead of the legacy `toast` component for notifications
Binary file
@@ -0,0 +1,24 @@
1
+ # Skill: YouTube Transcript
2
+
3
+ Fetch and save transcripts from YouTube videos.
4
+
5
+ ## When to use
6
+ When the user asks to get a transcript, subtitles, captions, or summary from a YouTube video URL.
7
+
8
+ ## How to execute
9
+
10
+ yt-dlp is installed on this system. Use this exact command:
11
+
12
+ ```bash
13
+ yt-dlp --skip-download --write-auto-sub --sub-lang en --convert-subs srt -o "/tmp/yt-%(id)s" "<VIDEO_URL>"
14
+ ```
15
+
16
+ This downloads auto-generated English subtitles as an SRT file to /tmp/yt-{video-id}.en.srt
17
+
18
+ Then read the SRT file and strip the timing markers to get clean transcript text.
19
+
20
+ ## Output
21
+ Save the cleaned transcript to the session workspace as `library/youtube-{video-id}-transcript.md` with:
22
+ - Video title and URL at the top
23
+ - Cleaned transcript text (strip SRT timing markers and duplicate lines)
24
+ - Key timestamps preserved as section markers if meaningful breaks exist
@@ -0,0 +1,59 @@
1
+ # Osborn Sandbox — Fly.io Machines (per-user)
2
+ # Installs osborn as npm package (not from source) for lightweight per-user machines.
3
+ # Build: docker build -f Dockerfile.sandbox -t registry.fly.io/osborn-sandbox/agent:latest .
4
+ # Push: fly auth docker && docker push registry.fly.io/osborn-sandbox/agent:latest
5
+
6
+ FROM node:22-slim
7
+
8
+ # Runtime deps for osborn + claude-code
9
+ RUN apt-get update -qq && \
10
+ apt-get install --no-install-recommends -y \
11
+ ca-certificates \
12
+ curl \
13
+ git \
14
+ python-is-python3 && \
15
+ rm -rf /var/lib/apt/lists/*
16
+
17
+ # Install osborn + claude-code globally
18
+ RUN npm install -g osborn@latest @anthropic-ai/claude-code
19
+
20
+ # Persistent workspace + claude config dirs
21
+ RUN mkdir -p /workspace /root/.claude
22
+
23
+ ENV OSBORN_CWD=/workspace
24
+ ENV OSBORN_API_PORT=8741
25
+ ENV NODE_ENV=production
26
+
27
+ WORKDIR /workspace
28
+
29
+ EXPOSE 8741
30
+
31
+ # Entrypoint: credential persistence + onboarding suppression + start
32
+ COPY <<'ENTRYPOINT' /entrypoint.sh
33
+ #!/bin/sh
34
+ set -e
35
+
36
+ # Claude credential persistence (volume at /workspace)
37
+ mkdir -p /workspace/.claude
38
+ rm -rf /root/.claude
39
+ ln -sf /workspace/.claude /root/.claude
40
+
41
+ # Suppress Claude Code interactive onboarding prompts
42
+ ONBOARDING_JSON='{"numStartups":10,"installMethod":"npm","autoUpdates":false,"hasCompletedOnboarding":true,"hasTrustDialogAccepted":true,"hasTrustDialogHooksAccepted":true,"hasCompletedProjectOnboarding":true,"hasAcknowledgedCostThreshold":true,"effortCalloutV2Dismissed":true,"theme":"dark","projects":{"/workspace":{"hasTrustDialogAccepted":true,"hasTrustDialogHooksAccepted":true,"hasCompletedProjectOnboarding":true}}}'
43
+ echo "$ONBOARDING_JSON" > /root/.claude.json
44
+ mkdir -p /workspace/.claude
45
+ echo "$ONBOARDING_JSON" > /workspace/.claude/.config.json
46
+ echo "$ONBOARDING_JSON" > /workspace/.claude/claude.json
47
+
48
+ # Restore OAuth token if persisted on volume
49
+ if [ -f /workspace/.claude/.oauth-token ]; then
50
+ export CLAUDE_CODE_OAUTH_TOKEN="$(cat /workspace/.claude/.oauth-token)"
51
+ echo "[sandbox] Restored CLAUDE_CODE_OAUTH_TOKEN from volume"
52
+ fi
53
+
54
+ exec osborn
55
+ ENTRYPOINT
56
+
57
+ RUN chmod +x /entrypoint.sh
58
+
59
+ CMD ["/entrypoint.sh"]
@@ -12,7 +12,11 @@ import { EventEmitter } from 'events';
12
12
  import { saveSessionMetadata, getSessionWorkspace } from './config.js';
13
13
  import { getResearchSystemPrompt, getDirectModeResearchPrompt } from './prompts.js';
14
14
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
15
- import { join } from 'node:path';
15
+ import { join, dirname } from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+ // Directory of this module — used to locate co-located prompt files (e.g., turn-shape reminder).
18
+ const __claudeLlmDir = dirname(fileURLToPath(import.meta.url));
19
+ const TURN_SHAPE_REMINDER_PATH = join(__claudeLlmDir, 'prompts', 'turn-shape-reminder.md');
16
20
  /**
17
21
  * Strip markdown formatting for TTS (text-to-speech)
18
22
  * Removes **bold**, ##headers, ```code```, etc. so TTS doesn't read them literally
@@ -617,7 +621,7 @@ export class ClaudeLLM extends llm.LLM {
617
621
  callbacks.eventEmitter.emit('assistant_text', { text: block.text });
618
622
  const ttsChunk = stripMarkdownForTTS(block.text);
619
623
  if (ttsChunk.trim()) {
620
- console.log(`🔊 TTS say (${ttsChunk.length} chars): "${ttsChunk.substring(0, 60)}..."`);
624
+ console.log(`🔊 TTS say (${ttsChunk.length} chars): "${ttsChunk}"`);
621
625
  callbacks.eventEmitter.emit('tts_say', { text: ttsChunk });
622
626
  }
623
627
  }
@@ -732,7 +736,7 @@ class ClaudeLLMStream extends llm.LLMStream {
732
736
  });
733
737
  return;
734
738
  }
735
- console.log(`🎤 User: "${userText.substring(0, 100)}${userText.length > 100 ? '...' : ''}"`);
739
+ console.log(`🎤 User (${userText.length} chars): "${userText}"`);
736
740
  // Build Claude Agent SDK options
737
741
  const resumeSessionId = this.#opts.resumeSessionId;
738
742
  const continueSession = this.#opts.continueSession;
@@ -746,7 +750,8 @@ class ClaudeLLMStream extends llm.LLMStream {
746
750
  cwd: this.#opts.workingDirectory,
747
751
  permissionMode: this.#opts.permissionMode,
748
752
  allowedTools,
749
- model: this.#opts.model || 'claude-sonnet-4-6', // Sonnet orchestrator with named sub-agents (Haiku tested but ignored delegation rules)
753
+ model: this.#opts.model || 'haiku', // haiku for speed with limited tools, sonnet for full research capabilities (including tool use trace in response)
754
+ // model: this.#opts.model || 'claude-sonnet-4-6', // Sonnet orchestrator with named sub-agents (Haiku tested but ignored delegation rules)
750
755
  enableFileCheckpointing: true,
751
756
  extraArgs: { 'replay-user-messages': null },
752
757
  ...(this.#abortController && { abortController: this.#abortController }),
@@ -852,6 +857,35 @@ class ClaudeLLMStream extends llm.LLMStream {
852
857
  this.#eventEmitter.emit('tool_result', { name: toolName, input: toolInput, response: toolResponse });
853
858
  return {};
854
859
  }]
860
+ }],
861
+ // Per-turn behavioral re-anchor. Fires on EVERY user message that reaches Claude
862
+ // (initial requests, follow-ups, mid-flight steering, resumed-session messages).
863
+ // Reads the reminder text from disk every call, so it's hot-editable just like the
864
+ // main prompt — edit agent/src/prompts/turn-shape-reminder.md, reconnect, next message
865
+ // sees the new reminder. The SDK injects `additionalContext` alongside the user's actual
866
+ // message so the model sees both the literal user input AND the reminder, weighing them
867
+ // together. This is what fights JSONL-history-overrides-system-prompt drift on resumed
868
+ // sessions: the conductor pattern gets re-asserted on every turn instead of being
869
+ // anchored only at session-init time.
870
+ UserPromptSubmit: [{
871
+ matcher: '.*',
872
+ hooks: [async (input) => {
873
+ try {
874
+ const reminder = readFileSync(TURN_SHAPE_REMINDER_PATH, 'utf-8');
875
+ const promptPreview = String(input?.prompt || '').substring(0, 60).replace(/\n/g, ' ');
876
+ console.log(`📌 UserPromptSubmit: injected turn-shape reminder (${reminder.length} chars) for prompt="${promptPreview}..."`);
877
+ return {
878
+ hookSpecificOutput: {
879
+ hookEventName: 'UserPromptSubmit',
880
+ additionalContext: reminder,
881
+ },
882
+ };
883
+ }
884
+ catch (err) {
885
+ console.error('⚠️ UserPromptSubmit: failed to load turn-shape-reminder.md:', err instanceof Error ? err.message : err);
886
+ return { hookSpecificOutput: { hookEventName: 'UserPromptSubmit' } };
887
+ }
888
+ }]
855
889
  }]
856
890
  },
857
891
  // Named sub-agents — Haiku overseer delegates to these specialists.
@@ -1076,12 +1110,12 @@ class ClaudeLLMStream extends llm.LLMStream {
1076
1110
  if (this.#opts.skipTTSQueue) {
1077
1111
  // Direct mode: emit event for session.say() — bypasses LiveKit's
1078
1112
  // BufferedTokenStream which causes stuck/delayed/out-of-order audio
1079
- console.log(`🔊 TTS say (${ttsChunk.length} chars): "${ttsChunk.substring(0, 60)}..."`);
1113
+ console.log(`🔊 TTS say (${ttsChunk.length} chars): "${ttsChunk}"`);
1080
1114
  this.#eventEmitter.emit('tts_say', { text: ttsChunk });
1081
1115
  }
1082
1116
  else {
1083
1117
  // Realtime mode: use LLM stream queue (framework handles TTS)
1084
- console.log(`🔊 TTS stream (${ttsChunk.length} chars): "${ttsChunk.substring(0, 60)}..."`);
1118
+ console.log(`🔊 TTS stream (${ttsChunk.length} chars): "${ttsChunk}"`);
1085
1119
  this.queue.put({
1086
1120
  id: requestId,
1087
1121
  delta: { role: 'assistant', content: ttsChunk },
@@ -1101,11 +1135,11 @@ class ClaudeLLMStream extends llm.LLMStream {
1101
1135
  const ttsText = stripMarkdownForTTS(rawResult);
1102
1136
  if (ttsText.trim()) {
1103
1137
  if (this.#opts.skipTTSQueue) {
1104
- console.log(`🔊 TTS say result (${ttsText.length} chars): "${ttsText.substring(0, 60)}..."`);
1138
+ console.log(`🔊 TTS say result (${ttsText.length} chars): "${ttsText}"`);
1105
1139
  this.#eventEmitter.emit('tts_say', { text: ttsText });
1106
1140
  }
1107
1141
  else {
1108
- console.log(`🔊 TTS result (${ttsText.length} chars): "${ttsText.substring(0, 60)}..."`);
1142
+ console.log(`🔊 TTS result (${ttsText.length} chars): "${ttsText}"`);
1109
1143
  this.queue.put({
1110
1144
  id: requestId,
1111
1145
  delta: { role: 'assistant', content: ttsText },
package/dist/codex-llm.js CHANGED
@@ -97,7 +97,7 @@ class CodexLLMStream extends llm.LLMStream {
97
97
  });
98
98
  return;
99
99
  }
100
- console.log(`🎤 User: "${userText.substring(0, 100)}${userText.length > 100 ? '...' : ''}"`);
100
+ console.log(`🎤 User (${userText.length} chars): "${userText}"`);
101
101
  // Create or reuse thread
102
102
  if (!this.#thread) {
103
103
  console.log('🆕 Starting new Codex thread');