osborn 0.8.16 → 0.8.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.
- package/.claude/skills/browser-apply/SKILL.md +114 -0
- package/.env.example +7 -0
- package/dist/claude-llm.js +28 -3
- package/dist/index.js +67 -22
- package/package.json +1 -1
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Skill: Browser Apply — Step-by-Step Workday Application
|
|
2
|
+
|
|
3
|
+
Automate Workday job applications interactively, one step at a time. Each step takes a screenshot, confirms what's on screen, fills the current page, and waits before proceeding.
|
|
4
|
+
|
|
5
|
+
**This skill uses the Playwright MCP tools** (`mcp__playwright__browser_*`) for direct browser control — no scripts needed.
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
- Any Workday ATS application (`*.wd1.myworkdayjobs.com`)
|
|
9
|
+
- Any multi-step JS-heavy job application form
|
|
10
|
+
- When you want visible, confirmable progress at each step
|
|
11
|
+
|
|
12
|
+
## Key Principle: Step-by-Step, Not One Big Script
|
|
13
|
+
|
|
14
|
+
Do NOT write a monolithic automation script. Instead:
|
|
15
|
+
1. Navigate to the URL
|
|
16
|
+
2. Take a screenshot — confirm what's on screen
|
|
17
|
+
3. Fill only the current step's fields
|
|
18
|
+
4. Take another screenshot — confirm fields filled correctly
|
|
19
|
+
5. Ask the user "Ready for next step?" before clicking Next
|
|
20
|
+
6. Click Next, wait for page load, screenshot again
|
|
21
|
+
7. Repeat for each step
|
|
22
|
+
|
|
23
|
+
This approach catches rendering issues, unexpected fields, and errors before they cascade.
|
|
24
|
+
|
|
25
|
+
## Step-by-Step Execution Pattern
|
|
26
|
+
|
|
27
|
+
### Step 0 — Open the browser
|
|
28
|
+
Use: `mcp__playwright__browser_navigate` with the applyManually URL
|
|
29
|
+
|
|
30
|
+
Then: `mcp__playwright__browser_take_screenshot` — show the user what loaded
|
|
31
|
+
|
|
32
|
+
### Step 1 — Create Account / Sign In
|
|
33
|
+
Take a snapshot with `mcp__playwright__browser_snapshot` to see element refs.
|
|
34
|
+
Fill fields using `mcp__playwright__browser_fill_form` or individual `mcp__playwright__browser_type` calls.
|
|
35
|
+
Screenshot to confirm. Then ask user before clicking Create Account / Sign In.
|
|
36
|
+
|
|
37
|
+
### Step 2 — Start Application
|
|
38
|
+
If "Start Your Application" screen appears with Apply Manually button:
|
|
39
|
+
Screenshot it. Click "Apply Manually" using `mcp__playwright__browser_click`.
|
|
40
|
+
Screenshot after.
|
|
41
|
+
|
|
42
|
+
### Step 3 — My Information
|
|
43
|
+
Snapshot → fill each field → screenshot → ask user before clicking Next.
|
|
44
|
+
|
|
45
|
+
Fields to fill:
|
|
46
|
+
- First Name, Last Name, Phone
|
|
47
|
+
- Address, City, State (dropdown), Zip
|
|
48
|
+
- Work authorization: Yes
|
|
49
|
+
- Sponsorship: No
|
|
50
|
+
|
|
51
|
+
### Step 4 — My Experience
|
|
52
|
+
Snapshot → click Add for each job entry → fill title/company/dates/description → save each → screenshot.
|
|
53
|
+
Then add education entries.
|
|
54
|
+
Ask user before clicking Next.
|
|
55
|
+
|
|
56
|
+
### Step 5 — Application Questions
|
|
57
|
+
Snapshot to see all questions. Fill each one. **Always confirm salary expectation with user before filling** — never guess. Screenshot. Ask before Next.
|
|
58
|
+
|
|
59
|
+
### Step 6 — Voluntary Disclosures
|
|
60
|
+
Select "I do not wish to answer" / "Prefer not to disclose" for all. Screenshot. Ask before Next.
|
|
61
|
+
|
|
62
|
+
### Step 7 — Self Identify
|
|
63
|
+
Fill name and date. Select disability option. Screenshot. Ask before Next.
|
|
64
|
+
|
|
65
|
+
### Step 8 — Review
|
|
66
|
+
Screenshot the full review page. Confirm with user before clicking Submit.
|
|
67
|
+
|
|
68
|
+
### Step 9 — Confirm submission
|
|
69
|
+
Screenshot the confirmation dialog. Save it.
|
|
70
|
+
|
|
71
|
+
## Candidate Data (Osborn Ojure)
|
|
72
|
+
|
|
73
|
+
- Email: osbornojure@gmail.com
|
|
74
|
+
- Password: Workday2026!
|
|
75
|
+
- First: Osborn, Last: Ojure
|
|
76
|
+
- Phone: 3127185561
|
|
77
|
+
- Address: 1234 N Michigan Ave, Chicago, IL 60601
|
|
78
|
+
|
|
79
|
+
Jobs:
|
|
80
|
+
1. Meta API Consultant at Prehype / Audos — April 2024 to Present
|
|
81
|
+
2. Full Stack Developer, Freelance — January 2016 to Present
|
|
82
|
+
|
|
83
|
+
Education:
|
|
84
|
+
1. A.S. Information Systems
|
|
85
|
+
2. B.S. Psychology
|
|
86
|
+
|
|
87
|
+
## Workday data-automation-id Selector Reference
|
|
88
|
+
|
|
89
|
+
| Field | Selector |
|
|
90
|
+
|---|---|
|
|
91
|
+
| Email | `input[type="email"]` |
|
|
92
|
+
| Password | `input[type="password"]` |
|
|
93
|
+
| First name | `[data-automation-id="legalNameSection_firstName"]` |
|
|
94
|
+
| Last name | `[data-automation-id="legalNameSection_lastName"]` |
|
|
95
|
+
| Phone | `[data-automation-id="phone-number"]` |
|
|
96
|
+
| Address | `[data-automation-id="addressSection_addressLine1"]` |
|
|
97
|
+
| City | `[data-automation-id="addressSection_city"]` |
|
|
98
|
+
| Zip | `[data-automation-id="addressSection_postalCode"]` |
|
|
99
|
+
| Job title | `[data-automation-id="jobTitle"]` |
|
|
100
|
+
| Company | `[data-automation-id="company"]` |
|
|
101
|
+
| Description | `[data-automation-id="description"]` |
|
|
102
|
+
| Next button | `[data-automation-id="bottom-navigation-next-btn"]` |
|
|
103
|
+
| Create Account | `[data-automation-id="click_filter"][aria-label="Create Account"]` |
|
|
104
|
+
|
|
105
|
+
## Critical Rules
|
|
106
|
+
- headless: false always (Workday renders blank in headless)
|
|
107
|
+
- Confirm salary with user before every submission — never auto-fill
|
|
108
|
+
- After "You already applied to this job" error — that confirms a previous submission worked
|
|
109
|
+
- Use `{ force: true }` on Workday buttons — overlay click filters block normal clicks
|
|
110
|
+
- Always wait for networkidle or waitForSelector after navigation before interacting
|
|
111
|
+
|
|
112
|
+
## Playwright Install Location
|
|
113
|
+
Run scripts from: `/Users/newupgrade/Desktop/Developer/osborn/frontend`
|
|
114
|
+
(playwright is in `node_modules` there)
|
package/.env.example
CHANGED
|
@@ -19,3 +19,10 @@ ANTHROPIC_API_KEY=sk-ant-...
|
|
|
19
19
|
# Smithery (cloud-hosted MCP servers - YouTube, GitHub, etc.)
|
|
20
20
|
# Get your key at: https://smithery.ai
|
|
21
21
|
# SMITHERY_API_KEY=your-smithery-api-key
|
|
22
|
+
|
|
23
|
+
# Frontend URL — used by the agent to upload workspace artifacts (resumes,
|
|
24
|
+
# reports, PDFs, search indexes) to Supabase Storage via the frontend's
|
|
25
|
+
# /api/upload route. Without this, the agent falls back to inlining file
|
|
26
|
+
# content through the LiveKit data channel (capped at 30KB to avoid
|
|
27
|
+
# corrupting the publisher connection). Local dev: http://localhost:3000
|
|
28
|
+
# OSBORN_FRONTEND_URL=http://localhost:3000
|
package/dist/claude-llm.js
CHANGED
|
@@ -17,6 +17,13 @@ import { fileURLToPath } from 'node:url';
|
|
|
17
17
|
// Directory of this module — used to locate co-located prompt files (e.g., turn-shape reminder).
|
|
18
18
|
const __claudeLlmDir = dirname(fileURLToPath(import.meta.url));
|
|
19
19
|
const TURN_SHAPE_REMINDER_PATH = join(__claudeLlmDir, 'prompts', 'turn-shape-reminder.md');
|
|
20
|
+
// ≤3 direct tool call budget per turn. Reset on every UserPromptSubmit (new user message).
|
|
21
|
+
// Enforced mechanically in PreToolUse — the model CANNOT exceed this regardless of JSONL history.
|
|
22
|
+
// Task/Agent delegations are exempt (delegation is what we WANT). Sub-agent tool calls
|
|
23
|
+
// (agent_type !== null) are exempt (they're inside a delegation). Only the main orchestrator
|
|
24
|
+
// agent's direct tool calls count against the budget.
|
|
25
|
+
let turnToolCallCount = 0;
|
|
26
|
+
const TOOL_CALL_BUDGET = 3;
|
|
20
27
|
/**
|
|
21
28
|
* Strip markdown formatting for TTS (text-to-speech)
|
|
22
29
|
* Removes **bold**, ##headers, ```code```, etc. so TTS doesn't read them literally
|
|
@@ -750,8 +757,8 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
750
757
|
cwd: this.#opts.workingDirectory,
|
|
751
758
|
permissionMode: this.#opts.permissionMode,
|
|
752
759
|
allowedTools,
|
|
753
|
-
model: this.#opts.model || 'haiku', // haiku for speed with limited tools, sonnet for full research capabilities (including tool use trace in response)
|
|
754
|
-
|
|
760
|
+
// model: this.#opts.model || 'haiku', // haiku for speed with limited tools, sonnet for full research capabilities (including tool use trace in response)
|
|
761
|
+
model: this.#opts.model || 'claude-sonnet-4-6', // Sonnet orchestrator with named sub-agents (Haiku tested but ignored delegation rules)
|
|
755
762
|
enableFileCheckpointing: true,
|
|
756
763
|
extraArgs: { 'replay-user-messages': null },
|
|
757
764
|
...(this.#abortController && { abortController: this.#abortController }),
|
|
@@ -824,6 +831,22 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
824
831
|
const toolInput = input?.tool_input || {};
|
|
825
832
|
const agentType = input?.agent_type || null;
|
|
826
833
|
console.log(`🔍 PreToolUse: toolName=${toolName} agent_type=${agentType} agent_id=${input?.agent_id || 'none'} all_keys=[${Object.keys(input || {}).join(', ')}]`);
|
|
834
|
+
// ≤3 direct tool call budget enforcement.
|
|
835
|
+
// Only counts calls from the MAIN orchestrator agent (agent_type === null).
|
|
836
|
+
// Task/Agent delegations are exempt — delegation is the desired behavior.
|
|
837
|
+
// Sub-agent tool calls are exempt — they're inside a delegation.
|
|
838
|
+
if (!agentType && toolName !== 'Task' && toolName !== 'Agent') {
|
|
839
|
+
turnToolCallCount++;
|
|
840
|
+
if (turnToolCallCount > TOOL_CALL_BUDGET) {
|
|
841
|
+
console.log(`🛑 Tool budget exceeded (${turnToolCallCount}/${TOOL_CALL_BUDGET}) — DENYING ${toolName}. Must delegate via Task.`);
|
|
842
|
+
this.#eventEmitter.emit('tool_blocked', { name: toolName, reason: `Tool call budget exceeded (${turnToolCallCount}/${TOOL_CALL_BUDGET}). Delegate via Task.` });
|
|
843
|
+
return {
|
|
844
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny' },
|
|
845
|
+
reason: `Hard limit: maximum ${TOOL_CALL_BUDGET} direct tool calls per turn (you are at ${turnToolCallCount}). Delegate the remaining work to a sub-agent via Task(subagent_type=\'researcher\'|\'writer\'|\'reasoner\', run_in_background: true). This is a system-enforced limit.`,
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
console.log(`🔧 Tool call ${turnToolCallCount}/${TOOL_CALL_BUDGET}: ${toolName}`);
|
|
849
|
+
}
|
|
827
850
|
// Write/Edit/MultiEdit access control
|
|
828
851
|
if (toolName === 'Write' || toolName === 'Edit' || toolName === 'MultiEdit') {
|
|
829
852
|
// Writer sub-agent gets full write access everywhere
|
|
@@ -871,9 +894,11 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
871
894
|
matcher: '.*',
|
|
872
895
|
hooks: [async (input) => {
|
|
873
896
|
try {
|
|
897
|
+
// Reset the per-turn tool call counter so the new turn starts fresh.
|
|
898
|
+
turnToolCallCount = 0;
|
|
874
899
|
const reminder = readFileSync(TURN_SHAPE_REMINDER_PATH, 'utf-8');
|
|
875
900
|
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}..."`);
|
|
901
|
+
console.log(`📌 UserPromptSubmit: injected turn-shape reminder (${reminder.length} chars) for prompt="${promptPreview}..." [tool budget reset to 0/${TOOL_CALL_BUDGET}]`);
|
|
877
902
|
return {
|
|
878
903
|
hookSpecificOutput: {
|
|
879
904
|
hookEventName: 'UserPromptSubmit',
|
package/dist/index.js
CHANGED
|
@@ -2703,40 +2703,85 @@ async function main() {
|
|
|
2703
2703
|
if (filePath && (filePath.includes('/osb/') || filePath.includes('.osborn/sessions/') || filePath.includes('.osborn/research/'))) {
|
|
2704
2704
|
try {
|
|
2705
2705
|
const fs = await import('fs');
|
|
2706
|
+
const path = await import('path');
|
|
2706
2707
|
const fileName = filePath.split('/').pop() || '';
|
|
2707
2708
|
const ext = fileName.split('.').pop()?.toLowerCase() || '';
|
|
2708
2709
|
const isImage = ['png', 'jpg', 'jpeg', 'gif', 'webp'].includes(ext);
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
//
|
|
2717
|
-
//
|
|
2718
|
-
//
|
|
2719
|
-
//
|
|
2720
|
-
//
|
|
2721
|
-
|
|
2722
|
-
|
|
2710
|
+
const mimeByExt = {
|
|
2711
|
+
png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg',
|
|
2712
|
+
gif: 'image/gif', webp: 'image/webp', pdf: 'application/pdf',
|
|
2713
|
+
html: 'text/html', md: 'text/markdown', txt: 'text/plain',
|
|
2714
|
+
json: 'application/json',
|
|
2715
|
+
};
|
|
2716
|
+
const mimeType = mimeByExt[ext] || 'application/octet-stream';
|
|
2717
|
+
// Strategy: upload the file to Supabase Storage via the frontend's
|
|
2718
|
+
// /api/upload route and send back just the URL. This mirrors the
|
|
2719
|
+
// existing frontend→agent attachment flow (where the browser uploads
|
|
2720
|
+
// user attachments to Supabase and passes URLs to the agent). For
|
|
2721
|
+
// the reverse direction we do the same: URLs are ~100 bytes, so
|
|
2722
|
+
// the LiveKit data channel stays healthy regardless of file size.
|
|
2723
|
+
//
|
|
2724
|
+
// Fallback to inline send if OSBORN_FRONTEND_URL isn't configured
|
|
2725
|
+
// OR the upload fails — with a small size cap so we don't kill the
|
|
2726
|
+
// publisher PC with a 480KB payload (see earlier career-ops bug).
|
|
2727
|
+
const FRONTEND_URL = process.env.OSBORN_FRONTEND_URL || process.env.NEXT_PUBLIC_FRONTEND_URL || '';
|
|
2728
|
+
const MAX_INLINE_BYTES = 30_000; // fallback-only cap
|
|
2729
|
+
let uploadedUrl = null;
|
|
2730
|
+
if (FRONTEND_URL) {
|
|
2731
|
+
try {
|
|
2732
|
+
const buf = fs.readFileSync(filePath);
|
|
2733
|
+
const form = new FormData();
|
|
2734
|
+
form.append('file', new Blob([buf], { type: mimeType }), fileName);
|
|
2735
|
+
form.append('folder', 'artifacts');
|
|
2736
|
+
const r = await fetch(`${FRONTEND_URL.replace(/\/$/, '')}/api/upload`, {
|
|
2737
|
+
method: 'POST', body: form,
|
|
2738
|
+
signal: AbortSignal.timeout(15_000),
|
|
2739
|
+
});
|
|
2740
|
+
if (r.ok) {
|
|
2741
|
+
const j = await r.json();
|
|
2742
|
+
if (j.success && j.url) {
|
|
2743
|
+
uploadedUrl = j.url;
|
|
2744
|
+
console.log(`☁️ Uploaded artifact to Supabase: ${fileName} (${(buf.length / 1024).toFixed(0)}KB) → ${j.url.substring(0, 80)}...`);
|
|
2745
|
+
}
|
|
2746
|
+
else {
|
|
2747
|
+
console.warn(`⚠️ Upload failed for ${fileName}: ${j.error || 'unknown'}`);
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
else {
|
|
2751
|
+
console.warn(`⚠️ Upload HTTP ${r.status} for ${fileName}`);
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
catch (err) {
|
|
2755
|
+
console.warn(`⚠️ Upload threw for ${fileName}:`, err.message);
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
if (uploadedUrl) {
|
|
2759
|
+
// Success path — send URL, no inline content.
|
|
2760
|
+
await sendToFrontend({
|
|
2761
|
+
type: 'research_artifact_content',
|
|
2762
|
+
filePath, fileName, url: uploadedUrl,
|
|
2763
|
+
isImage, mimeType,
|
|
2764
|
+
});
|
|
2765
|
+
}
|
|
2766
|
+
else if (isImage) {
|
|
2767
|
+
// Fallback: inline image (with size cap)
|
|
2723
2768
|
const stats = fs.statSync(filePath);
|
|
2724
|
-
const base64Size = Math.ceil(stats.size * 4 / 3);
|
|
2725
|
-
if (base64Size >
|
|
2726
|
-
console.log(`⚠️ Artifact too large for
|
|
2769
|
+
const base64Size = Math.ceil(stats.size * 4 / 3);
|
|
2770
|
+
if (base64Size > MAX_INLINE_BYTES) {
|
|
2771
|
+
console.log(`⚠️ Artifact too large for inline fallback: ${fileName} (${(base64Size / 1024).toFixed(0)}KB base64) — sending truncation notice`);
|
|
2727
2772
|
await sendToFrontend({ type: 'research_artifact_content', filePath, content: '', fileName, isImage: false, truncated: true, originalSize: stats.size });
|
|
2728
2773
|
}
|
|
2729
2774
|
else {
|
|
2730
2775
|
const base64 = fs.readFileSync(filePath, 'base64');
|
|
2731
|
-
await sendToFrontend({ type: 'research_artifact_content', filePath, content: base64, fileName, isImage: true, mimeType
|
|
2776
|
+
await sendToFrontend({ type: 'research_artifact_content', filePath, content: base64, fileName, isImage: true, mimeType });
|
|
2732
2777
|
}
|
|
2733
2778
|
}
|
|
2734
2779
|
else {
|
|
2780
|
+
// Fallback: inline text (with size cap)
|
|
2735
2781
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
2736
|
-
if (Buffer.byteLength(content, 'utf-8') >
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
console.log(`⚠️ Artifact too large for data channel: ${fileName} (${(Buffer.byteLength(content, 'utf-8') / 1024).toFixed(0)}KB) — sending truncated preview`);
|
|
2782
|
+
if (Buffer.byteLength(content, 'utf-8') > MAX_INLINE_BYTES) {
|
|
2783
|
+
const truncated = content.substring(0, 5_000);
|
|
2784
|
+
console.log(`⚠️ Artifact too large for inline fallback: ${fileName} (${(Buffer.byteLength(content, 'utf-8') / 1024).toFixed(0)}KB) — sending truncated preview`);
|
|
2740
2785
|
await sendToFrontend({ type: 'research_artifact_content', filePath, content: truncated, fileName, isImage: false, truncated: true, originalSize: Buffer.byteLength(content, 'utf-8') });
|
|
2741
2786
|
}
|
|
2742
2787
|
else {
|