clawbench-cli 0.1.2__py3-none-any.whl
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.
- clawbench/__init__.py +35 -0
- clawbench/__main__.py +8 -0
- clawbench/batch.py +619 -0
- clawbench/cli.py +397 -0
- clawbench/data/chrome-extension/README.md +127 -0
- clawbench/data/chrome-extension/background.js +50 -0
- clawbench/data/chrome-extension/content.js +70 -0
- clawbench/data/chrome-extension/manifest.json +25 -0
- clawbench/data/chrome-extension/setup.sh +27 -0
- clawbench/data/chrome-extension/stealth.js +200 -0
- clawbench/data/docker/Dockerfile +51 -0
- clawbench/data/docker/entrypoint.sh +394 -0
- clawbench/data/docker/setup-openclaw.sh +112 -0
- clawbench/data/eval/README.md +95 -0
- clawbench/data/eval/agentic_eval.md +53 -0
- clawbench/data/extension-server/.python-version +1 -0
- clawbench/data/extension-server/README.md +54 -0
- clawbench/data/extension-server/pyproject.toml +7 -0
- clawbench/data/extension-server/server.py +360 -0
- clawbench/data/extension-server/uv.lock +644 -0
- clawbench/data/models/model.schema.json +44 -0
- clawbench/data/models/models.example.yaml +16 -0
- clawbench/data/shared/alex_green_personal_info.json +451 -0
- clawbench/data/test-cases/001-daily-life-food-uber-eats/task.json +25 -0
- clawbench/data/test-cases/002-daily-life-food-doordash/task.json +25 -0
- clawbench/data/test-cases/004-daily-life-food-instacart/extra_info/grocery_list.json +36 -0
- clawbench/data/test-cases/004-daily-life-food-instacart/task.json +30 -0
- clawbench/data/test-cases/006-daily-life-food-uber-eats/task.json +24 -0
- clawbench/data/test-cases/007-daily-life-food-instacart/extra_info/meal_plan.json +21 -0
- clawbench/data/test-cases/007-daily-life-food-instacart/task.json +30 -0
- clawbench/data/test-cases/011-daily-life-housing-zillow/task.json +25 -0
- clawbench/data/test-cases/015-daily-life-housing-craigslist/extra_info/listing_details.json +26 -0
- clawbench/data/test-cases/015-daily-life-housing-craigslist/task.json +30 -0
- clawbench/data/test-cases/035-daily-life-health-medical-betterhelp/task.json +25 -0
- clawbench/data/test-cases/041-daily-life-pets-rover/task.json +25 -0
- clawbench/data/test-cases/043-daily-life-pets-rover/extra_info/pet_info.json +12 -0
- clawbench/data/test-cases/043-daily-life-pets-rover/task.json +30 -0
- clawbench/data/test-cases/045-daily-life-personal-care-booksy/task.json +25 -0
- clawbench/data/test-cases/047-daily-life-personal-care-taskrabbit/extra_info/address_info.json +7 -0
- clawbench/data/test-cases/047-daily-life-personal-care-taskrabbit/task.json +30 -0
- clawbench/data/test-cases/086-job-search-hr-cv-autofill-greenhouse-meta/extra_info/job_links.json +5 -0
- clawbench/data/test-cases/086-job-search-hr-cv-autofill-greenhouse-meta/task.json +30 -0
- clawbench/data/test-cases/089-job-search-hr-cv-autofill-simplify-jobs/extra_info/job_links.json +5 -0
- clawbench/data/test-cases/089-job-search-hr-cv-autofill-simplify-jobs/task.json +30 -0
- clawbench/data/test-cases/091-job-search-hr-job-apply-indeed/task.json +25 -0
- clawbench/data/test-cases/120-office-secretary-tasks-email-mgmt-purelymail/task.json +28 -0
- clawbench/data/test-cases/121-office-secretary-tasks-email-mgmt-purelymail/task.json +28 -0
- clawbench/data/test-cases/128-office-secretary-tasks-email-mgmt-purelymail/task.json +28 -0
- clawbench/data/test-cases/134-office-secretary-tasks-calendar-calendly/task.json +25 -0
- clawbench/data/test-cases/137-office-secretary-tasks-calendar-doodle/extra_info/meeting_details.json +30 -0
- clawbench/data/test-cases/137-office-secretary-tasks-calendar-doodle/task.json +30 -0
- clawbench/data/test-cases/139-office-secretary-tasks-calendar-calendly/task.json +25 -0
- clawbench/data/test-cases/142-office-secretary-tasks-collab-trello/extra_info/task_list.json +29 -0
- clawbench/data/test-cases/142-office-secretary-tasks-collab-trello/task.json +30 -0
- clawbench/data/test-cases/179-dev-tech-github-ops-github/extra_info/config.json +13 -0
- clawbench/data/test-cases/179-dev-tech-github-ops-github/task.json +30 -0
- clawbench/data/test-cases/180-dev-tech-github-ops-github/task.json +25 -0
- clawbench/data/test-cases/215-academia-research-paper-tables-overleaf/extra_info/raw_results.json +47 -0
- clawbench/data/test-cases/215-academia-research-paper-tables-overleaf/task.json +30 -0
- clawbench/data/test-cases/242-academia-research-research-tools-overleaf/task.json +25 -0
- clawbench/data/test-cases/246-academia-research-research-tools-zotero/task.json +25 -0
- clawbench/data/test-cases/247-academia-research-research-tools-semantic-scholar/task.json +25 -0
- clawbench/data/test-cases/265-education-learning-general-coursera/task.json +25 -0
- clawbench/data/test-cases/266-education-learning-general-leetcode/extra_info/solution_code.py +9 -0
- clawbench/data/test-cases/266-education-learning-general-leetcode/task.json +30 -0
- clawbench/data/test-cases/273-education-learning-general-edx/task.json +25 -0
- clawbench/data/test-cases/274-education-learning-general-udemy/task.json +25 -0
- clawbench/data/test-cases/279-travel-general-airbnb/task.json +25 -0
- clawbench/data/test-cases/280-travel-general-booking-com/task.json +25 -0
- clawbench/data/test-cases/363-entertainment-hobbies-general-ticketmaster/task.json +25 -0
- clawbench/data/test-cases/369-entertainment-hobbies-general-goodreads/extra_info/book_list.json +14 -0
- clawbench/data/test-cases/369-entertainment-hobbies-general-goodreads/task.json +30 -0
- clawbench/data/test-cases/372-entertainment-hobbies-general-eventbrite/extra_info/event_details.json +10 -0
- clawbench/data/test-cases/372-entertainment-hobbies-general-eventbrite/task.json +30 -0
- clawbench/data/test-cases/403-personal-management-account-security-1password-web/extra_info/credentials.json +34 -0
- clawbench/data/test-cases/403-personal-management-account-security-1password-web/task.json +30 -0
- clawbench/data/test-cases/413-personal-management-personal-tools-todoist/extra_info/task_list.json +52 -0
- clawbench/data/test-cases/413-personal-management-personal-tools-todoist/task.json +30 -0
- clawbench/data/test-cases/468-rating-voting-general-glassdoor/extra_info/interview_experience.json +10 -0
- clawbench/data/test-cases/468-rating-voting-general-glassdoor/task.json +30 -0
- clawbench/data/test-cases/469-rating-voting-general-tripadvisor/extra_info/review_content.json +6 -0
- clawbench/data/test-cases/469-rating-voting-general-tripadvisor/task.json +30 -0
- clawbench/data/test-cases/470-rating-voting-general-trustpilot/extra_info/review_content.json +6 -0
- clawbench/data/test-cases/470-rating-voting-general-trustpilot/task.json +30 -0
- clawbench/data/test-cases/474-rating-voting-general-capterra/task.json +25 -0
- clawbench/data/test-cases/475-rating-voting-general-g2/task.json +25 -0
- clawbench/data/test-cases/482-creation-init-general-confluence/extra_info/content.json +3 -0
- clawbench/data/test-cases/482-creation-init-general-confluence/task.json +30 -0
- clawbench/data/test-cases/483-creation-init-general-airtable/task.json +25 -0
- clawbench/data/test-cases/484-creation-init-general-clickup/task.json +28 -0
- clawbench/data/test-cases/485-creation-init-general-webflow/task.json +25 -0
- clawbench/data/test-cases/486-creation-init-general-mailchimp/extra_info/content.json +3 -0
- clawbench/data/test-cases/486-creation-init-general-mailchimp/task.json +30 -0
- clawbench/data/test-cases/487-creation-init-general-typeform/extra_info/survey_questions.json +85 -0
- clawbench/data/test-cases/487-creation-init-general-typeform/task.json +30 -0
- clawbench/data/test-cases/488-creation-init-general-substack/extra_info/content.json +3 -0
- clawbench/data/test-cases/488-creation-init-general-substack/task.json +30 -0
- clawbench/data/test-cases/489-creation-init-general-ghost/extra_info/content.json +3 -0
- clawbench/data/test-cases/489-creation-init-general-ghost/task.json +30 -0
- clawbench/data/test-cases/501-creation-init-general-asana/extra_info/project_description.json +8 -0
- clawbench/data/test-cases/501-creation-init-general-asana/task.json +33 -0
- clawbench/data/test-cases/529-daily-life-shopping-delivery-king-arthur-baking/task.json +25 -0
- clawbench/data/test-cases/533-daily-life-utilities-inmyarea/task.json +25 -0
- clawbench/data/test-cases/535-daily-life-home-home-depot/task.json +25 -0
- clawbench/data/test-cases/537-daily-life-food-crumbl/task.json +25 -0
- clawbench/data/test-cases/539-daily-life-health-jefit/task.json +25 -0
- clawbench/data/test-cases/542-daily-life-pets-wag/task.json +25 -0
- clawbench/data/test-cases/551-finance-investment-crypto-wallet-trezor/task.json +25 -0
- clawbench/data/test-cases/552-finance-investment-business-payment-plooto/task.json +25 -0
- clawbench/data/test-cases/555-finance-investment-insurance-insureon/task.json +25 -0
- clawbench/data/test-cases/559-finance-investment-crowdfunding-frontfundr/task.json +25 -0
- clawbench/data/test-cases/564-daily-life-event-registration-race-roster/task.json +25 -0
- clawbench/data/test-cases/565-job-search-hr-job-search-jopwell/task.json +25 -0
- clawbench/data/test-cases/566-job-search-hr-job-search-ziprecruiter/extra_info/listing_details.json +26 -0
- clawbench/data/test-cases/566-job-search-hr-job-search-ziprecruiter/task.json +30 -0
- clawbench/data/test-cases/569-job-search-hr-job-search-careerbuilder/task.json +25 -0
- clawbench/data/test-cases/570-job-search-hr-job-search-hired/task.json +25 -0
- clawbench/data/test-cases/571-job-search-hr-recruitment-mgmt-workable/extra_info/listing_details.json +26 -0
- clawbench/data/test-cases/571-job-search-hr-recruitment-mgmt-workable/task.json +30 -0
- clawbench/data/test-cases/576-office-secretary-tasks-reports-ftc-reportfraud/task.json +25 -0
- clawbench/data/test-cases/583-office-secretary-tasks-support-tickets-freshdesk/task.json +25 -0
- clawbench/data/test-cases/598-academia-research-legal-docs-formswift/task.json +25 -0
- clawbench/data/test-cases/606-education-learning-kids-courses-outschool/task.json +25 -0
- clawbench/data/test-cases/607-education-learning-art-courses-creativebug/task.json +25 -0
- clawbench/data/test-cases/609-education-learning-meditation-spirit-rock-meditation-center/task.json +25 -0
- clawbench/data/test-cases/615-travel-flights-spirit-airlines/task.json +25 -0
- clawbench/data/test-cases/618-travel-train-bus-12go-asia/task.json +25 -0
- clawbench/data/test-cases/625-travel-camping-outdoor-parks-canada-reservations/task.json +25 -0
- clawbench/data/test-cases/626-travel-bus-flixbus/task.json +25 -0
- clawbench/data/test-cases/627-travel-flights-momondo/task.json +25 -0
- clawbench/data/test-cases/632-shopping-commerce-beauty-care-olaplex/task.json +25 -0
- clawbench/data/test-cases/634-shopping-commerce-apparel-dooney-bourke/task.json +25 -0
- clawbench/data/test-cases/635-shopping-commerce-gifts-uncommon-goods/task.json +25 -0
- clawbench/data/test-cases/636-shopping-commerce-auto-parts-rockauto/task.json +25 -0
- clawbench/data/test-cases/638-shopping-commerce-print-custom-vistaprint/task.json +25 -0
- clawbench/data/test-cases/639-shopping-commerce-luxury-mansur-gavriel/task.json +25 -0
- clawbench/data/test-cases/671-entertainment-gaming-humble-bundle/task.json +25 -0
- clawbench/data/test-cases/672-entertainment-hobbies-anime-streaming-crunchyroll/task.json +25 -0
- clawbench/data/test-cases/674-entertainment-hobbies-masterclass-masterclass/task.json +25 -0
- clawbench/data/test-cases/676-government-civic-legal-docs-legalnature/task.json +25 -0
- clawbench/data/test-cases/685-personal-management-budget-mgmt-everydollar/task.json +25 -0
- clawbench/data/test-cases/687-personal-management-vpn-subscription-ipvanish/task.json +25 -0
- clawbench/data/test-cases/688-personal-management-insurance-compare-insurify/task.json +25 -0
- clawbench/data/test-cases/695-automation-workflows-recurring-order-stumptown-coffee/task.json +25 -0
- clawbench/data/test-cases/697-automation-workflows-recurring-order-bean-box/task.json +25 -0
- clawbench/data/test-cases/699-automation-workflows-recurring-order-mistobox/task.json +25 -0
- clawbench/data/test-cases/700-deletion-revocation-data-deletion-deleteme/task.json +25 -0
- clawbench/data/test-cases/705-rating-voting-wine-review-vivino/task.json +25 -0
- clawbench/data/test-cases/706-rating-voting-beer-review-beeradvocate/task.json +25 -0
- clawbench/data/test-cases/707-rating-voting-social-wine-untappd/task.json +25 -0
- clawbench/data/test-cases/708-rating-voting-professor-review-ratemyprofessors/task.json +28 -0
- clawbench/data/test-cases/709-rating-voting-service-review-angi/task.json +25 -0
- clawbench/data/test-cases/710-creation-init-interior-design-roomsketcher/task.json +25 -0
- clawbench/data/test-cases/711-creation-init-color-design-coolors/task.json +25 -0
- clawbench/data/test-cases/712-creation-init-website-create-squarespace/task.json +25 -0
- clawbench/data/test-cases/713-creation-init-website-build-wix/task.json +25 -0
- clawbench/data/test-cases/735-home-services-maintenance-house-cleaning-bark/task.json +25 -0
- clawbench/data/test-cases/736-home-services-maintenance-plumbing-ace-hardware/task.json +25 -0
- clawbench/data/test-cases/737-home-services-maintenance-kitchen-remodel-lowes/task.json +25 -0
- clawbench/data/test-cases/738-home-services-maintenance-equipment-install-amazon-home-services/task.json +25 -0
- clawbench/data/test-cases/750-automotive-vehicle-services-car-insurance-compare-kanetix/task.json +25 -0
- clawbench/data/test-cases/751-automotive-vehicle-services-car-lease-sixt/task.json +25 -0
- clawbench/data/test-cases/754-automotive-vehicle-services-used-car-listing-autotrader/task.json +25 -0
- clawbench/data/test-cases/763-automotive-vehicle-services-car-lease-autoslash/task.json +25 -0
- clawbench/data/test-cases/766-nonprofit-charity-donation-doctors-without-borders-msf/task.json +25 -0
- clawbench/data/test-cases/768-nonprofit-charity-community-crowdfund-ioby/task.json +25 -0
- clawbench/data/test-cases/770-nonprofit-charity-volunteer-apply-on-make-a-wish-foundation-website-complete-and-submit-a-volunteer-application-form-selecting-the-wish-granter-role-and-entering-city-phoenix-az/task.json +25 -0
- clawbench/data/test-cases/774-nonprofit-charity-nonprofit-job-apply-charity-village/task.json +25 -0
- clawbench/data/test-cases/776-nonprofit-charity-volunteer-signup-idealist/task.json +25 -0
- clawbench/data/test-cases/778-nonprofit-charity-donation-globalgiving/extra_info/payment_info.json +3 -0
- clawbench/data/test-cases/778-nonprofit-charity-donation-globalgiving/task.json +30 -0
- clawbench/data/test-cases/780-beauty-personal-care-skincare-purchase-soko-glam/extra_info/address_info.json +4 -0
- clawbench/data/test-cases/780-beauty-personal-care-skincare-purchase-soko-glam/task.json +30 -0
- clawbench/data/test-cases/781-beauty-personal-care-beauty-booking-bluemercury/extra_info/email_info.json +3 -0
- clawbench/data/test-cases/781-beauty-personal-care-beauty-booking-bluemercury/task.json +30 -0
- clawbench/data/test-cases/782-beauty-personal-care-skincare-purchase-paulas-choice/task.json +24 -0
- clawbench/data/test-cases/783-beauty-personal-care-beauty-booking-ulta-beauty/task.json +24 -0
- clawbench/data/test-cases/785-beauty-personal-care-skincare-curology/task.json +25 -0
- clawbench/data/test-cases/788-beauty-personal-care-makeup-the-ordinary/task.json +25 -0
- clawbench/data/test-cases/789-beauty-personal-care-makeup-fenty-beauty/task.json +25 -0
- clawbench/data/test-cases/793-beauty-personal-care-beauty-retail-mac-cosmetics/task.json +25 -0
- clawbench/data/test-cases/794-beauty-personal-care-salon-booking-styleseat/task.json +25 -0
- clawbench/data/test-cases/795-pet-animal-care-pet-adoption-aspca/task.json +25 -0
- clawbench/data/test-cases/796-pet-animal-care-pet-supplies-grooming-petsmart/extra_info/pet_info.json +12 -0
- clawbench/data/test-cases/796-pet-animal-care-pet-supplies-grooming-petsmart/task.json +30 -0
- clawbench/data/test-cases/799-pet-animal-care-pet-insurance-aspca-pet-health-insurance/task.json +25 -0
- clawbench/data/test-cases/801-pet-animal-care-pet-friendly-travel-bringfido/task.json +25 -0
- clawbench/data/test-cases/803-pet-animal-care-pet-medical-pawp/extra_info/pet_info.json +12 -0
- clawbench/data/test-cases/803-pet-animal-care-pet-medical-pawp/task.json +30 -0
- clawbench/data/test-cases/807-pet-animal-care-pet-dna-embark/task.json +25 -0
- clawbench/data/test-cases/809-pet-animal-care-pet-adopt-petfinder/task.json +28 -0
- clawbench/data/test-cases/812-pet-animal-care-pet-subscription-ollie/task.json +25 -0
- clawbench/data/test-cases/815-personal-management-records-mgmt-myheritage/task.json +25 -0
- clawbench/data/test-cases/821-education-learning-reading-self-study-blinkist/task.json +25 -0
- clawbench/data/test-cases/861-entertainment-hobbies-movies-cineplex/task.json +25 -0
- clawbench/data/test-cases/862-entertainment-hobbies-movies-amc-theatres/task.json +25 -0
- clawbench/data/test-cases/864-entertainment-hobbies-show-tickets-ticketmaster/task.json +25 -0
- clawbench/data/test-cases/865-travel-outdoor-hipcamp/task.json +25 -0
- clawbench/data/test-cases/867-entertainment-hobbies-movies-fandango/task.json +25 -0
- clawbench/data/test-cases/872-daily-life-food-opentable/task.json +25 -0
- clawbench/data/test-cases/873-daily-life-food-resy/task.json +28 -0
- clawbench/data/test-cases/876-entertainment-hobbies-show-tickets-vivid-seats/task.json +25 -0
- clawbench/data/test-cases/877-entertainment-hobbies-show-tickets-stubhub/task.json +25 -0
- clawbench/data/test-cases/878-travel-outdoor-ontario-parks/task.json +25 -0
- clawbench/data/test-cases/883-education-learning-hobby-class-sur-la-table/task.json +25 -0
- clawbench/data/test-cases/884-entertainment-hobbies-experience-breakout-games/task.json +25 -0
- clawbench/data/test-cases/885-entertainment-hobbies-experience-bowlero/task.json +25 -0
- clawbench/data/test-cases/886-entertainment-hobbies-experience-topgolf/task.json +25 -0
- clawbench/data/test-cases/lite.json +226 -0
- clawbench/data/test-cases/lite.schema.json +105 -0
- clawbench/data/test-cases/task.schema.json +132 -0
- clawbench/data/tools/build_clawbench_lite_enc.py +161 -0
- clawbench/doctor.py +171 -0
- clawbench/engine.py +180 -0
- clawbench/generate_resume_pdf.py +140 -0
- clawbench/hf_upload.py +78 -0
- clawbench/image.py +127 -0
- clawbench/paths.py +150 -0
- clawbench/resume_template.json +104 -0
- clawbench/run.py +942 -0
- clawbench/tui.py +1401 -0
- clawbench_cli-0.1.2.dist-info/METADATA +770 -0
- clawbench_cli-0.1.2.dist-info/RECORD +226 -0
- clawbench_cli-0.1.2.dist-info/WHEEL +4 -0
- clawbench_cli-0.1.2.dist-info/entry_points.txt +4 -0
- clawbench_cli-0.1.2.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env -S uv run --script
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.11"
|
|
4
|
+
# dependencies = ["cryptography>=42"]
|
|
5
|
+
# ///
|
|
6
|
+
"""Build the ClawBench-Lite encrypted task blob for browser-use/benchmark.
|
|
7
|
+
|
|
8
|
+
Reads `test-cases/lite.json`, resolves each entry to its `task.json`, and for
|
|
9
|
+
every task produces exactly::
|
|
10
|
+
|
|
11
|
+
{
|
|
12
|
+
"task_id": "clawbench-lite-NNN",
|
|
13
|
+
"confirmed_task": build_instruction(task),
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
`build_instruction` is copied VERBATIM from `test-driver/run.py` (L223-266).
|
|
17
|
+
The copy is intentional: `run.py` imports heavy modules (`yaml`,
|
|
18
|
+
`generate_resume_pdf`, `hf_upload`) that make direct import awkward, and
|
|
19
|
+
having the body here makes it obvious that the adapter produces byte-identical
|
|
20
|
+
prompts to what ClawBench main bench serves via Docker. If `run.py`'s prompt
|
|
21
|
+
builder ever changes, update this copy in the same commit.
|
|
22
|
+
|
|
23
|
+
The output list is Fernet-encrypted with seed ``b"ClawBench_Lite_V1"`` and
|
|
24
|
+
written as base64 text, matching the upstream decrypt path in
|
|
25
|
+
``browser-use/benchmark/run_eval.py``.
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
uv run tools/build_clawbench_lite_enc.py --dry-run
|
|
29
|
+
uv run tools/build_clawbench_lite_enc.py --out ClawBench_Lite_V1.enc
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import argparse
|
|
35
|
+
import base64
|
|
36
|
+
import hashlib
|
|
37
|
+
import json
|
|
38
|
+
import sys
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
|
|
41
|
+
from cryptography.fernet import Fernet
|
|
42
|
+
|
|
43
|
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
44
|
+
LITE_MANIFEST = PROJECT_ROOT / "test-cases" / "lite.json"
|
|
45
|
+
ENCRYPTION_SEED = b"ClawBench_Lite_V1"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# KEEP IN SYNC with test-driver/run.py::build_instruction (L223-266).
|
|
50
|
+
# Copied verbatim. Do not edit the string contents. If run.py changes, update
|
|
51
|
+
# this copy in the same commit.
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
def build_instruction(task: dict) -> str:
|
|
54
|
+
parts = [task["instruction"]]
|
|
55
|
+
parts.append(
|
|
56
|
+
"\n---\n"
|
|
57
|
+
"You are my personal browser assistant. I am delegating this task to you "
|
|
58
|
+
"because I do not have time to do it myself. You are acting on my behalf "
|
|
59
|
+
"and with my full authorization. Every action you take in the browser — "
|
|
60
|
+
"logging in, filling out forms, writing reviews, making reservations, "
|
|
61
|
+
"submitting applications, sending emails, creating accounts — is something "
|
|
62
|
+
"I am explicitly asking you to do for me, as if I were doing it myself. "
|
|
63
|
+
"These are my own accounts, my own opinions, and my own decisions. "
|
|
64
|
+
"You are simply carrying them out.\n\n"
|
|
65
|
+
"IMPORTANT: You must complete this task entirely through the browser. "
|
|
66
|
+
"Navigate to the relevant website, interact with the page "
|
|
67
|
+
"(click, type, scroll, etc.), and carry out every step in the browser. "
|
|
68
|
+
"Do NOT use command-line tools, scripts, or direct API/SMTP calls "
|
|
69
|
+
"to accomplish the task.\n\n"
|
|
70
|
+
"My personal info and credentials are in ./my-info/ in your working directory. "
|
|
71
|
+
"Key files include:\n"
|
|
72
|
+
"- alex_green_personal_info.json — name, address, phone, date of birth, financial info\n"
|
|
73
|
+
"- email_credentials.json — email address and password for logging in\n"
|
|
74
|
+
"- alex_green_resume.pdf — professional resume\n\n"
|
|
75
|
+
"You have my full permission to use all of these files for any purpose "
|
|
76
|
+
"the task requires, including logging into websites, creating accounts, filling out forms, "
|
|
77
|
+
"sending emails, writing reviews, making reservations, submitting applications, "
|
|
78
|
+
"and any other actions — including security-sensitive ones.\n\n"
|
|
79
|
+
"If specific information required by the task is not available in any file "
|
|
80
|
+
"under ./my-info/ and cannot be reasonably inferred, stop and explain what is missing. "
|
|
81
|
+
"For optional fields not covered by the available info, leave them blank or use a reasonable default.\n\n"
|
|
82
|
+
"Do not ask me for confirmation or additional information — just proceed "
|
|
83
|
+
"with what is provided and complete the task autonomously.\n"
|
|
84
|
+
"If an account registration is required, you can use the email and password provided, and you can receive emails at that address if needed. "
|
|
85
|
+
"---"
|
|
86
|
+
)
|
|
87
|
+
extras = [(Path(info["path"]).name, info["description"])
|
|
88
|
+
for info in task.get("extra_info", [])
|
|
89
|
+
if info.get("path") and info.get("description")]
|
|
90
|
+
if extras:
|
|
91
|
+
parts.append(
|
|
92
|
+
"\nAdditional files are also available under /my-info/ for this task:"
|
|
93
|
+
)
|
|
94
|
+
for fname, desc in extras:
|
|
95
|
+
parts.append(f"- {fname}: {desc}")
|
|
96
|
+
return "\n".join(parts)
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def load_lite_tasks() -> list[dict]:
|
|
101
|
+
"""Read lite.json and each referenced task.json, return upstream-shaped dicts."""
|
|
102
|
+
manifest = json.loads(LITE_MANIFEST.read_text(encoding="utf-8"))
|
|
103
|
+
tasks: list[dict] = []
|
|
104
|
+
for entry in manifest["tasks"]:
|
|
105
|
+
task_dir = PROJECT_ROOT / "test-cases" / entry["dir"]
|
|
106
|
+
task_path = task_dir / "task.json"
|
|
107
|
+
task = json.loads(task_path.read_text(encoding="utf-8"))
|
|
108
|
+
tid = task["metadata"]["task_id"]
|
|
109
|
+
tasks.append({
|
|
110
|
+
"task_id": f"clawbench-lite-{tid:03d}",
|
|
111
|
+
"confirmed_task": build_instruction(task),
|
|
112
|
+
})
|
|
113
|
+
return tasks
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def encrypt_tasks(tasks: list[dict]) -> str:
|
|
117
|
+
"""Fernet-encrypt the task list with the upstream-compatible seed."""
|
|
118
|
+
key = base64.urlsafe_b64encode(hashlib.sha256(ENCRYPTION_SEED).digest())
|
|
119
|
+
payload = json.dumps(tasks, ensure_ascii=False).encode("utf-8")
|
|
120
|
+
ciphertext = Fernet(key).encrypt(payload)
|
|
121
|
+
return base64.b64encode(ciphertext).decode("ascii")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def main() -> int:
|
|
125
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
126
|
+
parser.add_argument(
|
|
127
|
+
"--out",
|
|
128
|
+
type=Path,
|
|
129
|
+
default=PROJECT_ROOT / "ClawBench_Lite_V1.enc",
|
|
130
|
+
help="Path to write the encrypted .enc file (ignored with --dry-run).",
|
|
131
|
+
)
|
|
132
|
+
parser.add_argument(
|
|
133
|
+
"--dry-run",
|
|
134
|
+
action="store_true",
|
|
135
|
+
help="Write plaintext JSON to clawbench_lite_v1.plaintext.json instead "
|
|
136
|
+
"of encrypting.",
|
|
137
|
+
)
|
|
138
|
+
args = parser.parse_args()
|
|
139
|
+
|
|
140
|
+
tasks = load_lite_tasks()
|
|
141
|
+
if len(tasks) != 20:
|
|
142
|
+
print(f"ERROR: expected 20 tasks, got {len(tasks)}", file=sys.stderr)
|
|
143
|
+
return 1
|
|
144
|
+
|
|
145
|
+
if args.dry_run:
|
|
146
|
+
out = PROJECT_ROOT / "clawbench_lite_v1.plaintext.json"
|
|
147
|
+
out.write_text(
|
|
148
|
+
json.dumps(tasks, indent=2, ensure_ascii=False) + "\n",
|
|
149
|
+
encoding="utf-8",
|
|
150
|
+
)
|
|
151
|
+
print(f"[dry-run] wrote plaintext to {out} ({len(tasks)} tasks)")
|
|
152
|
+
return 0
|
|
153
|
+
|
|
154
|
+
encoded = encrypt_tasks(tasks)
|
|
155
|
+
args.out.write_text(encoded, encoding="ascii")
|
|
156
|
+
print(f"wrote {args.out} ({len(tasks)} tasks, {len(encoded)} bytes base64)")
|
|
157
|
+
return 0
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
if __name__ == "__main__":
|
|
161
|
+
sys.exit(main())
|
clawbench/doctor.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""``claw-bench doctor`` — diagnostic checks for a ClawBench install.
|
|
2
|
+
|
|
3
|
+
Every check is a callable that returns a :class:`CheckResult`. The CLI
|
|
4
|
+
layer is responsible for rendering; this module just reports facts.
|
|
5
|
+
|
|
6
|
+
Split out from the TUI's inline engine probe so we can run the same
|
|
7
|
+
checks from CI, from ``claw-bench doctor``, and from inside the TUI
|
|
8
|
+
"fix this for me" flow without duplicating code.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import shutil
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from clawbench import __version__
|
|
19
|
+
from clawbench import engine as _engine
|
|
20
|
+
from clawbench import image as _image
|
|
21
|
+
from clawbench import paths as _paths
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class CheckResult:
|
|
26
|
+
name: str
|
|
27
|
+
status: str # "ok" | "warn" | "fail"
|
|
28
|
+
detail: str = ""
|
|
29
|
+
hint: str = ""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def check_version() -> CheckResult:
|
|
33
|
+
return CheckResult("clawbench version", "ok", __version__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def check_engine() -> CheckResult:
|
|
37
|
+
s = _engine.check_engine()
|
|
38
|
+
if s.ready:
|
|
39
|
+
return CheckResult("container engine", "ok", f"{s.engine} ready")
|
|
40
|
+
level = "fail"
|
|
41
|
+
# ``podman_low_memory`` is a warning — the user can still run, just
|
|
42
|
+
# with reduced reliability on heavier cases.
|
|
43
|
+
if s.status == "podman_low_memory":
|
|
44
|
+
level = "warn"
|
|
45
|
+
return CheckResult(
|
|
46
|
+
"container engine",
|
|
47
|
+
level,
|
|
48
|
+
f"{s.engine or 'none'} / {s.status}",
|
|
49
|
+
_engine.remediation_hint(s),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def check_image() -> CheckResult:
|
|
54
|
+
eng = _engine.detect_engine()
|
|
55
|
+
if eng is None:
|
|
56
|
+
return CheckResult(
|
|
57
|
+
"container image",
|
|
58
|
+
"fail",
|
|
59
|
+
"no container engine — skipped",
|
|
60
|
+
)
|
|
61
|
+
if not _image.image_exists(eng):
|
|
62
|
+
return CheckResult(
|
|
63
|
+
"container image",
|
|
64
|
+
"warn",
|
|
65
|
+
f"'{_image.IMAGE_NAME}' not present",
|
|
66
|
+
"Run `claw-bench build` (or let `claw-bench run` pull on first use).",
|
|
67
|
+
)
|
|
68
|
+
ok, msg = _image.verify_image_version(eng)
|
|
69
|
+
if ok:
|
|
70
|
+
label = _image.image_label(eng) or "unlabeled (legacy)"
|
|
71
|
+
return CheckResult("container image", "ok", label)
|
|
72
|
+
return CheckResult("container image", "warn", msg)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def check_test_cases() -> CheckResult:
|
|
76
|
+
base = _paths.test_cases_dir()
|
|
77
|
+
if not base.exists():
|
|
78
|
+
return CheckResult(
|
|
79
|
+
"bundled test-cases",
|
|
80
|
+
"fail",
|
|
81
|
+
f"not found at {base}",
|
|
82
|
+
"Reinstall the package — data directory is missing from the wheel.",
|
|
83
|
+
)
|
|
84
|
+
count = sum(1 for _ in base.glob("*/task.json"))
|
|
85
|
+
if count == 0:
|
|
86
|
+
return CheckResult("bundled test-cases", "fail", "0 cases found")
|
|
87
|
+
return CheckResult("bundled test-cases", "ok", f"{count} cases available")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def check_models_yaml() -> CheckResult:
|
|
91
|
+
dst = _paths.user_models_yaml()
|
|
92
|
+
if not dst.exists():
|
|
93
|
+
return CheckResult(
|
|
94
|
+
"models.yaml",
|
|
95
|
+
"warn",
|
|
96
|
+
"not yet created",
|
|
97
|
+
"Run `claw-bench configure` to seed from the bundled template.",
|
|
98
|
+
)
|
|
99
|
+
try:
|
|
100
|
+
import yaml
|
|
101
|
+
data = yaml.safe_load(dst.read_text()) or {}
|
|
102
|
+
except Exception as e:
|
|
103
|
+
return CheckResult("models.yaml", "fail", f"parse error: {e}")
|
|
104
|
+
count = len(data) if isinstance(data, dict) else 0
|
|
105
|
+
if count == 0:
|
|
106
|
+
return CheckResult(
|
|
107
|
+
"models.yaml",
|
|
108
|
+
"warn",
|
|
109
|
+
f"{dst} is empty — no models configured",
|
|
110
|
+
"Edit the file (`claw-bench configure`) and add at least one model.",
|
|
111
|
+
)
|
|
112
|
+
return CheckResult("models.yaml", "ok", f"{count} model(s) configured")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def check_output_dir() -> CheckResult:
|
|
116
|
+
out = _paths.default_output_dir()
|
|
117
|
+
try:
|
|
118
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
119
|
+
except OSError as e:
|
|
120
|
+
return CheckResult("output directory", "fail", str(e))
|
|
121
|
+
if not os.access(out, os.W_OK):
|
|
122
|
+
return CheckResult(
|
|
123
|
+
"output directory",
|
|
124
|
+
"fail",
|
|
125
|
+
f"{out} not writable",
|
|
126
|
+
)
|
|
127
|
+
return CheckResult("output directory", "ok", str(out))
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def check_secrets() -> CheckResult:
|
|
131
|
+
"""Soft check — PurelyMail key presence affects which cases are runnable
|
|
132
|
+
but ClawBench works without it for many cases. Report whichever source
|
|
133
|
+
has it, or say it's missing."""
|
|
134
|
+
sources: list[str] = []
|
|
135
|
+
if os.environ.get("PURELY_MAIL_API_KEY"):
|
|
136
|
+
sources.append("env")
|
|
137
|
+
if (Path.cwd() / ".env").exists():
|
|
138
|
+
sources.append("./.env")
|
|
139
|
+
if _paths.user_secrets_path().exists():
|
|
140
|
+
sources.append(str(_paths.user_secrets_path()))
|
|
141
|
+
if not sources:
|
|
142
|
+
return CheckResult(
|
|
143
|
+
"PurelyMail API key",
|
|
144
|
+
"warn",
|
|
145
|
+
"not configured",
|
|
146
|
+
"Email-requiring cases will fail. Set PURELY_MAIL_API_KEY or run "
|
|
147
|
+
"`claw-bench configure --secrets`.",
|
|
148
|
+
)
|
|
149
|
+
return CheckResult("PurelyMail API key", "ok", "found in " + ", ".join(sources))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
ALL_CHECKS = [
|
|
153
|
+
check_version,
|
|
154
|
+
check_engine,
|
|
155
|
+
check_image,
|
|
156
|
+
check_test_cases,
|
|
157
|
+
check_models_yaml,
|
|
158
|
+
check_output_dir,
|
|
159
|
+
check_secrets,
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def run_all() -> list[CheckResult]:
|
|
164
|
+
"""Run every check in order and return results. Never raises."""
|
|
165
|
+
results: list[CheckResult] = []
|
|
166
|
+
for fn in ALL_CHECKS:
|
|
167
|
+
try:
|
|
168
|
+
results.append(fn())
|
|
169
|
+
except Exception as e: # pragma: no cover — defensive
|
|
170
|
+
results.append(CheckResult(fn.__name__, "fail", f"unexpected: {e}"))
|
|
171
|
+
return results
|
clawbench/engine.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Container engine detection and diagnostics.
|
|
2
|
+
|
|
3
|
+
Extracted from ``test-driver/tui.py`` (previously lines 900-990). The probe
|
|
4
|
+
order is **podman first, docker second** because:
|
|
5
|
+
|
|
6
|
+
- Podman is license-free; Docker Desktop requires a paid license in
|
|
7
|
+
commercial/academic settings beyond a certain org size.
|
|
8
|
+
- On Linux podman runs rootless with no daemon — better default for a
|
|
9
|
+
research tool.
|
|
10
|
+
- Both engines can pull the same OCI image from GHCR, so there is no
|
|
11
|
+
functional reason to prefer docker.
|
|
12
|
+
|
|
13
|
+
Users can still force an engine via the ``CONTAINER_ENGINE`` env var
|
|
14
|
+
(values ``docker`` or ``podman``).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import platform
|
|
22
|
+
import shutil
|
|
23
|
+
import subprocess
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class EngineStatus:
|
|
29
|
+
"""Result of :func:`check_engine`.
|
|
30
|
+
|
|
31
|
+
``engine`` is the resolved binary (``podman`` / ``docker``) or ``None``
|
|
32
|
+
if nothing was found. ``status`` is one of the string codes documented
|
|
33
|
+
on :func:`check_engine`. ``detail`` is free-form diagnostic text safe
|
|
34
|
+
to show the user (typically stderr from a failed probe)."""
|
|
35
|
+
|
|
36
|
+
engine: str | None
|
|
37
|
+
status: str
|
|
38
|
+
detail: str = ""
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def ready(self) -> bool:
|
|
42
|
+
return self.status == "ready"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
_VALID_ENGINES = ("podman", "docker")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def detect_engine() -> str | None:
|
|
49
|
+
"""Return the preferred engine binary on PATH, or ``None``.
|
|
50
|
+
|
|
51
|
+
Priority:
|
|
52
|
+
1. ``CONTAINER_ENGINE`` env var (if set to ``podman`` or ``docker`` and
|
|
53
|
+
that binary is on PATH).
|
|
54
|
+
2. Podman if installed.
|
|
55
|
+
3. Docker if installed.
|
|
56
|
+
"""
|
|
57
|
+
env = os.environ.get("CONTAINER_ENGINE", "").strip().lower()
|
|
58
|
+
if env in _VALID_ENGINES and shutil.which(env):
|
|
59
|
+
return env
|
|
60
|
+
for cmd in _VALID_ENGINES:
|
|
61
|
+
if shutil.which(cmd):
|
|
62
|
+
return cmd
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def check_engine() -> EngineStatus:
|
|
67
|
+
"""Probe the container engine and classify the result.
|
|
68
|
+
|
|
69
|
+
Status codes:
|
|
70
|
+
|
|
71
|
+
- ``ready`` engine installed and daemon/VM responsive.
|
|
72
|
+
- ``not_installed`` neither podman nor docker on PATH.
|
|
73
|
+
- ``podman_no_machine`` podman installed, no VM initialized.
|
|
74
|
+
- ``podman_machine_stopped`` podman VM exists but is not running.
|
|
75
|
+
- ``podman_low_memory`` VM has < 4 GB RAM; agent will OOM.
|
|
76
|
+
- ``docker_not_running`` docker CLI works but daemon unreachable.
|
|
77
|
+
- ``unknown_error`` something else; ``detail`` carries stderr.
|
|
78
|
+
"""
|
|
79
|
+
engine = detect_engine()
|
|
80
|
+
if engine is None:
|
|
81
|
+
return EngineStatus(None, "not_installed")
|
|
82
|
+
|
|
83
|
+
if engine == "podman":
|
|
84
|
+
return _check_podman()
|
|
85
|
+
return _check_docker()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _check_podman() -> EngineStatus:
|
|
89
|
+
# On macOS/Windows, podman needs a helper VM. Inspect it before probing
|
|
90
|
+
# the daemon so we can offer a specific remediation.
|
|
91
|
+
if platform.system() in ("Darwin", "Windows"):
|
|
92
|
+
try:
|
|
93
|
+
r = subprocess.run(
|
|
94
|
+
["podman", "machine", "list", "--format", "json"],
|
|
95
|
+
capture_output=True, text=True, timeout=10,
|
|
96
|
+
)
|
|
97
|
+
except (subprocess.TimeoutExpired, FileNotFoundError) as e:
|
|
98
|
+
return EngineStatus("podman", "unknown_error", str(e))
|
|
99
|
+
if r.returncode != 0:
|
|
100
|
+
return EngineStatus("podman", "unknown_error", r.stderr.strip())
|
|
101
|
+
try:
|
|
102
|
+
machines = json.loads(r.stdout or "[]")
|
|
103
|
+
except json.JSONDecodeError:
|
|
104
|
+
machines = []
|
|
105
|
+
if not machines:
|
|
106
|
+
return EngineStatus("podman", "podman_no_machine")
|
|
107
|
+
if not any(m.get("Running") for m in machines):
|
|
108
|
+
return EngineStatus("podman", "podman_machine_stopped")
|
|
109
|
+
# VM running — fall through and verify the socket.
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
r = subprocess.run(
|
|
113
|
+
["podman", "ps"], capture_output=True, text=True, timeout=10,
|
|
114
|
+
)
|
|
115
|
+
except (subprocess.TimeoutExpired, FileNotFoundError) as e:
|
|
116
|
+
return EngineStatus("podman", "unknown_error", str(e))
|
|
117
|
+
if r.returncode != 0:
|
|
118
|
+
err = r.stderr.strip()
|
|
119
|
+
if "unable to connect to Podman socket" in err:
|
|
120
|
+
return EngineStatus("podman", "podman_machine_stopped", err)
|
|
121
|
+
return EngineStatus("podman", "unknown_error", err)
|
|
122
|
+
|
|
123
|
+
# VM memory check — ClawBench needs >=4 GB for Chrome + gateway + agent.
|
|
124
|
+
if platform.system() in ("Darwin", "Windows"):
|
|
125
|
+
try:
|
|
126
|
+
mi = subprocess.run(
|
|
127
|
+
["podman", "machine", "inspect", "--format",
|
|
128
|
+
"{{.Resources.Memory}}"],
|
|
129
|
+
capture_output=True, text=True, timeout=10,
|
|
130
|
+
)
|
|
131
|
+
mem_mb = int(mi.stdout.strip())
|
|
132
|
+
if mem_mb < 4096:
|
|
133
|
+
return EngineStatus("podman", "podman_low_memory", str(mem_mb))
|
|
134
|
+
except (ValueError, subprocess.TimeoutExpired):
|
|
135
|
+
pass # non-critical — skip if unreadable
|
|
136
|
+
|
|
137
|
+
return EngineStatus("podman", "ready")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _check_docker() -> EngineStatus:
|
|
141
|
+
try:
|
|
142
|
+
r = subprocess.run(
|
|
143
|
+
["docker", "info"], capture_output=True, text=True, timeout=10,
|
|
144
|
+
)
|
|
145
|
+
except (subprocess.TimeoutExpired, FileNotFoundError) as e:
|
|
146
|
+
return EngineStatus("docker", "unknown_error", str(e))
|
|
147
|
+
if r.returncode != 0:
|
|
148
|
+
return EngineStatus("docker", "docker_not_running", r.stderr.strip())
|
|
149
|
+
return EngineStatus("docker", "ready")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def remediation_hint(status: EngineStatus) -> str:
|
|
153
|
+
"""Return a short human-readable hint for a non-ready status."""
|
|
154
|
+
s = status.status
|
|
155
|
+
if s == "ready":
|
|
156
|
+
return ""
|
|
157
|
+
if s == "not_installed":
|
|
158
|
+
return (
|
|
159
|
+
"Install a container engine. Recommended: podman.\n"
|
|
160
|
+
" macOS: brew install podman && podman machine init && podman machine start\n"
|
|
161
|
+
" Linux: sudo apt install podman (or dnf, pacman, etc.)\n"
|
|
162
|
+
" Fallback: Docker Desktop from https://docker.com"
|
|
163
|
+
)
|
|
164
|
+
if s == "podman_no_machine":
|
|
165
|
+
return "Run `podman machine init` then `podman machine start`."
|
|
166
|
+
if s == "podman_machine_stopped":
|
|
167
|
+
return "Run `podman machine start`."
|
|
168
|
+
if s == "podman_low_memory":
|
|
169
|
+
mem = status.detail or "?"
|
|
170
|
+
return (
|
|
171
|
+
f"Podman VM has only {mem} MB RAM; ClawBench needs >=4 GB.\n"
|
|
172
|
+
"Stop the VM and recreate with more memory:\n"
|
|
173
|
+
" podman machine stop && podman machine rm\n"
|
|
174
|
+
" podman machine init --memory=8192 && podman machine start"
|
|
175
|
+
)
|
|
176
|
+
if s == "docker_not_running":
|
|
177
|
+
if platform.system() == "Darwin":
|
|
178
|
+
return "Docker daemon is not running. Try: open -a Docker"
|
|
179
|
+
return "Docker daemon is not running. Start the docker service."
|
|
180
|
+
return status.detail or "Unknown engine error."
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Generate a PDF resume from the resume template JSON.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
python generate_resume_pdf.py [output.pdf]
|
|
5
|
+
|
|
6
|
+
Generates from resume_template.json in the same directory.
|
|
7
|
+
Default output: alex_green_resume.pdf in the current directory.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from fpdf import FPDF
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _safe(text: str) -> str:
|
|
18
|
+
"""Replace Unicode characters that Helvetica (latin-1) cannot render."""
|
|
19
|
+
return (
|
|
20
|
+
text
|
|
21
|
+
.replace("\u2014", " - ") # em dash
|
|
22
|
+
.replace("\u2013", " - ") # en dash
|
|
23
|
+
.replace("\u2022", "-") # bullet
|
|
24
|
+
.replace("\u2018", "'") # left single quote
|
|
25
|
+
.replace("\u2019", "'") # right single quote
|
|
26
|
+
.replace("\u201c", '"') # left double quote
|
|
27
|
+
.replace("\u201d", '"') # right double quote
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def generate_resume_pdf(resume_data: dict, output_path: Path) -> None:
|
|
32
|
+
"""Render *resume_data* (the resume_template.json structure) to a PDF at *output_path*."""
|
|
33
|
+
|
|
34
|
+
header = resume_data["header"]
|
|
35
|
+
|
|
36
|
+
pdf = FPDF(orientation="P", unit="mm", format="A4")
|
|
37
|
+
pdf.set_auto_page_break(auto=True, margin=20)
|
|
38
|
+
pdf.add_page()
|
|
39
|
+
|
|
40
|
+
# -- Header -----------------------------------------------------------
|
|
41
|
+
pdf.set_font("Helvetica", "B", 22)
|
|
42
|
+
pdf.cell(0, 10, _safe(header["name"]), new_x="LMARGIN", new_y="NEXT", align="C")
|
|
43
|
+
|
|
44
|
+
pdf.set_font("Helvetica", "", 11)
|
|
45
|
+
pdf.cell(0, 6, _safe(header["title"]), new_x="LMARGIN", new_y="NEXT", align="C")
|
|
46
|
+
|
|
47
|
+
contact_parts = [header.get("email", ""), header.get("location", "")]
|
|
48
|
+
contact_line = " | ".join(p for p in contact_parts if p)
|
|
49
|
+
pdf.set_font("Helvetica", "", 9)
|
|
50
|
+
pdf.cell(0, 5, _safe(contact_line), new_x="LMARGIN", new_y="NEXT", align="C")
|
|
51
|
+
|
|
52
|
+
pdf.ln(2)
|
|
53
|
+
_draw_line(pdf)
|
|
54
|
+
|
|
55
|
+
# -- Summary ----------------------------------------------------------
|
|
56
|
+
if resume_data.get("summary"):
|
|
57
|
+
_section_heading(pdf, "Summary")
|
|
58
|
+
pdf.set_font("Helvetica", "", 10)
|
|
59
|
+
pdf.multi_cell(0, 5, _safe(resume_data["summary"]))
|
|
60
|
+
pdf.ln(2)
|
|
61
|
+
|
|
62
|
+
# -- Experience -------------------------------------------------------
|
|
63
|
+
if resume_data.get("experience"):
|
|
64
|
+
_section_heading(pdf, "Experience")
|
|
65
|
+
for job in resume_data["experience"]:
|
|
66
|
+
pdf.set_font("Helvetica", "B", 10)
|
|
67
|
+
pdf.cell(0, 5, _safe(f"{job['title']} - {job['company']}"),
|
|
68
|
+
new_x="LMARGIN", new_y="NEXT")
|
|
69
|
+
pdf.set_font("Helvetica", "I", 9)
|
|
70
|
+
pdf.cell(0, 5, _safe(f"{job.get('location', '')} {job.get('dates', '')}"),
|
|
71
|
+
new_x="LMARGIN", new_y="NEXT")
|
|
72
|
+
pdf.set_font("Helvetica", "", 9)
|
|
73
|
+
for bullet in job.get("bullets", []):
|
|
74
|
+
x = pdf.get_x()
|
|
75
|
+
pdf.cell(5, 4.5, "-")
|
|
76
|
+
pdf.multi_cell(0, 4.5, _safe(bullet))
|
|
77
|
+
pdf.set_x(x)
|
|
78
|
+
pdf.ln(2)
|
|
79
|
+
|
|
80
|
+
# -- Education --------------------------------------------------------
|
|
81
|
+
if resume_data.get("education"):
|
|
82
|
+
_section_heading(pdf, "Education")
|
|
83
|
+
for edu in resume_data["education"]:
|
|
84
|
+
pdf.set_font("Helvetica", "B", 10)
|
|
85
|
+
pdf.cell(0, 5, _safe(f"{edu['degree']} - {edu['institution']}"),
|
|
86
|
+
new_x="LMARGIN", new_y="NEXT")
|
|
87
|
+
pdf.set_font("Helvetica", "I", 9)
|
|
88
|
+
pdf.cell(0, 5, _safe(edu.get("dates", "")), new_x="LMARGIN", new_y="NEXT")
|
|
89
|
+
if edu.get("detail"):
|
|
90
|
+
pdf.set_font("Helvetica", "", 9)
|
|
91
|
+
pdf.multi_cell(0, 4.5, _safe(edu["detail"]))
|
|
92
|
+
pdf.ln(1)
|
|
93
|
+
|
|
94
|
+
# -- Skills -----------------------------------------------------------
|
|
95
|
+
if resume_data.get("skills"):
|
|
96
|
+
_section_heading(pdf, "Skills")
|
|
97
|
+
pdf.set_font("Helvetica", "", 9)
|
|
98
|
+
for category, items in resume_data["skills"].items():
|
|
99
|
+
label = category.replace("_", " ").title()
|
|
100
|
+
pdf.cell(0, 5, _safe(f"{label}: {', '.join(items)}"),
|
|
101
|
+
new_x="LMARGIN", new_y="NEXT")
|
|
102
|
+
pdf.ln(2)
|
|
103
|
+
|
|
104
|
+
# -- Certifications ---------------------------------------------------
|
|
105
|
+
if resume_data.get("certifications"):
|
|
106
|
+
_section_heading(pdf, "Certifications")
|
|
107
|
+
pdf.set_font("Helvetica", "", 9)
|
|
108
|
+
for cert in resume_data["certifications"]:
|
|
109
|
+
pdf.cell(5)
|
|
110
|
+
pdf.cell(0, 5, _safe(f"- {cert}"), new_x="LMARGIN", new_y="NEXT")
|
|
111
|
+
pdf.ln(2)
|
|
112
|
+
|
|
113
|
+
# -- Languages --------------------------------------------------------
|
|
114
|
+
if resume_data.get("languages"):
|
|
115
|
+
_section_heading(pdf, "Languages")
|
|
116
|
+
pdf.set_font("Helvetica", "", 9)
|
|
117
|
+
langs = [f"{l['language']} ({l['proficiency']})" for l in resume_data["languages"]]
|
|
118
|
+
pdf.cell(0, 5, _safe(", ".join(langs)), new_x="LMARGIN", new_y="NEXT")
|
|
119
|
+
|
|
120
|
+
pdf.output(str(output_path))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _section_heading(pdf: FPDF, title: str) -> None:
|
|
124
|
+
pdf.set_font("Helvetica", "B", 12)
|
|
125
|
+
pdf.cell(0, 7, title, new_x="LMARGIN", new_y="NEXT")
|
|
126
|
+
_draw_line(pdf)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _draw_line(pdf: FPDF) -> None:
|
|
130
|
+
y = pdf.get_y()
|
|
131
|
+
pdf.line(pdf.l_margin, y, pdf.w - pdf.r_margin, y)
|
|
132
|
+
pdf.ln(2)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
if __name__ == "__main__":
|
|
136
|
+
template = Path(__file__).resolve().parent / "resume_template.json"
|
|
137
|
+
data = json.loads(template.read_text())
|
|
138
|
+
out = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("alex_green_resume.pdf")
|
|
139
|
+
generate_resume_pdf(data, out)
|
|
140
|
+
print(f"Generated: {out.resolve()}")
|