create-walle 0.9.21 → 0.9.23
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/README.md +27 -5
- package/package.json +2 -2
- package/template/CLAUDE.md +2 -2
- package/template/LICENSE +1 -1
- package/template/bin/ctm-dev-cleanup.js +24 -3
- package/template/bin/ctm-launch.sh +13 -0
- package/template/bin/dev.sh +156 -18
- package/template/bin/node-bin.sh +84 -0
- package/template/bin/pin-node.sh +51 -0
- package/template/claude-task-manager/api-prompts.js +1203 -182
- package/template/claude-task-manager/api-reviews.js +109 -15
- package/template/claude-task-manager/approval-agent.js +1360 -280
- package/template/claude-task-manager/bin/restart-ctm.sh +64 -23
- package/template/claude-task-manager/bin/storage-migration-supervisor.js +338 -0
- package/template/claude-task-manager/db.js +4417 -295
- package/template/claude-task-manager/docs/app-update-refresh-protocol.md +69 -0
- package/template/claude-task-manager/docs/approval-ai-refinement.md +138 -0
- package/template/claude-task-manager/docs/approval-rescue-loop.md +74 -0
- package/template/claude-task-manager/docs/codex-operational-warning-health.md +107 -0
- package/template/claude-task-manager/docs/codex-resume-state-guard-design.md +17 -12
- package/template/claude-task-manager/docs/codex-terminal-render-controller-handoff.md +311 -0
- package/template/claude-task-manager/docs/coding-agent-hooks-architecture.md +418 -0
- package/template/claude-task-manager/docs/conversation-import-freshness.md +20 -0
- package/template/claude-task-manager/docs/google-workspace-auth-health.md +77 -0
- package/template/claude-task-manager/docs/image-paste-ux.md +13 -0
- package/template/claude-task-manager/docs/ipad-web-preview.md +88 -0
- package/template/claude-task-manager/docs/main-loop-offload-architecture.md +66 -0
- package/template/claude-task-manager/docs/microsoft-dev-tunnel-phone-access-design.md +274 -519
- package/template/claude-task-manager/docs/mobile-live-streaming.md +27 -5
- package/template/claude-task-manager/docs/mobile-remote-submission-lifecycle.md +69 -0
- package/template/claude-task-manager/docs/phone-access-design.md +53 -15
- package/template/claude-task-manager/docs/phone-passkey-identity.md +122 -0
- package/template/claude-task-manager/docs/phone-setup.md +3 -0
- package/template/claude-task-manager/docs/prompt-editing-tree-design.md +25 -1
- package/template/claude-task-manager/docs/remote-desktop-access-design.md +268 -0
- package/template/claude-task-manager/docs/restart-lifecycle-architecture.md +95 -0
- package/template/claude-task-manager/docs/runtime-work-control-plane.md +53 -0
- package/template/claude-task-manager/docs/session-interactive-wait-surfaces.md +38 -0
- package/template/claude-task-manager/docs/session-needs-you-dismissal.md +84 -0
- package/template/claude-task-manager/docs/session-render-state-management-design.md +91 -3
- package/template/claude-task-manager/docs/session-standup-command-center-design.md +25 -1
- package/template/claude-task-manager/docs/session-title-authority.md +32 -0
- package/template/claude-task-manager/docs/session-workspace-binding.md +33 -0
- package/template/claude-task-manager/docs/skill-intent-resolution-design.md +72 -0
- package/template/claude-task-manager/docs/walle-mcp-supervisor-health.md +86 -0
- package/template/claude-task-manager/docs/walle-relay-phone-access-design.md +24 -15
- package/template/claude-task-manager/docs/walle-session-history-hydration.md +114 -0
- package/template/claude-task-manager/docs/walle-session-input-queue.md +104 -0
- package/template/claude-task-manager/docs/walle-session-model-catalog.md +90 -0
- package/template/claude-task-manager/docs/walle-session-model-preferences.md +15 -6
- package/template/claude-task-manager/git-utils.js +897 -27
- package/template/claude-task-manager/lib/agent-capabilities.js +33 -0
- package/template/claude-task-manager/lib/agent-cli-cache.js +37 -7
- package/template/claude-task-manager/lib/agent-hooks-installer.js +26 -2
- package/template/claude-task-manager/lib/agent-presets.js +17 -1
- package/template/claude-task-manager/lib/all-sessions-query.js +108 -0
- package/template/claude-task-manager/lib/approval-ai-refinement.js +488 -0
- package/template/claude-task-manager/lib/approval-self-adapt.js +168 -0
- package/template/claude-task-manager/lib/async-semaphore.js +44 -0
- package/template/claude-task-manager/lib/auth-context.js +5 -0
- package/template/claude-task-manager/lib/auth-rate-limit.js +47 -4
- package/template/claude-task-manager/lib/auth-rules.js +29 -2
- package/template/claude-task-manager/lib/auto-approval-verifier.js +129 -16
- package/template/claude-task-manager/lib/background-llm.js +144 -17
- package/template/claude-task-manager/lib/branch-inventory.js +212 -0
- package/template/claude-task-manager/lib/claude-desktop-sessions.js +15 -3
- package/template/claude-task-manager/lib/coalesce-sync-frames.js +151 -0
- package/template/claude-task-manager/lib/codex-launch-health.js +762 -0
- package/template/claude-task-manager/lib/codex-transcript-pager.js +51 -0
- package/template/claude-task-manager/lib/codex-zst.js +124 -0
- package/template/claude-task-manager/lib/coding-agent-models.js +233 -30
- package/template/claude-task-manager/lib/connection-health.js +232 -0
- package/template/claude-task-manager/lib/conversation-blob-parser.js +42 -0
- package/template/claude-task-manager/lib/conversation-tail-merge.js +89 -26
- package/template/claude-task-manager/lib/ctm-session-context-api.js +39 -10
- package/template/claude-task-manager/lib/cursor-conversation-store.js +354 -0
- package/template/claude-task-manager/lib/db-owner-worker-client.js +315 -0
- package/template/claude-task-manager/lib/document-review.js +141 -6
- package/template/claude-task-manager/lib/escalation-review.js +152 -0
- package/template/claude-task-manager/lib/graceful-shutdown.js +159 -0
- package/template/claude-task-manager/lib/headless-term-service.js +678 -0
- package/template/claude-task-manager/lib/heavy-worker-fallback.js +38 -0
- package/template/claude-task-manager/lib/jsonl-conversation-parser.js +542 -0
- package/template/claude-task-manager/lib/jsonl-range-reader.js +112 -0
- package/template/claude-task-manager/lib/main-db-census.js +216 -0
- package/template/claude-task-manager/lib/message-pagination.js +106 -4
- package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +750 -26
- package/template/claude-task-manager/lib/mobile-auth-api.js +274 -7
- package/template/claude-task-manager/lib/mobile-auth-store.js +592 -10
- package/template/claude-task-manager/lib/mobile-notification-dispatcher.js +15 -0
- package/template/claude-task-manager/lib/model-overview-brain-fallback.js +311 -0
- package/template/claude-task-manager/lib/model-overview-cache.js +141 -0
- package/template/claude-task-manager/lib/models-health-routing-notice.js +126 -0
- package/template/claude-task-manager/lib/node-pin-guard.js +93 -0
- package/template/claude-task-manager/lib/perf-tracker.js +242 -6
- package/template/claude-task-manager/lib/permission-match.js +76 -0
- package/template/claude-task-manager/lib/permission-sync.js +133 -20
- package/template/claude-task-manager/lib/process-title.js +35 -0
- package/template/claude-task-manager/lib/prompt-executions-query.js +25 -0
- package/template/claude-task-manager/lib/prompt-index-disk-cache.js +44 -0
- package/template/claude-task-manager/lib/prompt-intent.js +132 -0
- package/template/claude-task-manager/lib/provider-user-context.js +34 -0
- package/template/claude-task-manager/lib/read-pool-client.js +313 -0
- package/template/claude-task-manager/lib/readpool-breaker.js +31 -0
- package/template/claude-task-manager/lib/recent-sessions-breaker.js +12 -0
- package/template/claude-task-manager/lib/remote-feedback-client.js +72 -0
- package/template/claude-task-manager/lib/remote-relay-protocol.js +37 -4
- package/template/claude-task-manager/lib/remote-relay-store.js +159 -0
- package/template/claude-task-manager/lib/remote-submission-observer.js +278 -0
- package/template/claude-task-manager/lib/restart-guard.js +109 -0
- package/template/claude-task-manager/lib/restore-interruption-detector.js +439 -0
- package/template/claude-task-manager/lib/restore-policy.js +13 -0
- package/template/claude-task-manager/lib/restore-resume-batch.js +74 -0
- package/template/claude-task-manager/lib/restore-runtime.js +68 -0
- package/template/claude-task-manager/lib/restore-storm.js +34 -0
- package/template/claude-task-manager/lib/resume-cwd.js +36 -0
- package/template/claude-task-manager/lib/resume-preflight.js +313 -0
- package/template/claude-task-manager/lib/runtime-work-registry.js +444 -0
- package/template/claude-task-manager/lib/sanitize-openai-auth.js +31 -0
- package/template/claude-task-manager/lib/scheduler.js +21 -1
- package/template/claude-task-manager/lib/scrollback-snapshot-store.js +159 -0
- package/template/claude-task-manager/lib/serial-task-queue.js +64 -0
- package/template/claude-task-manager/lib/server-listeners.js +239 -0
- package/template/claude-task-manager/lib/session-capture.js +42 -7
- package/template/claude-task-manager/lib/session-content-backfill.js +131 -0
- package/template/claude-task-manager/lib/session-history.js +388 -43
- package/template/claude-task-manager/lib/session-host-manager.js +287 -0
- package/template/claude-task-manager/lib/session-image-refs.js +209 -0
- package/template/claude-task-manager/lib/session-jobs.js +399 -59
- package/template/claude-task-manager/lib/session-prompt-index.js +137 -0
- package/template/claude-task-manager/lib/session-restore.js +53 -0
- package/template/claude-task-manager/lib/session-standup.js +123 -23
- package/template/claude-task-manager/lib/session-state-bus.js +14 -0
- package/template/claude-task-manager/lib/session-stream.js +64 -16
- package/template/claude-task-manager/lib/session-timeline-summary.js +260 -0
- package/template/claude-task-manager/lib/session-token-usage.js +494 -0
- package/template/claude-task-manager/lib/session-workspace-binding.js +356 -0
- package/template/claude-task-manager/lib/setup-network-config.js +9 -0
- package/template/claude-task-manager/lib/size-cap.js +45 -0
- package/template/claude-task-manager/lib/size-cap.test.js +62 -0
- package/template/claude-task-manager/lib/skill-autocomplete.js +180 -1
- package/template/claude-task-manager/lib/skill-intent-resolver.js +304 -0
- package/template/claude-task-manager/lib/sqlite-driver.js +19 -3
- package/template/claude-task-manager/lib/standup-attention.js +7 -3
- package/template/claude-task-manager/lib/status-authority.js +39 -0
- package/template/claude-task-manager/lib/status-hooks.js +4 -0
- package/template/claude-task-manager/lib/storage-migration.js +235 -0
- package/template/claude-task-manager/lib/structured-capture.js +298 -0
- package/template/claude-task-manager/lib/sync-io-census.js +163 -0
- package/template/claude-task-manager/lib/tailscale-setup.js +6 -0
- package/template/claude-task-manager/lib/terminal-activity-evidence.js +33 -0
- package/template/claude-task-manager/lib/terminal-choice.js +364 -0
- package/template/claude-task-manager/lib/terminal-control-sanitize.js +17 -0
- package/template/claude-task-manager/lib/terminal-fingerprint.js +48 -0
- package/template/claude-task-manager/lib/terminal-output-flush.js +84 -0
- package/template/claude-task-manager/lib/timeline-order.js +122 -0
- package/template/claude-task-manager/lib/transcript-store.js +348 -43
- package/template/claude-task-manager/lib/transport-security.js +84 -1
- package/template/claude-task-manager/lib/wait-state.js +184 -0
- package/template/claude-task-manager/lib/walle-client.js +47 -5
- package/template/claude-task-manager/lib/walle-ctm-history.js +564 -4
- package/template/claude-task-manager/lib/walle-external-actions.js +135 -16
- package/template/claude-task-manager/lib/walle-history-hydration.js +46 -0
- package/template/claude-task-manager/lib/walle-native-health.js +403 -0
- package/template/claude-task-manager/lib/walle-repair.js +701 -0
- package/template/claude-task-manager/lib/walle-session-cache.js +109 -0
- package/template/claude-task-manager/lib/walle-session-context.js +57 -21
- package/template/claude-task-manager/lib/walle-session-model-catalog.js +34 -0
- package/template/claude-task-manager/lib/walle-supervisor.js +539 -63
- package/template/claude-task-manager/lib/walle-transcript.js +52 -0
- package/template/claude-task-manager/lib/worktree-active-sync.js +11 -7
- package/template/claude-task-manager/lib/worktree-cwd.js +32 -1
- package/template/claude-task-manager/package.json +1 -1
- package/template/claude-task-manager/prompt-harvest.js +89 -66
- package/template/claude-task-manager/providers/claude-code.js +51 -3
- package/template/claude-task-manager/providers/cursor.js +140 -45
- package/template/claude-task-manager/public/css/reviews.css +551 -61
- package/template/claude-task-manager/public/css/setup.css +191 -0
- package/template/claude-task-manager/public/css/walle-session.css +865 -10
- package/template/claude-task-manager/public/css/walle.css +154 -0
- package/template/claude-task-manager/public/designs/ai-providers-consolidation-v2.html +830 -0
- package/template/claude-task-manager/public/index.html +18516 -2058
- package/template/claude-task-manager/public/ipad.html +363 -0
- package/template/claude-task-manager/public/js/document-review-links.js +301 -0
- package/template/claude-task-manager/public/js/image-normalize.js +69 -36
- package/template/claude-task-manager/public/js/message-renderer.js +1265 -77
- package/template/claude-task-manager/public/js/prompts.js +66 -29
- package/template/claude-task-manager/public/js/reviews.js +901 -133
- package/template/claude-task-manager/public/js/session-activity-utils.js +11 -1
- package/template/claude-task-manager/public/js/session-search-utils.js +94 -10
- package/template/claude-task-manager/public/js/session-status-precedence.js +23 -5
- package/template/claude-task-manager/public/js/setup.js +1273 -176
- package/template/claude-task-manager/public/js/stream-view.js +691 -73
- package/template/claude-task-manager/public/js/terminal-reconciler.js +210 -0
- package/template/claude-task-manager/public/js/walle-session.js +2455 -158
- package/template/claude-task-manager/public/js/walle.js +455 -28
- package/template/claude-task-manager/public/m/app.css +2909 -262
- package/template/claude-task-manager/public/m/app.js +6601 -398
- package/template/claude-task-manager/public/m/claim.html +224 -17
- package/template/claude-task-manager/public/m/index.html +117 -21
- package/template/claude-task-manager/public/m/sw.js +3 -1
- package/template/claude-task-manager/public/manifest.json +2 -2
- package/template/claude-task-manager/public/prompts.html +30 -14
- package/template/claude-task-manager/queue-engine.js +507 -28
- package/template/claude-task-manager/scripts/repair-claude-session-images.js +27 -8
- package/template/claude-task-manager/server.js +14341 -2197
- package/template/claude-task-manager/session-integrity.js +160 -18
- package/template/claude-task-manager/session-search-ranking.js +1 -0
- package/template/claude-task-manager/session-utils.js +25 -5
- package/template/claude-task-manager/workers/approval-blocklist.js +96 -6
- package/template/claude-task-manager/workers/approval-widget-validator.js +14 -8
- package/template/claude-task-manager/workers/conversation-import-worker.js +11 -50
- package/template/claude-task-manager/workers/db-owner-worker.js +386 -0
- package/template/claude-task-manager/workers/harvest-worker.js +9 -55
- package/template/claude-task-manager/workers/headless-term-worker.js +9 -530
- package/template/claude-task-manager/workers/read-pool-worker.js +387 -0
- package/template/claude-task-manager/workers/scrollback-worker.js +11 -72
- package/template/claude-task-manager/workers/session-host-process.js +146 -0
- package/template/claude-task-manager/workers/session-integrity-worker.js +10 -54
- package/template/claude-task-manager/workers/state-detectors/base.js +18 -1
- package/template/claude-task-manager/workers/state-detectors/claude-code.js +182 -9
- package/template/claude-task-manager/workers/state-detectors/codex.js +150 -2
- package/template/claude-task-manager/workers/state-detectors/cursor.js +127 -0
- package/template/claude-task-manager/workers/state-detectors/gemini.js +21 -0
- package/template/claude-task-manager/workers/state-detectors/index.js +29 -0
- package/template/claude-task-manager/workers/state-detectors/opencode.js +103 -0
- package/template/docs/design/markdown-review-pane.md +206 -0
- package/template/docs/designs/2026-05-17-portkey-gateway-provider-ux.md +129 -38
- package/template/docs/designs/2026-05-20-mobile-worktree-finish-command.md +27 -0
- package/template/docs/designs/2026-05-22-ai-configuration-consolidation.md +248 -0
- package/template/docs/designs/ai-configuration-consolidation-mock.html +812 -0
- package/template/docs/private-memory-and-pii-policy.md +69 -0
- package/template/package.json +2 -1
- package/template/scripts/check-private-data.js +201 -0
- package/template/shared/sqlite-owner-guard.js +30 -0
- package/template/shared/sqlite-owner-write-queue.js +225 -0
- package/template/shared/sqlite-storage-policy.js +111 -0
- package/template/shared/sqlite-write-lock.js +428 -0
- package/template/wall-e/agent-runners/claude-code.js +5 -0
- package/template/wall-e/agent.js +166 -22
- package/template/wall-e/api-walle.js +524 -70
- package/template/wall-e/auth/provider-flows.js +11 -1
- package/template/wall-e/bin/walle-mcp-stdio.js +341 -17
- package/template/wall-e/brain.js +1614 -141
- package/template/wall-e/chat/attachment-blocks.js +96 -0
- package/template/wall-e/chat/attachments.js +2 -1
- package/template/wall-e/chat/capability-resolver.js +7 -7
- package/template/wall-e/chat/context-messages.js +28 -0
- package/template/wall-e/chat/conversation-frame.js +630 -0
- package/template/wall-e/chat/provider-messages.js +125 -0
- package/template/wall-e/chat.js +1002 -233
- package/template/wall-e/coding/acceptance-contract.js +170 -0
- package/template/wall-e/coding/acp-adapter.js +1 -1
- package/template/wall-e/coding/agent-catalog.js +3 -0
- package/template/wall-e/coding/artifact-store.js +93 -0
- package/template/wall-e/coding/capability-router.js +120 -0
- package/template/wall-e/coding/coding-run-controller.js +423 -0
- package/template/wall-e/coding/compaction-service.js +157 -12
- package/template/wall-e/coding/frontend-verification.js +258 -0
- package/template/wall-e/coding/lifecycle-hooks.js +75 -0
- package/template/wall-e/coding/local-preview-contract.js +157 -0
- package/template/wall-e/coding/permission-service.js +57 -13
- package/template/wall-e/coding/prompt-bundle.js +19 -1
- package/template/wall-e/coding/prompt-section-registry.js +227 -0
- package/template/wall-e/coding/provider-compat.js +15 -0
- package/template/wall-e/coding/runtime-events.js +224 -0
- package/template/wall-e/coding/runtime-mode.js +3 -0
- package/template/wall-e/coding/side-git-snapshot.js +160 -4
- package/template/wall-e/coding/snapshot-service.js +143 -1
- package/template/wall-e/coding/stream-processor.js +388 -34
- package/template/wall-e/coding/task-tool.js +141 -4
- package/template/wall-e/coding/tool-execution-controller.js +365 -0
- package/template/wall-e/coding/tool-registry.js +43 -5
- package/template/wall-e/coding/user-hooks.js +217 -0
- package/template/wall-e/coding-orchestrator.js +1330 -221
- package/template/wall-e/coding-prompts.js +20 -4
- package/template/wall-e/context/context-builder.js +15 -2
- package/template/wall-e/decision/confidence.js +1 -1
- package/template/wall-e/docs/coding-acceptance-contract.md +41 -0
- package/template/wall-e/docs/external-action-controller.md +26 -6
- package/template/wall-e/docs/telemetry-lifecycle.md +8 -2
- package/template/wall-e/embeddings.js +591 -53
- package/template/wall-e/external-action-controller.js +12 -0
- package/template/wall-e/http/auth.js +1 -0
- package/template/wall-e/http/chat-api.js +46 -11
- package/template/wall-e/http/model-admin.js +836 -34
- package/template/wall-e/lib/boot-profile.js +88 -0
- package/template/wall-e/lib/event-loop-monitor.js +93 -0
- package/template/wall-e/lib/service-health.js +194 -0
- package/template/wall-e/llm/anthropic.js +130 -5
- package/template/wall-e/llm/client.js +266 -63
- package/template/wall-e/llm/default-fallback.js +382 -0
- package/template/wall-e/llm/health.js +19 -0
- package/template/wall-e/llm/message-guard.js +78 -0
- package/template/wall-e/llm/model-catalog.js +252 -1
- package/template/wall-e/llm/openai.js +26 -4
- package/template/wall-e/llm/portkey-sync.js +654 -0
- package/template/wall-e/llm/provider-error.js +30 -2
- package/template/wall-e/llm/registry.js +5 -1
- package/template/wall-e/llm/request-compat.js +67 -0
- package/template/wall-e/loops/backfill.js +79 -23
- package/template/wall-e/loops/brain-optimize.js +67 -0
- package/template/wall-e/loops/ingest.js +25 -10
- package/template/wall-e/loops/question-digest.js +160 -0
- package/template/wall-e/loops/reflect.js +6 -4
- package/template/wall-e/loops/think.js +39 -12
- package/template/wall-e/mcp-server.js +318 -36
- package/template/wall-e/memory/ctm-context-client.js +52 -14
- package/template/wall-e/memory/ctm-operational-context.js +237 -0
- package/template/wall-e/memory/ctm-prompt-executions-client.js +128 -0
- package/template/wall-e/memory/ctm-session-context.js +111 -63
- package/template/wall-e/prompts/coding/deepseek.txt +3 -0
- package/template/wall-e/prompts/coding/gemini.txt +6 -0
- package/template/wall-e/prompts/coding/gpt.txt +6 -0
- package/template/wall-e/prompts/coding/local.txt +7 -0
- package/template/wall-e/runtime/decision-hooks.js +115 -0
- package/template/wall-e/runtime/devbox-gateway.js +82 -8
- package/template/wall-e/runtime/prompt-manifest.js +86 -0
- package/template/wall-e/runtime/tool-executor.js +269 -0
- package/template/wall-e/runtime/tool-result-envelope.js +138 -0
- package/template/wall-e/runtime/transcript-projection.js +60 -0
- package/template/wall-e/runtime/walle-runtime.js +224 -0
- package/template/wall-e/scripts/db-optimize/migrate.js +162 -0
- package/template/wall-e/scripts/db-optimize/recall-eval.js +117 -0
- package/template/wall-e/server.js +15 -0
- package/template/wall-e/session-files.js +9 -0
- package/template/wall-e/skills/_bundled/google-calendar/run.js +1 -1
- package/template/wall-e/skills/_bundled/gws-workspace/run.js +1 -1
- package/template/wall-e/skills/_bundled/slack-mentions/run.js +76 -6
- package/template/wall-e/skills/claude-code-reader.js +7 -3
- package/template/wall-e/skills/script-skill-runner.js +10 -0
- package/template/wall-e/skills/skill-planner.js +38 -0
- package/template/wall-e/tools/builtin-middleware.js +19 -9
- package/template/wall-e/tools/local-tools.js +1428 -16
- package/template/wall-e/tools/permission-checker.js +73 -5
- package/template/wall-e/tools/question-manager.js +117 -7
- package/template/wall-e/training/harvester.js +12 -28
- package/template/wall-e/training/replay.js +25 -80
- package/template/website/index.html +10 -10
- package/template/wall-e/eval/ab-test.js +0 -203
- package/template/wall-e/eval/agent-runner.js +0 -772
- package/template/wall-e/eval/agent-scorer.js +0 -461
- package/template/wall-e/eval/aggregator.js +0 -414
- package/template/wall-e/eval/allowed-test-commands.js +0 -34
- package/template/wall-e/eval/benchmark-generator.js +0 -113
- package/template/wall-e/eval/benchmarks/chat-eval.json +0 -1662
- package/template/wall-e/eval/benchmarks/chat.json +0 -82
- package/template/wall-e/eval/benchmarks/coding-agent-real.json +0 -1
- package/template/wall-e/eval/benchmarks/coding-agent.json +0 -1581
- package/template/wall-e/eval/benchmarks/coding.json +0 -122
- package/template/wall-e/eval/benchmarks/memory-retrieval.json +0 -234
- package/template/wall-e/eval/benchmarks/reasoning.json +0 -82
- package/template/wall-e/eval/benchmarks/swebench-lite-30.json +0 -212
- package/template/wall-e/eval/benchmarks.js +0 -669
- package/template/wall-e/eval/cc-replay.js +0 -719
- package/template/wall-e/eval/chat-eval.js +0 -525
- package/template/wall-e/eval/check-keys.js +0 -15
- package/template/wall-e/eval/check-providers.js +0 -42
- package/template/wall-e/eval/codex-cli-baseline.js +0 -669
- package/template/wall-e/eval/coding-agent-real.js +0 -570
- package/template/wall-e/eval/context-compactor.js +0 -251
- package/template/wall-e/eval/debug-agent003.js +0 -68
- package/template/wall-e/eval/diagnostics.js +0 -216
- package/template/wall-e/eval/eval-orchestrator.js +0 -642
- package/template/wall-e/eval/evaluate.js +0 -202
- package/template/wall-e/eval/evaluator.js +0 -373
- package/template/wall-e/eval/exporter.js +0 -212
- package/template/wall-e/eval/fixtures/express-basic/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-basic/server.js +0 -115
- package/template/wall-e/eval/fixtures/express-basic/test.js +0 -83
- package/template/wall-e/eval/fixtures/express-buggy/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-buggy/server.js +0 -113
- package/template/wall-e/eval/fixtures/express-buggy/test.js +0 -83
- package/template/wall-e/eval/fixtures/express-buggy-items/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-buggy-items/server.js +0 -112
- package/template/wall-e/eval/fixtures/express-buggy-items/test.js +0 -83
- package/template/wall-e/eval/fixtures/express-buggy-search/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-buggy-search/server.js +0 -121
- package/template/wall-e/eval/fixtures/express-buggy-search/test.js +0 -83
- package/template/wall-e/eval/fixtures/express-rename-data/data.js +0 -34
- package/template/wall-e/eval/fixtures/express-rename-data/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-rename-data/server.js +0 -97
- package/template/wall-e/eval/fixtures/express-rename-data/test.js +0 -88
- package/template/wall-e/eval/fixtures/express-xss/package.json +0 -12
- package/template/wall-e/eval/fixtures/express-xss/server.js +0 -90
- package/template/wall-e/eval/fixtures/express-xss/test.js +0 -67
- package/template/wall-e/eval/fixtures/express-xss/views/profile.ejs +0 -9
- package/template/wall-e/eval/fixtures/fullstack-app/config/default.js +0 -9
- package/template/wall-e/eval/fixtures/fullstack-app/config/test.js +0 -13
- package/template/wall-e/eval/fixtures/fullstack-app/package.json +0 -11
- package/template/wall-e/eval/fixtures/fullstack-app/public/css/style.css +0 -137
- package/template/wall-e/eval/fixtures/fullstack-app/public/index.html +0 -46
- package/template/wall-e/eval/fixtures/fullstack-app/public/js/app.js +0 -121
- package/template/wall-e/eval/fixtures/fullstack-app/public/js/auth.js +0 -71
- package/template/wall-e/eval/fixtures/fullstack-app/public/js/items.js +0 -80
- package/template/wall-e/eval/fixtures/fullstack-app/public/js/users.js +0 -46
- package/template/wall-e/eval/fixtures/fullstack-app/public/login.html +0 -45
- package/template/wall-e/eval/fixtures/fullstack-app/public/register.html +0 -38
- package/template/wall-e/eval/fixtures/fullstack-app/scripts/migrate.js +0 -23
- package/template/wall-e/eval/fixtures/fullstack-app/scripts/seed.js +0 -46
- package/template/wall-e/eval/fixtures/fullstack-app/server/db.js +0 -99
- package/template/wall-e/eval/fixtures/fullstack-app/server/index.js +0 -94
- package/template/wall-e/eval/fixtures/fullstack-app/server/middleware/auth.js +0 -19
- package/template/wall-e/eval/fixtures/fullstack-app/server/middleware/logger.js +0 -19
- package/template/wall-e/eval/fixtures/fullstack-app/server/router.js +0 -50
- package/template/wall-e/eval/fixtures/fullstack-app/server/routes/auth.js +0 -69
- package/template/wall-e/eval/fixtures/fullstack-app/server/routes/health.js +0 -23
- package/template/wall-e/eval/fixtures/fullstack-app/server/routes/items.js +0 -88
- package/template/wall-e/eval/fixtures/fullstack-app/server/routes/users.js +0 -75
- package/template/wall-e/eval/fixtures/fullstack-app/server/test.js +0 -198
- package/template/wall-e/eval/fixtures/fullstack-app/server/utils/response.js +0 -34
- package/template/wall-e/eval/fixtures/fullstack-app/server/utils/validate.js +0 -26
- package/template/wall-e/eval/fixtures/fullstack-app/server.js +0 -8
- package/template/wall-e/eval/fixtures/fullstack-app/test.js +0 -12
- package/template/wall-e/eval/fixtures/monorepo-basic/package.json +0 -8
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/data.js +0 -58
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/middleware.js +0 -46
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/package.json +0 -8
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/routes.js +0 -64
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/server.js +0 -56
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/test.js +0 -116
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/commands.js +0 -61
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/index.js +0 -62
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/output.js +0 -43
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/package.json +0 -11
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/test.js +0 -44
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/formatters.js +0 -43
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/index.js +0 -12
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/package.json +0 -5
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/test.js +0 -55
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/validators.js +0 -29
- package/template/wall-e/eval/fixtures/monorepo-basic/test.js +0 -46
- package/template/wall-e/eval/fixtures/node-cli/index.js +0 -78
- package/template/wall-e/eval/fixtures/node-cli/package.json +0 -10
- package/template/wall-e/eval/fixtures/node-cli/test.js +0 -57
- package/template/wall-e/eval/fixtures/node-typed/package.json +0 -8
- package/template/wall-e/eval/fixtures/node-typed/src/handlers.js +0 -31
- package/template/wall-e/eval/fixtures/node-typed/src/utils.js +0 -33
- package/template/wall-e/eval/fixtures/node-typed/test.js +0 -36
- package/template/wall-e/eval/fixtures/python-flask/app.py +0 -14
- package/template/wall-e/eval/fixtures/python-flask/requirements.txt +0 -2
- package/template/wall-e/eval/fixtures/python-flask/test_app.py +0 -25
- package/template/wall-e/eval/fixtures/wall-e-subset/brain.js +0 -105
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/aggregator.js +0 -101
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks/chat.json +0 -20
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks/coding.json +0 -32
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks.js +0 -64
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/package.json +0 -6
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/server.js +0 -31
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/test.js +0 -18
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/utils.js +0 -34
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/runner.js +0 -104
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/scorer.js +0 -73
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/test.js +0 -134
- package/template/wall-e/eval/fixtures/wall-e-subset/llm/client.js +0 -99
- package/template/wall-e/eval/fixtures/wall-e-subset/llm/providers.js +0 -63
- package/template/wall-e/eval/fixtures/wall-e-subset/llm/test.js +0 -70
- package/template/wall-e/eval/fixtures/wall-e-subset/package.json +0 -10
- package/template/wall-e/eval/fixtures/wall-e-subset/test.js +0 -86
- package/template/wall-e/eval/harvester.js +0 -685
- package/template/wall-e/eval/head-to-head.js +0 -388
- package/template/wall-e/eval/humaneval-adapter.js +0 -321
- package/template/wall-e/eval/list-models.js +0 -31
- package/template/wall-e/eval/livecodebench-adapter.js +0 -291
- package/template/wall-e/eval/mail-integration.js +0 -443
- package/template/wall-e/eval/manifest.js +0 -186
- package/template/wall-e/eval/meta-harness/adapters/coding-agent.js +0 -57
- package/template/wall-e/eval/meta-harness/bootstrap-snapshot.js +0 -149
- package/template/wall-e/eval/meta-harness/candidate-store.js +0 -117
- package/template/wall-e/eval/meta-harness/cli.js +0 -86
- package/template/wall-e/eval/meta-harness/domain-spec.js +0 -154
- package/template/wall-e/eval/meta-harness/domains/coding-agent.domain.json +0 -84
- package/template/wall-e/eval/meta-harness/examples/env-bootstrap-candidate.js +0 -29
- package/template/wall-e/eval/meta-harness/experience-store.js +0 -174
- package/template/wall-e/eval/meta-harness/frontier.js +0 -96
- package/template/wall-e/eval/meta-harness/harness-interface.js +0 -90
- package/template/wall-e/eval/meta-harness/leakage-guard.js +0 -80
- package/template/wall-e/eval/meta-harness/optimizer.js +0 -207
- package/template/wall-e/eval/meta-harness/proposer-runner.js +0 -110
- package/template/wall-e/eval/meta-harness/reporting.js +0 -58
- package/template/wall-e/eval/meta-harness/telemetry.js +0 -27
- package/template/wall-e/eval/meta-harness/validation.js +0 -81
- package/template/wall-e/eval/promoter.js +0 -228
- package/template/wall-e/eval/provider-normalizer.js +0 -33
- package/template/wall-e/eval/replay.js +0 -395
- package/template/wall-e/eval/run-agent-benchmarks.js +0 -386
- package/template/wall-e/eval/run-codex-cli-baseline.js +0 -177
- package/template/wall-e/eval/run-coding-agent-real.js +0 -187
- package/template/wall-e/eval/run-eval.js +0 -435
- package/template/wall-e/eval/run-model-comparison.js +0 -142
- package/template/wall-e/eval/session-evaluator.js +0 -187
- package/template/wall-e/eval/session-miner.js +0 -207
- package/template/wall-e/eval/session-retrieval-benchmark.js +0 -150
- package/template/wall-e/eval/session-transcripts.js +0 -509
- package/template/wall-e/eval/shadow.js +0 -161
- package/template/wall-e/eval/swebench-adapter.js +0 -345
- package/template/wall-e/eval/swebench-docker.js +0 -192
- package/template/wall-e/eval/train.py +0 -320
- package/template/wall-e/eval/trainer.js +0 -232
- package/template/wall-e/eval/weekly-eval-loop.js +0 -241
|
@@ -4,6 +4,8 @@ const { promisify } = require('util');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const os = require('os');
|
|
7
|
+
const crypto = require('crypto');
|
|
8
|
+
const http = require('http');
|
|
7
9
|
const https = require('https');
|
|
8
10
|
const execFileAsync = promisify(execFile);
|
|
9
11
|
const execAsync = promisify(exec);
|
|
@@ -16,6 +18,15 @@ const MAIL_AUTOMATION_COOLDOWN_PATH = process.env.WALLE_MAIL_COOLDOWN_PATH ||
|
|
|
16
18
|
path.join(os.tmpdir(), 'wall-e-mail-appleevent-cooldown.json');
|
|
17
19
|
const MAIL_AUTOMATION_COOLDOWN_MS = 5 * 60 * 1000;
|
|
18
20
|
const MAIL_AUTOMATION_SLOW_MS = 30 * 1000;
|
|
21
|
+
const STATIC_SERVER_PROCESSES = new Map();
|
|
22
|
+
|
|
23
|
+
function expandUserPath(value) {
|
|
24
|
+
if (typeof value !== 'string' || !value.startsWith('~')) return value;
|
|
25
|
+
const home = process.env.HOME || HOME;
|
|
26
|
+
if (value === '~') return home;
|
|
27
|
+
if (value.startsWith('~/')) return path.join(home, value.slice(2));
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
19
30
|
|
|
20
31
|
function _pidIsAlive(pid) {
|
|
21
32
|
if (!pid || pid <= 0) return false;
|
|
@@ -1513,9 +1524,12 @@ function hasShellControlArg(args) {
|
|
|
1513
1524
|
async function runShell(commandOrOpts, legacyArgs, legacyOpts) {
|
|
1514
1525
|
let command, args, timeout_ms, cwd;
|
|
1515
1526
|
|
|
1527
|
+
let background = false;
|
|
1528
|
+
let sessionId = '';
|
|
1529
|
+
let persist = false;
|
|
1516
1530
|
if (typeof commandOrOpts === 'object' && commandOrOpts !== null) {
|
|
1517
|
-
// New format: runShell({ command, args, timeout_ms, cwd })
|
|
1518
|
-
({ command, args, timeout_ms = 30000, cwd } = commandOrOpts);
|
|
1531
|
+
// New format: runShell({ command, args, timeout_ms, cwd, background })
|
|
1532
|
+
({ command, args, timeout_ms = 30000, cwd, background = false, sessionId = '', persist = false } = commandOrOpts);
|
|
1519
1533
|
} else {
|
|
1520
1534
|
// Legacy format: runShell(command, args, { timeout_ms })
|
|
1521
1535
|
command = commandOrOpts;
|
|
@@ -1542,6 +1556,12 @@ async function runShell(commandOrOpts, legacyArgs, legacyOpts) {
|
|
|
1542
1556
|
commandStr = command;
|
|
1543
1557
|
}
|
|
1544
1558
|
|
|
1559
|
+
// Background mode: detach into the session-scoped registry (it runs its
|
|
1560
|
+
// own shell-analyzer gate before spawning).
|
|
1561
|
+
if (background) {
|
|
1562
|
+
return runShellBackground({ command: commandStr, cwd, sessionId, persist });
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1545
1565
|
// Tree-sitter analysis gates execution
|
|
1546
1566
|
const analysis = await analyzeShellCommand(commandStr, cwd || process.cwd());
|
|
1547
1567
|
if (!analysis.allowed) {
|
|
@@ -1895,11 +1915,18 @@ async function writeFile(filePath, content, { sessionId, projectRoot } = {}) {
|
|
|
1895
1915
|
return { written: true, path: resolved, bytes: Buffer.byteLength(content) };
|
|
1896
1916
|
}
|
|
1897
1917
|
|
|
1898
|
-
async function searchFiles(query, { directory, extensions, max_results = 20 } = {}) {
|
|
1899
|
-
|
|
1918
|
+
async function searchFiles(query, { directory, extensions, max_results = 20, projectRoot } = {}) {
|
|
1919
|
+
let searchDir;
|
|
1920
|
+
try {
|
|
1921
|
+
searchDir = projectRoot
|
|
1922
|
+
? resolveToolPath(directory || '.', projectRoot)
|
|
1923
|
+
: (directory ? resolveSafePath(directory) : HOME);
|
|
1924
|
+
} catch (err) {
|
|
1925
|
+
return { error: err.message, count: 0, files: [] };
|
|
1926
|
+
}
|
|
1900
1927
|
// Use macOS Spotlight (mdfind) for fast indexed search
|
|
1901
1928
|
const args = [query];
|
|
1902
|
-
if (directory) args.push('-onlyin', searchDir);
|
|
1929
|
+
if (directory || projectRoot) args.push('-onlyin', searchDir);
|
|
1903
1930
|
try {
|
|
1904
1931
|
const { stdout } = await execFileAsync('mdfind', args, {
|
|
1905
1932
|
timeout: 10000,
|
|
@@ -1972,12 +1999,260 @@ function findChromeExecutable() {
|
|
|
1972
1999
|
return CHROME_CANDIDATES.find((p) => fs.existsSync(p)) || null;
|
|
1973
2000
|
}
|
|
1974
2001
|
|
|
1975
|
-
|
|
2002
|
+
function hostAutomationEnv() {
|
|
2003
|
+
const env = { ...process.env };
|
|
2004
|
+
const realHome = process.env.WALL_E_REAL_HOME;
|
|
2005
|
+
if (realHome && fs.existsSync(realHome)) env.HOME = realHome;
|
|
2006
|
+
delete env.WALL_E_DATA_DIR;
|
|
2007
|
+
delete env.WALLE_NOTIFICATIONS;
|
|
2008
|
+
return env;
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
// Minimal CDP-over-pipe client. Chrome 86+ supports --remote-debugging-pipe
|
|
2012
|
+
// which exposes the DevTools Protocol on FD 3 (browser→client) and FD 4
|
|
2013
|
+
// (client→browser) with null-byte-delimited JSON messages. Lets us drive
|
|
2014
|
+
// Page.captureScreenshot({ captureBeyondViewport: true }) for true full-
|
|
2015
|
+
// page captures without depending on puppeteer/playwright/ws.
|
|
2016
|
+
function _openCdpPipe(chromePath, extraArgs = []) {
|
|
2017
|
+
const { spawn } = require('node:child_process');
|
|
2018
|
+
const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'walle-chrome-cdp-'));
|
|
2019
|
+
const args = [
|
|
2020
|
+
'--headless=new',
|
|
2021
|
+
'--disable-gpu',
|
|
2022
|
+
'--no-sandbox',
|
|
2023
|
+
'--hide-scrollbars',
|
|
2024
|
+
`--user-data-dir=${userDataDir}`,
|
|
2025
|
+
'--remote-debugging-pipe',
|
|
2026
|
+
...extraArgs,
|
|
2027
|
+
'about:blank',
|
|
2028
|
+
];
|
|
2029
|
+
// stdio: stdin, stdout, stderr, FD3 (browser→client), FD4 (client→browser).
|
|
2030
|
+
// Node treats every entry as a pipe when set to 'pipe'.
|
|
2031
|
+
const proc = spawn(chromePath, args, { stdio: ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'], env: hostAutomationEnv() });
|
|
2032
|
+
// CDP-over-pipe wire format: Chrome's FD 3 is its READ side (we write commands
|
|
2033
|
+
// there), FD 4 is its WRITE side (we read events/responses there). On the
|
|
2034
|
+
// Node side, proc.stdio[N] is the OTHER end — so stdio[3] is our writable
|
|
2035
|
+
// and stdio[4] is our readable. Flipping these silently hangs every call.
|
|
2036
|
+
const send = proc.stdio[3];
|
|
2037
|
+
const recv = proc.stdio[4];
|
|
2038
|
+
let msgId = 0;
|
|
2039
|
+
const pending = new Map();
|
|
2040
|
+
const events = new Map(); // method -> handler
|
|
2041
|
+
let buffer = Buffer.alloc(0);
|
|
2042
|
+
|
|
2043
|
+
const rejectPending = (err) => {
|
|
2044
|
+
for (const { reject } of pending.values()) reject(err);
|
|
2045
|
+
pending.clear();
|
|
2046
|
+
};
|
|
2047
|
+
|
|
2048
|
+
recv.on('data', (chunk) => {
|
|
2049
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
2050
|
+
let nul;
|
|
2051
|
+
while ((nul = buffer.indexOf(0)) !== -1) {
|
|
2052
|
+
const frame = buffer.subarray(0, nul).toString('utf8');
|
|
2053
|
+
buffer = buffer.subarray(nul + 1);
|
|
2054
|
+
if (!frame) continue;
|
|
2055
|
+
let msg;
|
|
2056
|
+
try { msg = JSON.parse(frame); } catch { continue; }
|
|
2057
|
+
if (msg.id != null && pending.has(msg.id)) {
|
|
2058
|
+
const { resolve, reject } = pending.get(msg.id);
|
|
2059
|
+
pending.delete(msg.id);
|
|
2060
|
+
if (msg.error) reject(new Error(`CDP ${msg.error.code || ''}: ${msg.error.message || JSON.stringify(msg.error)}`));
|
|
2061
|
+
else resolve(msg.result);
|
|
2062
|
+
} else if (msg.method && events.has(msg.method)) {
|
|
2063
|
+
// Event handlers receive (params, sessionId) so callers can filter
|
|
2064
|
+
// by attached page session vs. browser-level events.
|
|
2065
|
+
events.get(msg.method)(msg.params || {}, msg.sessionId);
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
});
|
|
2069
|
+
|
|
2070
|
+
// CDP-over-pipe lands on the Browser target. To send Page.*, Runtime.*,
|
|
2071
|
+
// Emulation.* etc, attach to a page target via Target.attachToTarget
|
|
2072
|
+
// with flatten:true, then pass sessionId on every subsequent message.
|
|
2073
|
+
const call = (method, params = {}, sessionId, timeoutMs = 15000) => new Promise((resolve, reject) => {
|
|
2074
|
+
const id = ++msgId;
|
|
2075
|
+
const timer = setTimeout(() => {
|
|
2076
|
+
pending.delete(id);
|
|
2077
|
+
reject(new Error(`CDP ${method} timed out after ${timeoutMs}ms`));
|
|
2078
|
+
}, timeoutMs);
|
|
2079
|
+
pending.set(id, {
|
|
2080
|
+
resolve: (value) => {
|
|
2081
|
+
clearTimeout(timer);
|
|
2082
|
+
resolve(value);
|
|
2083
|
+
},
|
|
2084
|
+
reject: (err) => {
|
|
2085
|
+
clearTimeout(timer);
|
|
2086
|
+
reject(err);
|
|
2087
|
+
},
|
|
2088
|
+
});
|
|
2089
|
+
const frame = sessionId
|
|
2090
|
+
? { id, method, params, sessionId }
|
|
2091
|
+
: { id, method, params };
|
|
2092
|
+
try {
|
|
2093
|
+
send.write(JSON.stringify(frame) + '\0');
|
|
2094
|
+
} catch (err) {
|
|
2095
|
+
clearTimeout(timer);
|
|
2096
|
+
pending.delete(id);
|
|
2097
|
+
reject(err);
|
|
2098
|
+
}
|
|
2099
|
+
});
|
|
2100
|
+
const on = (method, handler) => { events.set(method, handler); };
|
|
2101
|
+
const close = () => {
|
|
2102
|
+
try { proc.kill('SIGTERM'); } catch {}
|
|
2103
|
+
setTimeout(() => { try { proc.kill('SIGKILL'); } catch {} }, 1500).unref();
|
|
2104
|
+
try { fs.rmSync(userDataDir, { recursive: true, force: true }); } catch {}
|
|
2105
|
+
};
|
|
2106
|
+
|
|
2107
|
+
// Surface spawn failures (e.g. Chrome binary missing) as a usable error.
|
|
2108
|
+
const readyOrError = new Promise((resolve, reject) => {
|
|
2109
|
+
let resolved = false;
|
|
2110
|
+
proc.on('error', (err) => { if (!resolved) { resolved = true; reject(err); } });
|
|
2111
|
+
// The pipe is open immediately; resolve on next tick so callers can wire up handlers first.
|
|
2112
|
+
setImmediate(() => { if (!resolved) { resolved = true; resolve(); } });
|
|
2113
|
+
});
|
|
2114
|
+
proc.once('error', (err) => rejectPending(err));
|
|
2115
|
+
proc.once('exit', (code, signal) => {
|
|
2116
|
+
rejectPending(new Error(`Chrome exited before completing CDP call: ${code ?? signal}`));
|
|
2117
|
+
});
|
|
2118
|
+
|
|
2119
|
+
return { proc, call, on, close, ready: readyOrError };
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
async function _fullPageScreenshotViaCdp({ chrome, url, outPath, width, height, settleMs = 2000, timeoutMs = 45000 }) {
|
|
2123
|
+
const cdp = _openCdpPipe(chrome);
|
|
2124
|
+
let timeoutHandle;
|
|
2125
|
+
try {
|
|
2126
|
+
await Promise.race([
|
|
2127
|
+
cdp.ready,
|
|
2128
|
+
new Promise((_, reject) => { timeoutHandle = setTimeout(() => reject(new Error('Chrome CDP startup timed out')), 10000); }),
|
|
2129
|
+
]);
|
|
2130
|
+
clearTimeout(timeoutHandle);
|
|
2131
|
+
|
|
2132
|
+
// Browser-target call: find the about:blank page and attach to it.
|
|
2133
|
+
// Without an attached page session, Page.*/Runtime.*/Emulation.* are
|
|
2134
|
+
// not routable and Chrome returns "method not found".
|
|
2135
|
+
const { targetInfos } = await cdp.call('Target.getTargets');
|
|
2136
|
+
const pageTarget = (targetInfos || []).find((t) => t.type === 'page');
|
|
2137
|
+
if (!pageTarget) throw new Error('No page target found in headless Chrome');
|
|
2138
|
+
const { sessionId } = await cdp.call('Target.attachToTarget', {
|
|
2139
|
+
targetId: pageTarget.targetId,
|
|
2140
|
+
flatten: true,
|
|
2141
|
+
});
|
|
2142
|
+
if (!sessionId) throw new Error('Target.attachToTarget returned no sessionId');
|
|
2143
|
+
|
|
2144
|
+
// Filter Page.loadEventFired to this session only — other (background)
|
|
2145
|
+
// targets can fire the same event under flattened sessions.
|
|
2146
|
+
const loaded = new Promise((resolve) => {
|
|
2147
|
+
cdp.on('Page.loadEventFired', (_params, sid) => { if (sid === sessionId) resolve(); });
|
|
2148
|
+
});
|
|
2149
|
+
await cdp.call('Page.enable', {}, sessionId);
|
|
2150
|
+
// Emulation lets us simulate the right device pixel + viewport even
|
|
2151
|
+
// when capturing beyond it — important so responsive CSS picks the
|
|
2152
|
+
// right breakpoint.
|
|
2153
|
+
await cdp.call('Emulation.setDeviceMetricsOverride', {
|
|
2154
|
+
width,
|
|
2155
|
+
height,
|
|
2156
|
+
deviceScaleFactor: 1,
|
|
2157
|
+
mobile: false,
|
|
2158
|
+
}, sessionId);
|
|
2159
|
+
await cdp.call('Page.navigate', { url }, sessionId);
|
|
2160
|
+
// Cap how long we wait for `load`; static file:// pages usually fire
|
|
2161
|
+
// immediately, but a remote URL can stall. After that, give JS a beat
|
|
2162
|
+
// to lay out fonts/images before capturing.
|
|
2163
|
+
await Promise.race([
|
|
2164
|
+
loaded,
|
|
2165
|
+
new Promise((resolve) => setTimeout(resolve, Math.max(2000, Math.min(timeoutMs - 5000, 15000)))),
|
|
2166
|
+
]);
|
|
2167
|
+
await new Promise((resolve) => setTimeout(resolve, settleMs));
|
|
2168
|
+
|
|
2169
|
+
const result = await cdp.call('Page.captureScreenshot', {
|
|
2170
|
+
format: 'png',
|
|
2171
|
+
captureBeyondViewport: true,
|
|
2172
|
+
fromSurface: true,
|
|
2173
|
+
}, sessionId);
|
|
2174
|
+
if (!result?.data) throw new Error('Page.captureScreenshot returned no data');
|
|
2175
|
+
fs.writeFileSync(outPath, Buffer.from(result.data, 'base64'));
|
|
2176
|
+
return outPath;
|
|
2177
|
+
} finally {
|
|
2178
|
+
clearTimeout(timeoutHandle);
|
|
2179
|
+
cdp.close();
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
async function _navigateCdpPage({
|
|
2184
|
+
cdp,
|
|
2185
|
+
sessionId,
|
|
2186
|
+
url,
|
|
2187
|
+
width,
|
|
2188
|
+
height,
|
|
2189
|
+
timeoutMs = 45000,
|
|
2190
|
+
settleMs = 750,
|
|
2191
|
+
eventSink,
|
|
2192
|
+
}) {
|
|
2193
|
+
const loaded = new Promise((resolve) => {
|
|
2194
|
+
cdp.on('Page.loadEventFired', (_params, sid) => { if (sid === sessionId) resolve({ ok: true }); });
|
|
2195
|
+
});
|
|
2196
|
+
if (eventSink) {
|
|
2197
|
+
cdp.on('Runtime.exceptionThrown', (params, sid) => {
|
|
2198
|
+
if (sid !== sessionId) return;
|
|
2199
|
+
const details = params.exceptionDetails || {};
|
|
2200
|
+
eventSink.runtimeExceptions.push({
|
|
2201
|
+
text: details.text || '',
|
|
2202
|
+
url: details.url || '',
|
|
2203
|
+
lineNumber: details.lineNumber ?? null,
|
|
2204
|
+
columnNumber: details.columnNumber ?? null,
|
|
2205
|
+
exception: details.exception?.description || details.exception?.value || '',
|
|
2206
|
+
});
|
|
2207
|
+
});
|
|
2208
|
+
cdp.on('Runtime.consoleAPICalled', (params, sid) => {
|
|
2209
|
+
if (sid !== sessionId) return;
|
|
2210
|
+
if (!['error', 'assert'].includes(params.type)) return;
|
|
2211
|
+
eventSink.consoleErrors.push({
|
|
2212
|
+
type: params.type,
|
|
2213
|
+
args: (params.args || []).map((arg) => arg.value ?? arg.description ?? '').join(' ').slice(0, 1000),
|
|
2214
|
+
});
|
|
2215
|
+
});
|
|
2216
|
+
cdp.on('Network.loadingFailed', (params, sid) => {
|
|
2217
|
+
if (sid !== sessionId) return;
|
|
2218
|
+
eventSink.failedRequests.push({
|
|
2219
|
+
requestId: params.requestId,
|
|
2220
|
+
type: params.type,
|
|
2221
|
+
errorText: params.errorText || '',
|
|
2222
|
+
canceled: Boolean(params.canceled),
|
|
2223
|
+
});
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
await cdp.call('Page.enable', {}, sessionId);
|
|
2228
|
+
await cdp.call('Runtime.enable', {}, sessionId);
|
|
2229
|
+
await cdp.call('Network.enable', {}, sessionId);
|
|
2230
|
+
await cdp.call('Emulation.setDeviceMetricsOverride', {
|
|
2231
|
+
width,
|
|
2232
|
+
height,
|
|
2233
|
+
deviceScaleFactor: 1,
|
|
2234
|
+
mobile: width <= 480,
|
|
2235
|
+
}, sessionId);
|
|
2236
|
+
await cdp.call('Page.navigate', { url }, sessionId);
|
|
2237
|
+
await Promise.race([
|
|
2238
|
+
loaded,
|
|
2239
|
+
new Promise((resolve) => setTimeout(resolve, Math.max(2000, Math.min(timeoutMs - 5000, 15000)))),
|
|
2240
|
+
]);
|
|
2241
|
+
await new Promise((resolve) => setTimeout(resolve, settleMs));
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
async function browserSmokeTest({
|
|
2245
|
+
url,
|
|
2246
|
+
viewport = 'desktop',
|
|
2247
|
+
click_selectors,
|
|
2248
|
+
max_clicks = 20,
|
|
2249
|
+
settle_ms = 750,
|
|
2250
|
+
timeout_ms = 45000,
|
|
2251
|
+
} = {}) {
|
|
1976
2252
|
if (!url || typeof url !== 'string') {
|
|
1977
2253
|
return { ok: false, error: 'url is required (string)' };
|
|
1978
2254
|
}
|
|
1979
2255
|
const vp = BROWSER_VIEWPORTS[viewport] || BROWSER_VIEWPORTS.desktop;
|
|
1980
|
-
const outPath = output_path || path.join(os.tmpdir(), `walle-shot-${viewport}-${Date.now()}.png`);
|
|
1981
2256
|
const chrome = findChromeExecutable();
|
|
1982
2257
|
if (!chrome) {
|
|
1983
2258
|
return {
|
|
@@ -1988,6 +2263,179 @@ async function browserScreenshot({ url, output_path, viewport = 'desktop' } = {}
|
|
|
1988
2263
|
};
|
|
1989
2264
|
}
|
|
1990
2265
|
|
|
2266
|
+
const cdp = _openCdpPipe(chrome);
|
|
2267
|
+
let timeoutHandle;
|
|
2268
|
+
const events = { runtimeExceptions: [], consoleErrors: [], failedRequests: [] };
|
|
2269
|
+
try {
|
|
2270
|
+
await Promise.race([
|
|
2271
|
+
cdp.ready,
|
|
2272
|
+
new Promise((_, reject) => { timeoutHandle = setTimeout(() => reject(new Error('Chrome CDP startup timed out')), 10000); }),
|
|
2273
|
+
]);
|
|
2274
|
+
clearTimeout(timeoutHandle);
|
|
2275
|
+
|
|
2276
|
+
const { targetInfos } = await cdp.call('Target.getTargets');
|
|
2277
|
+
const pageTarget = (targetInfos || []).find((t) => t.type === 'page');
|
|
2278
|
+
if (!pageTarget) throw new Error('No page target found in headless Chrome');
|
|
2279
|
+
const { sessionId } = await cdp.call('Target.attachToTarget', {
|
|
2280
|
+
targetId: pageTarget.targetId,
|
|
2281
|
+
flatten: true,
|
|
2282
|
+
});
|
|
2283
|
+
if (!sessionId) throw new Error('Target.attachToTarget returned no sessionId');
|
|
2284
|
+
|
|
2285
|
+
await _navigateCdpPage({
|
|
2286
|
+
cdp,
|
|
2287
|
+
sessionId,
|
|
2288
|
+
url,
|
|
2289
|
+
width: vp.width,
|
|
2290
|
+
height: vp.height,
|
|
2291
|
+
timeoutMs: timeout_ms,
|
|
2292
|
+
settleMs: settle_ms,
|
|
2293
|
+
eventSink: events,
|
|
2294
|
+
});
|
|
2295
|
+
|
|
2296
|
+
const selectors = Array.isArray(click_selectors) && click_selectors.length
|
|
2297
|
+
? click_selectors
|
|
2298
|
+
: ['[onclick]', 'button', '[role="button"]', 'a[href^="#"]'];
|
|
2299
|
+
const clickResult = await cdp.call('Runtime.evaluate', {
|
|
2300
|
+
awaitPromise: true,
|
|
2301
|
+
returnByValue: true,
|
|
2302
|
+
expression: `(() => {
|
|
2303
|
+
const selectors = ${JSON.stringify(selectors)};
|
|
2304
|
+
const maxClicks = ${Math.max(0, Math.min(100, Number(max_clicks) || 20))};
|
|
2305
|
+
const seen = new Set();
|
|
2306
|
+
const failures = [];
|
|
2307
|
+
const clicked = [];
|
|
2308
|
+
const describe = (el) => (el.getAttribute('aria-label') || el.textContent || el.id || el.className || el.tagName || '').toString().replace(/\\s+/g, ' ').trim().slice(0, 120);
|
|
2309
|
+
const elements = [];
|
|
2310
|
+
for (const selector of selectors) {
|
|
2311
|
+
for (const el of Array.from(document.querySelectorAll(selector))) {
|
|
2312
|
+
if (seen.has(el)) continue;
|
|
2313
|
+
seen.add(el);
|
|
2314
|
+
if (elements.length >= maxClicks) break;
|
|
2315
|
+
elements.push({ el, selector });
|
|
2316
|
+
}
|
|
2317
|
+
if (elements.length >= maxClicks) break;
|
|
2318
|
+
}
|
|
2319
|
+
for (const { el, selector } of elements) {
|
|
2320
|
+
if (el.disabled || el.getAttribute('aria-disabled') === 'true') continue;
|
|
2321
|
+
try {
|
|
2322
|
+
el.scrollIntoView({ block: 'center', inline: 'center' });
|
|
2323
|
+
el.click();
|
|
2324
|
+
clicked.push({ selector, label: describe(el) });
|
|
2325
|
+
} catch (err) {
|
|
2326
|
+
failures.push({ selector, label: describe(el), error: String(err && (err.message || err)) });
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
return {
|
|
2330
|
+
readyState: document.readyState,
|
|
2331
|
+
title: document.title || '',
|
|
2332
|
+
bodyTextLength: document.body ? document.body.innerText.length : 0,
|
|
2333
|
+
clicked,
|
|
2334
|
+
failures,
|
|
2335
|
+
};
|
|
2336
|
+
})()`,
|
|
2337
|
+
}, sessionId);
|
|
2338
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(1000, Math.max(100, settle_ms))));
|
|
2339
|
+
|
|
2340
|
+
const value = clickResult?.result?.value || {};
|
|
2341
|
+
const failures = [];
|
|
2342
|
+
for (const item of events.runtimeExceptions) failures.push({ type: 'runtime_exception', ...item });
|
|
2343
|
+
for (const item of events.consoleErrors) failures.push({ type: 'console_error', ...item });
|
|
2344
|
+
for (const item of value.failures || []) failures.push({ type: 'click_failure', ...item });
|
|
2345
|
+
const significantFailedRequests = events.failedRequests.filter((item) => !item.canceled);
|
|
2346
|
+
for (const item of significantFailedRequests) failures.push({ type: 'network_failure', ...item });
|
|
2347
|
+
|
|
2348
|
+
return {
|
|
2349
|
+
ok: failures.length === 0,
|
|
2350
|
+
url,
|
|
2351
|
+
viewport,
|
|
2352
|
+
width: vp.width,
|
|
2353
|
+
height: vp.height,
|
|
2354
|
+
chrome,
|
|
2355
|
+
readyState: value.readyState || '',
|
|
2356
|
+
title: value.title || '',
|
|
2357
|
+
bodyTextLength: value.bodyTextLength || 0,
|
|
2358
|
+
clicked: value.clicked || [],
|
|
2359
|
+
runtimeExceptions: events.runtimeExceptions,
|
|
2360
|
+
consoleErrors: events.consoleErrors,
|
|
2361
|
+
failedRequests: significantFailedRequests,
|
|
2362
|
+
failures,
|
|
2363
|
+
};
|
|
2364
|
+
} catch (err) {
|
|
2365
|
+
return {
|
|
2366
|
+
ok: false,
|
|
2367
|
+
error: `Chrome CDP browser smoke test failed: ${err.message}`,
|
|
2368
|
+
url,
|
|
2369
|
+
viewport,
|
|
2370
|
+
chrome,
|
|
2371
|
+
};
|
|
2372
|
+
} finally {
|
|
2373
|
+
clearTimeout(timeoutHandle);
|
|
2374
|
+
cdp.close();
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
async function browserScreenshot({ url, output_path, viewport = 'desktop', full_page = false } = {}) {
|
|
2379
|
+
if (!url || typeof url !== 'string') {
|
|
2380
|
+
return { ok: false, error: 'url is required (string)' };
|
|
2381
|
+
}
|
|
2382
|
+
const vp = BROWSER_VIEWPORTS[viewport] || BROWSER_VIEWPORTS.desktop;
|
|
2383
|
+
const outPath = expandUserPath(output_path) || path.join(os.tmpdir(), `walle-shot-${viewport}${full_page ? '-fullpage' : ''}-${Date.now()}.png`);
|
|
2384
|
+
const chrome = findChromeExecutable();
|
|
2385
|
+
if (!chrome) {
|
|
2386
|
+
return {
|
|
2387
|
+
ok: false,
|
|
2388
|
+
error: 'No Chromium-based browser found at standard /Applications paths. Install Chrome, Chromium, Edge, or Brave; or set WALLE_CHROME_PATH.',
|
|
2389
|
+
url,
|
|
2390
|
+
viewport,
|
|
2391
|
+
};
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
// Full-page path: drive Chrome via CDP and use captureBeyondViewport so
|
|
2395
|
+
// content below the fold is included. Slower (~3-5s) but captures the
|
|
2396
|
+
// entire scrollable page in a single PNG — the only reliable way to
|
|
2397
|
+
// self-critique galleries, footers, etc. that sit below the viewport.
|
|
2398
|
+
if (full_page) {
|
|
2399
|
+
try {
|
|
2400
|
+
await _fullPageScreenshotViaCdp({
|
|
2401
|
+
chrome,
|
|
2402
|
+
url,
|
|
2403
|
+
outPath,
|
|
2404
|
+
width: vp.width,
|
|
2405
|
+
height: vp.height,
|
|
2406
|
+
});
|
|
2407
|
+
} catch (err) {
|
|
2408
|
+
return {
|
|
2409
|
+
ok: false,
|
|
2410
|
+
error: `Chrome CDP full-page capture failed: ${err.message}`,
|
|
2411
|
+
url,
|
|
2412
|
+
viewport,
|
|
2413
|
+
chrome,
|
|
2414
|
+
};
|
|
2415
|
+
}
|
|
2416
|
+
const exists = fs.existsSync(outPath);
|
|
2417
|
+
const size = exists ? fs.statSync(outPath).size : 0;
|
|
2418
|
+
return {
|
|
2419
|
+
ok: exists && size > 0,
|
|
2420
|
+
path: outPath,
|
|
2421
|
+
exists,
|
|
2422
|
+
size,
|
|
2423
|
+
viewport,
|
|
2424
|
+
width: vp.width,
|
|
2425
|
+
height: vp.height, // viewport height; actual PNG can be taller
|
|
2426
|
+
full_page: true,
|
|
2427
|
+
url,
|
|
2428
|
+
chrome,
|
|
2429
|
+
artifact: exists && size > 0 ? {
|
|
2430
|
+
kind: 'screenshot',
|
|
2431
|
+
path: outPath,
|
|
2432
|
+
mimeType: 'image/png',
|
|
2433
|
+
bytes: size,
|
|
2434
|
+
metadata: { viewport, width: vp.width, height: vp.height, full_page: true, url },
|
|
2435
|
+
} : null,
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2438
|
+
|
|
1991
2439
|
const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'walle-chrome-'));
|
|
1992
2440
|
const args = [
|
|
1993
2441
|
'--headless=new',
|
|
@@ -2002,11 +2450,12 @@ async function browserScreenshot({ url, output_path, viewport = 'desktop' } = {}
|
|
|
2002
2450
|
];
|
|
2003
2451
|
|
|
2004
2452
|
try {
|
|
2005
|
-
await execFileAsync(chrome, args, { timeout: 30000, maxBuffer: 4 * 1024 * 1024 });
|
|
2453
|
+
await execFileAsync(chrome, args, { timeout: 30000, maxBuffer: 4 * 1024 * 1024, env: hostAutomationEnv() });
|
|
2006
2454
|
} catch (err) {
|
|
2007
2455
|
return {
|
|
2008
2456
|
ok: false,
|
|
2009
2457
|
error: `Chrome headless exited with error: ${err.message}`,
|
|
2458
|
+
stderr: String(err.stderr || '').slice(0, 2000),
|
|
2010
2459
|
url,
|
|
2011
2460
|
viewport,
|
|
2012
2461
|
chrome,
|
|
@@ -2027,9 +2476,739 @@ async function browserScreenshot({ url, output_path, viewport = 'desktop' } = {}
|
|
|
2027
2476
|
height: vp.height,
|
|
2028
2477
|
url,
|
|
2029
2478
|
chrome,
|
|
2479
|
+
artifact: exists && size > 0 ? {
|
|
2480
|
+
kind: 'screenshot',
|
|
2481
|
+
path: outPath,
|
|
2482
|
+
mimeType: 'image/png',
|
|
2483
|
+
bytes: size,
|
|
2484
|
+
metadata: { viewport, width: vp.width, height: vp.height, url },
|
|
2485
|
+
} : null,
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
const PDF_MAX_BYTES = 32 * 1024 * 1024;
|
|
2490
|
+
const PDF_MAX_PAGE_RANGE = 20;
|
|
2491
|
+
|
|
2492
|
+
function fileSha256(filePath) {
|
|
2493
|
+
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
function normalizePdfPath(filePath, projectRoot) {
|
|
2497
|
+
if (!filePath || typeof filePath !== 'string') throw new Error('file_path is required');
|
|
2498
|
+
return resolveToolPath(expandUserPath(filePath), projectRoot);
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
function validatePdfFile(filePath, { maxBytes = PDF_MAX_BYTES } = {}) {
|
|
2502
|
+
if (!fs.existsSync(filePath)) return { ok: false, error: `PDF not found: ${filePath}` };
|
|
2503
|
+
const stat = fs.statSync(filePath);
|
|
2504
|
+
if (!stat.isFile()) return { ok: false, error: `PDF path is not a file: ${filePath}` };
|
|
2505
|
+
if (stat.size <= 0) return { ok: false, error: 'PDF file is empty', path: filePath, bytes: stat.size };
|
|
2506
|
+
if (stat.size > maxBytes) {
|
|
2507
|
+
return { ok: false, error: `PDF file exceeds size limit (${stat.size} > ${maxBytes} bytes)`, path: filePath, bytes: stat.size };
|
|
2508
|
+
}
|
|
2509
|
+
const fd = fs.openSync(filePath, 'r');
|
|
2510
|
+
try {
|
|
2511
|
+
const magic = Buffer.alloc(5);
|
|
2512
|
+
fs.readSync(fd, magic, 0, 5, 0);
|
|
2513
|
+
if (magic.toString('utf8') !== '%PDF-') {
|
|
2514
|
+
return { ok: false, error: 'File does not start with %PDF- magic bytes', path: filePath, bytes: stat.size };
|
|
2515
|
+
}
|
|
2516
|
+
} finally {
|
|
2517
|
+
fs.closeSync(fd);
|
|
2518
|
+
}
|
|
2519
|
+
return { ok: true, path: filePath, bytes: stat.size, sha256: fileSha256(filePath) };
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
async function pdfInfo({ file_path, max_bytes, projectRoot } = {}) {
|
|
2523
|
+
let resolved;
|
|
2524
|
+
try {
|
|
2525
|
+
resolved = normalizePdfPath(file_path, projectRoot);
|
|
2526
|
+
} catch (err) {
|
|
2527
|
+
return { ok: false, error: err.message };
|
|
2528
|
+
}
|
|
2529
|
+
const validation = validatePdfFile(resolved, { maxBytes: Number(max_bytes) || PDF_MAX_BYTES });
|
|
2530
|
+
if (!validation.ok) return validation;
|
|
2531
|
+
|
|
2532
|
+
const base = {
|
|
2533
|
+
ok: true,
|
|
2534
|
+
path: resolved,
|
|
2535
|
+
bytes: validation.bytes,
|
|
2536
|
+
sha256: validation.sha256,
|
|
2537
|
+
mimeType: 'application/pdf',
|
|
2538
|
+
page_count: null,
|
|
2539
|
+
encrypted: null,
|
|
2540
|
+
};
|
|
2541
|
+
|
|
2542
|
+
try {
|
|
2543
|
+
const { stdout } = await execFileAsync('pdfinfo', [resolved], {
|
|
2544
|
+
timeout: 15000,
|
|
2545
|
+
maxBuffer: 512 * 1024,
|
|
2546
|
+
});
|
|
2547
|
+
const pages = stdout.match(/^Pages:\s*(\d+)/mi);
|
|
2548
|
+
const encrypted = stdout.match(/^Encrypted:\s*(yes|no)/mi);
|
|
2549
|
+
return {
|
|
2550
|
+
...base,
|
|
2551
|
+
page_count: pages ? Number(pages[1]) : null,
|
|
2552
|
+
encrypted: encrypted ? encrypted[1].toLowerCase() === 'yes' : null,
|
|
2553
|
+
raw_info: stdout.trim().slice(0, 8000),
|
|
2554
|
+
artifact: {
|
|
2555
|
+
kind: 'pdf',
|
|
2556
|
+
path: resolved,
|
|
2557
|
+
mimeType: 'application/pdf',
|
|
2558
|
+
bytes: validation.bytes,
|
|
2559
|
+
sha256: validation.sha256,
|
|
2560
|
+
metadata: { page_count: pages ? Number(pages[1]) : null },
|
|
2561
|
+
},
|
|
2562
|
+
};
|
|
2563
|
+
} catch (err) {
|
|
2564
|
+
if (err && err.code === 'ENOENT') {
|
|
2565
|
+
return {
|
|
2566
|
+
...base,
|
|
2567
|
+
warnings: ['pdfinfo is unavailable; validated file existence, size, sha256, and %PDF- magic bytes only.'],
|
|
2568
|
+
artifact: {
|
|
2569
|
+
kind: 'pdf',
|
|
2570
|
+
path: resolved,
|
|
2571
|
+
mimeType: 'application/pdf',
|
|
2572
|
+
bytes: validation.bytes,
|
|
2573
|
+
sha256: validation.sha256,
|
|
2574
|
+
metadata: { page_count: null, verification: 'magic-bytes-only' },
|
|
2575
|
+
},
|
|
2576
|
+
};
|
|
2577
|
+
}
|
|
2578
|
+
return {
|
|
2579
|
+
...base,
|
|
2580
|
+
ok: false,
|
|
2581
|
+
error: `pdfinfo failed: ${String(err.stderr || err.message || err).trim().slice(0, 2000)}`,
|
|
2582
|
+
exitCode: err.code,
|
|
2583
|
+
};
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
function parsePdfPageRange(pages, { pageCount = null, defaultRange = '1', maxPages = PDF_MAX_PAGE_RANGE } = {}) {
|
|
2588
|
+
const raw = String(pages || defaultRange || '1').trim();
|
|
2589
|
+
const match = raw.match(/^(\d+)(?:-(\d*)?)?$/);
|
|
2590
|
+
if (!match) return { ok: false, error: `Invalid page range "${raw}". Use "1", "1-3", or "2-".` };
|
|
2591
|
+
const start = Number(match[1]);
|
|
2592
|
+
if (!Number.isInteger(start) || start < 1) return { ok: false, error: 'PDF page range must start at page 1 or later.' };
|
|
2593
|
+
let end;
|
|
2594
|
+
if (!raw.includes('-')) end = start;
|
|
2595
|
+
else if (match[2]) end = Number(match[2]);
|
|
2596
|
+
else end = pageCount ? Number(pageCount) : start + maxPages - 1;
|
|
2597
|
+
if (!Number.isInteger(end) || end < start) return { ok: false, error: `Invalid page range "${raw}". End must be >= start.` };
|
|
2598
|
+
if (pageCount && start > pageCount) return { ok: false, error: `PDF page range starts after the last page (${pageCount}).` };
|
|
2599
|
+
if (pageCount) end = Math.min(end, Number(pageCount));
|
|
2600
|
+
const count = end - start + 1;
|
|
2601
|
+
if (count > maxPages) return { ok: false, error: `PDF page range requests ${count} pages; maximum is ${maxPages}.` };
|
|
2602
|
+
return { ok: true, start, end, count, raw };
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
function resolvePdfOutputDir(outputDir, projectRoot) {
|
|
2606
|
+
const expanded = expandUserPath(outputDir);
|
|
2607
|
+
const resolved = path.isAbsolute(expanded)
|
|
2608
|
+
? path.resolve(expanded)
|
|
2609
|
+
: path.resolve(projectRoot || process.cwd(), expanded);
|
|
2610
|
+
const roots = [os.tmpdir(), HOME];
|
|
2611
|
+
if (projectRoot) roots.push(projectRoot);
|
|
2612
|
+
const realResolved = realpathBestEffort(resolved);
|
|
2613
|
+
if (!roots.map(realpathBestEffort).some((root) => isPathWithin(root, realResolved))) {
|
|
2614
|
+
throw new Error(`PDF preview output directory ${resolved} must be under the project, HOME, or the system temp directory`);
|
|
2615
|
+
}
|
|
2616
|
+
return realResolved;
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
async function pdfRenderPages({ file_path, pages = '1', output_dir, dpi = 144, projectRoot } = {}) {
|
|
2620
|
+
const info = await pdfInfo({ file_path, projectRoot });
|
|
2621
|
+
if (!info.ok) return info;
|
|
2622
|
+
const range = parsePdfPageRange(pages, { pageCount: info.page_count, defaultRange: '1' });
|
|
2623
|
+
if (!range.ok) return { ok: false, error: range.error, path: info.path };
|
|
2624
|
+
const renderDpi = Math.max(72, Math.min(200, Number(dpi) || 144));
|
|
2625
|
+
let outDir;
|
|
2626
|
+
try {
|
|
2627
|
+
outDir = output_dir
|
|
2628
|
+
? resolvePdfOutputDir(output_dir, projectRoot)
|
|
2629
|
+
: fs.mkdtempSync(path.join(os.tmpdir(), 'walle-pdf-pages-'));
|
|
2630
|
+
} catch (err) {
|
|
2631
|
+
return { ok: false, error: err.message, path: info.path };
|
|
2632
|
+
}
|
|
2633
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
2634
|
+
const prefix = path.join(outDir, `page-${Date.now()}`);
|
|
2635
|
+
try {
|
|
2636
|
+
await execFileAsync('pdftoppm', [
|
|
2637
|
+
'-jpeg',
|
|
2638
|
+
'-r', String(renderDpi),
|
|
2639
|
+
'-f', String(range.start),
|
|
2640
|
+
'-l', String(range.end),
|
|
2641
|
+
info.path,
|
|
2642
|
+
prefix,
|
|
2643
|
+
], {
|
|
2644
|
+
timeout: 30000,
|
|
2645
|
+
maxBuffer: 1024 * 1024,
|
|
2646
|
+
});
|
|
2647
|
+
} catch (err) {
|
|
2648
|
+
if (err && err.code === 'ENOENT') {
|
|
2649
|
+
return { ok: false, error: 'pdftoppm is unavailable; install poppler to render PDF page previews.', dependency: 'pdftoppm', path: info.path };
|
|
2650
|
+
}
|
|
2651
|
+
return { ok: false, error: `pdftoppm failed: ${String(err.stderr || err.message || err).trim().slice(0, 2000)}`, path: info.path };
|
|
2652
|
+
}
|
|
2653
|
+
const base = path.basename(prefix);
|
|
2654
|
+
const previews = fs.readdirSync(outDir)
|
|
2655
|
+
.filter((name) => name.startsWith(base) && /\.(?:jpg|jpeg)$/i.test(name))
|
|
2656
|
+
.map((name) => path.join(outDir, name))
|
|
2657
|
+
.sort((a, b) => a.localeCompare(b));
|
|
2658
|
+
if (!previews.length) return { ok: false, error: 'pdftoppm completed but produced no preview images.', path: info.path, output_dir: outDir };
|
|
2659
|
+
return {
|
|
2660
|
+
ok: true,
|
|
2661
|
+
path: info.path,
|
|
2662
|
+
output_dir: outDir,
|
|
2663
|
+
pages: { start: range.start, end: range.end, count: range.count },
|
|
2664
|
+
dpi: renderDpi,
|
|
2665
|
+
preview_paths: previews,
|
|
2666
|
+
artifacts: previews.map((previewPath, index) => {
|
|
2667
|
+
const stat = fs.statSync(previewPath);
|
|
2668
|
+
return {
|
|
2669
|
+
kind: 'pdf_page_preview',
|
|
2670
|
+
path: previewPath,
|
|
2671
|
+
mimeType: 'image/jpeg',
|
|
2672
|
+
bytes: stat.size,
|
|
2673
|
+
metadata: {
|
|
2674
|
+
source_pdf: info.path,
|
|
2675
|
+
page: range.start + index,
|
|
2676
|
+
page_count: info.page_count,
|
|
2677
|
+
dpi: renderDpi,
|
|
2678
|
+
},
|
|
2679
|
+
};
|
|
2680
|
+
}),
|
|
2681
|
+
};
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
async function pdfReadPages({ file_path, pages = '1-5', max_chars = 20000, projectRoot } = {}) {
|
|
2685
|
+
const info = await pdfInfo({ file_path, projectRoot });
|
|
2686
|
+
if (!info.ok) return info;
|
|
2687
|
+
const range = parsePdfPageRange(pages, { pageCount: info.page_count, defaultRange: '1-5' });
|
|
2688
|
+
if (!range.ok) return { ok: false, error: range.error, path: info.path };
|
|
2689
|
+
try {
|
|
2690
|
+
const { stdout } = await execFileAsync('pdftotext', [
|
|
2691
|
+
'-f', String(range.start),
|
|
2692
|
+
'-l', String(range.end),
|
|
2693
|
+
'-layout',
|
|
2694
|
+
info.path,
|
|
2695
|
+
'-',
|
|
2696
|
+
], {
|
|
2697
|
+
timeout: 30000,
|
|
2698
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
2699
|
+
});
|
|
2700
|
+
const maxChars = Math.max(1000, Math.min(Number(max_chars) || 20000, 200000));
|
|
2701
|
+
return {
|
|
2702
|
+
ok: true,
|
|
2703
|
+
path: info.path,
|
|
2704
|
+
page_count: info.page_count,
|
|
2705
|
+
pages: { start: range.start, end: range.end, count: range.count },
|
|
2706
|
+
text: stdout.slice(0, maxChars),
|
|
2707
|
+
truncated: stdout.length > maxChars,
|
|
2708
|
+
artifact: info.artifact || {
|
|
2709
|
+
kind: 'pdf',
|
|
2710
|
+
path: info.path,
|
|
2711
|
+
mimeType: 'application/pdf',
|
|
2712
|
+
bytes: info.bytes,
|
|
2713
|
+
sha256: info.sha256,
|
|
2714
|
+
},
|
|
2715
|
+
};
|
|
2716
|
+
} catch (err) {
|
|
2717
|
+
if (err && err.code === 'ENOENT') {
|
|
2718
|
+
return { ok: false, error: 'pdftotext is unavailable; install poppler to extract PDF text.', dependency: 'pdftotext', path: info.path };
|
|
2719
|
+
}
|
|
2720
|
+
return { ok: false, error: `pdftotext failed: ${String(err.stderr || err.message || err).trim().slice(0, 2000)}`, path: info.path };
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
function makePdfBinaryCandidates(projectRoot) {
|
|
2725
|
+
const env = process.env;
|
|
2726
|
+
return [
|
|
2727
|
+
env.MAKE_PDF_BIN,
|
|
2728
|
+
projectRoot ? path.join(projectRoot, '.agents/skills/gstack/make-pdf/dist/pdf') : '',
|
|
2729
|
+
path.join(HOME, '.codex/skills/gstack/make-pdf/dist/pdf'),
|
|
2730
|
+
path.join(HOME, '.gstack/repos/gstack/.agents/skills/gstack/make-pdf/dist/pdf'),
|
|
2731
|
+
path.join(HOME, '.gstack/repos/gstack/.agents/skills/gstack-make-pdf/dist/pdf'),
|
|
2732
|
+
env.GSTACK_MAKE_PDF ? path.join(HOME, env.GSTACK_MAKE_PDF, 'pdf') : '',
|
|
2733
|
+
].filter(Boolean);
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
function findMakePdfBinary(projectRoot) {
|
|
2737
|
+
return makePdfBinaryCandidates(projectRoot).find((candidate) => {
|
|
2738
|
+
try {
|
|
2739
|
+
return fs.existsSync(candidate) && fs.statSync(candidate).isFile() && (fs.statSync(candidate).mode & 0o111);
|
|
2740
|
+
} catch {
|
|
2741
|
+
return false;
|
|
2742
|
+
}
|
|
2743
|
+
}) || '';
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
function inferPdfOutputPath(inputPath, outputPath, projectRoot) {
|
|
2747
|
+
if (outputPath) return resolveToolPath(expandUserPath(outputPath), projectRoot);
|
|
2748
|
+
return path.join(path.dirname(inputPath), `${path.basename(inputPath, path.extname(inputPath))}.pdf`);
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
async function makePdf({
|
|
2752
|
+
input_path,
|
|
2753
|
+
output_path,
|
|
2754
|
+
title,
|
|
2755
|
+
author,
|
|
2756
|
+
date,
|
|
2757
|
+
page_size,
|
|
2758
|
+
margins,
|
|
2759
|
+
cover = false,
|
|
2760
|
+
toc = false,
|
|
2761
|
+
watermark = '',
|
|
2762
|
+
allow_network = false,
|
|
2763
|
+
no_confidential = false,
|
|
2764
|
+
render_preview = true,
|
|
2765
|
+
projectRoot,
|
|
2766
|
+
} = {}) {
|
|
2767
|
+
const project = projectRoot || process.cwd();
|
|
2768
|
+
const binary = findMakePdfBinary(project);
|
|
2769
|
+
if (!binary) {
|
|
2770
|
+
return {
|
|
2771
|
+
ok: false,
|
|
2772
|
+
error: 'make-pdf renderer is not available. Build gstack make-pdf or set MAKE_PDF_BIN to the renderer binary.',
|
|
2773
|
+
candidates: makePdfBinaryCandidates(project),
|
|
2774
|
+
};
|
|
2775
|
+
}
|
|
2776
|
+
let inputPath;
|
|
2777
|
+
let outputPath;
|
|
2778
|
+
try {
|
|
2779
|
+
if (!input_path || typeof input_path !== 'string') throw new Error('input_path is required');
|
|
2780
|
+
inputPath = resolveToolPath(expandUserPath(input_path), project);
|
|
2781
|
+
outputPath = inferPdfOutputPath(inputPath, output_path, project);
|
|
2782
|
+
} catch (err) {
|
|
2783
|
+
return { ok: false, error: err.message };
|
|
2784
|
+
}
|
|
2785
|
+
if (!fs.existsSync(inputPath) || !fs.statSync(inputPath).isFile()) {
|
|
2786
|
+
return { ok: false, error: `Input file not found: ${inputPath}` };
|
|
2787
|
+
}
|
|
2788
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
2789
|
+
const args = ['generate', '--quiet'];
|
|
2790
|
+
if (cover) args.push('--cover');
|
|
2791
|
+
if (toc) args.push('--toc');
|
|
2792
|
+
if (watermark) args.push('--watermark', String(watermark));
|
|
2793
|
+
if (allow_network) args.push('--allow-network');
|
|
2794
|
+
if (no_confidential) args.push('--no-confidential');
|
|
2795
|
+
if (title) args.push('--title', String(title));
|
|
2796
|
+
if (author) args.push('--author', String(author));
|
|
2797
|
+
if (date) args.push('--date', String(date));
|
|
2798
|
+
if (page_size) args.push('--page-size', String(page_size));
|
|
2799
|
+
if (margins) args.push('--margins', String(margins));
|
|
2800
|
+
args.push(inputPath, outputPath);
|
|
2801
|
+
|
|
2802
|
+
let stdout = '';
|
|
2803
|
+
let stderr = '';
|
|
2804
|
+
try {
|
|
2805
|
+
const result = await execFileAsync(binary, args, {
|
|
2806
|
+
timeout: 120000,
|
|
2807
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
2808
|
+
cwd: project,
|
|
2809
|
+
});
|
|
2810
|
+
stdout = result.stdout || '';
|
|
2811
|
+
stderr = result.stderr || '';
|
|
2812
|
+
} catch (err) {
|
|
2813
|
+
return {
|
|
2814
|
+
ok: false,
|
|
2815
|
+
error: `make-pdf failed: ${String(err.stderr || err.message || err).trim().slice(0, 4000)}`,
|
|
2816
|
+
stdout: String(err.stdout || '').slice(0, 4000),
|
|
2817
|
+
stderr: String(err.stderr || '').slice(0, 4000),
|
|
2818
|
+
exitCode: err.code,
|
|
2819
|
+
};
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
const stdoutPath = stdout.trim().split(/\r?\n/).find((line) => /\.pdf$/i.test(line.trim()));
|
|
2823
|
+
const producedPath = stdoutPath ? resolveToolPath(expandUserPath(stdoutPath.trim()), project) : outputPath;
|
|
2824
|
+
const info = await pdfInfo({ file_path: producedPath, projectRoot: project });
|
|
2825
|
+
if (!info.ok) {
|
|
2826
|
+
return {
|
|
2827
|
+
ok: false,
|
|
2828
|
+
error: `Generated PDF failed validation: ${info.error || 'unknown validation error'}`,
|
|
2829
|
+
path: producedPath,
|
|
2830
|
+
stdout: stdout.trim(),
|
|
2831
|
+
stderr: stderr.trim(),
|
|
2832
|
+
};
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
const artifacts = [info.artifact || {
|
|
2836
|
+
kind: 'pdf',
|
|
2837
|
+
path: producedPath,
|
|
2838
|
+
mimeType: 'application/pdf',
|
|
2839
|
+
bytes: info.bytes,
|
|
2840
|
+
sha256: info.sha256,
|
|
2841
|
+
metadata: { page_count: info.page_count },
|
|
2842
|
+
}];
|
|
2843
|
+
let preview = null;
|
|
2844
|
+
if (render_preview !== false && process.env.WALLE_DISABLE_PDF_PREVIEW !== '1') {
|
|
2845
|
+
preview = await pdfRenderPages({
|
|
2846
|
+
file_path: producedPath,
|
|
2847
|
+
pages: '1',
|
|
2848
|
+
output_dir: path.join(os.tmpdir(), `walle-pdf-preview-${Date.now()}`),
|
|
2849
|
+
projectRoot: project,
|
|
2850
|
+
});
|
|
2851
|
+
if (!preview.ok && preview.dependency !== 'pdftoppm') {
|
|
2852
|
+
return {
|
|
2853
|
+
ok: false,
|
|
2854
|
+
error: `Generated PDF preview validation failed: ${preview.error}`,
|
|
2855
|
+
path: producedPath,
|
|
2856
|
+
pdf: info,
|
|
2857
|
+
};
|
|
2858
|
+
}
|
|
2859
|
+
if (preview.ok && Array.isArray(preview.artifacts)) artifacts.push(...preview.artifacts);
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
return {
|
|
2863
|
+
ok: true,
|
|
2864
|
+
path: producedPath,
|
|
2865
|
+
bytes: info.bytes,
|
|
2866
|
+
sha256: info.sha256,
|
|
2867
|
+
page_count: info.page_count,
|
|
2868
|
+
preview_paths: preview?.ok ? preview.preview_paths : [],
|
|
2869
|
+
preview_warning: preview && !preview.ok ? preview.error : '',
|
|
2870
|
+
stdout: stdout.trim(),
|
|
2871
|
+
stderr: stderr.trim(),
|
|
2872
|
+
artifact: artifacts[0],
|
|
2873
|
+
artifacts,
|
|
2030
2874
|
};
|
|
2031
2875
|
}
|
|
2032
2876
|
|
|
2877
|
+
function checkUrl({ url, timeout_ms = 5000 } = {}) {
|
|
2878
|
+
return new Promise((resolve) => {
|
|
2879
|
+
if (!url || typeof url !== 'string') {
|
|
2880
|
+
resolve({ ok: false, error: 'url is required (string)' });
|
|
2881
|
+
return;
|
|
2882
|
+
}
|
|
2883
|
+
let parsed;
|
|
2884
|
+
try {
|
|
2885
|
+
parsed = new URL(url);
|
|
2886
|
+
} catch (err) {
|
|
2887
|
+
resolve({ ok: false, error: `Invalid URL: ${err.message}`, url });
|
|
2888
|
+
return;
|
|
2889
|
+
}
|
|
2890
|
+
const client = parsed.protocol === 'https:' ? https : parsed.protocol === 'http:' ? http : null;
|
|
2891
|
+
if (!client) {
|
|
2892
|
+
resolve({ ok: false, error: 'Only http:// and https:// URLs are supported', url });
|
|
2893
|
+
return;
|
|
2894
|
+
}
|
|
2895
|
+
const started = Date.now();
|
|
2896
|
+
const req = client.request(parsed, { method: 'GET', timeout: timeout_ms }, (res) => {
|
|
2897
|
+
let bytes = 0;
|
|
2898
|
+
res.on('data', (chunk) => {
|
|
2899
|
+
bytes += chunk.length;
|
|
2900
|
+
});
|
|
2901
|
+
res.on('end', () => {
|
|
2902
|
+
resolve({
|
|
2903
|
+
ok: res.statusCode >= 200 && res.statusCode < 400,
|
|
2904
|
+
status: res.statusCode,
|
|
2905
|
+
bytes,
|
|
2906
|
+
elapsed_ms: Date.now() - started,
|
|
2907
|
+
url,
|
|
2908
|
+
reachability_scope: _localUrlScope(parsed),
|
|
2909
|
+
user_browser_reachability: _localUrlScope(parsed) === 'wall-e-host-loopback' ? 'not_proven' : 'not_checked',
|
|
2910
|
+
note: _localUrlScope(parsed) === 'wall-e-host-loopback'
|
|
2911
|
+
? 'This proves only that Wall-E host loopback can reach the URL. Phone or remote-browser reachability needs CTM remote/tunnel context.'
|
|
2912
|
+
: undefined,
|
|
2913
|
+
});
|
|
2914
|
+
});
|
|
2915
|
+
});
|
|
2916
|
+
req.on('timeout', () => {
|
|
2917
|
+
req.destroy(new Error(`Timed out after ${timeout_ms}ms`));
|
|
2918
|
+
});
|
|
2919
|
+
req.on('error', (err) => {
|
|
2920
|
+
resolve({
|
|
2921
|
+
ok: false,
|
|
2922
|
+
error: err.message,
|
|
2923
|
+
elapsed_ms: Date.now() - started,
|
|
2924
|
+
url,
|
|
2925
|
+
reachability_scope: _localUrlScope(parsed),
|
|
2926
|
+
user_browser_reachability: _localUrlScope(parsed) === 'wall-e-host-loopback' ? 'not_proven' : 'not_checked',
|
|
2927
|
+
});
|
|
2928
|
+
});
|
|
2929
|
+
req.end();
|
|
2930
|
+
});
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
function _localUrlScope(parsed) {
|
|
2934
|
+
const hostname = String(parsed?.hostname || '').toLowerCase();
|
|
2935
|
+
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1'
|
|
2936
|
+
? 'wall-e-host-loopback'
|
|
2937
|
+
: 'network';
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
const STATIC_SERVER_SCRIPT = `
|
|
2941
|
+
const http = require('http');
|
|
2942
|
+
const fs = require('fs');
|
|
2943
|
+
const path = require('path');
|
|
2944
|
+
const root = path.resolve(process.argv[1]);
|
|
2945
|
+
const port = Number(process.argv[2] || 0);
|
|
2946
|
+
const mime = {
|
|
2947
|
+
'.html': 'text/html; charset=utf-8',
|
|
2948
|
+
'.css': 'text/css; charset=utf-8',
|
|
2949
|
+
'.js': 'application/javascript; charset=utf-8',
|
|
2950
|
+
'.json': 'application/json; charset=utf-8',
|
|
2951
|
+
'.png': 'image/png',
|
|
2952
|
+
'.jpg': 'image/jpeg',
|
|
2953
|
+
'.jpeg': 'image/jpeg',
|
|
2954
|
+
'.gif': 'image/gif',
|
|
2955
|
+
'.svg': 'image/svg+xml',
|
|
2956
|
+
};
|
|
2957
|
+
function send(res, status, body) {
|
|
2958
|
+
res.writeHead(status, { 'content-type': 'text/plain; charset=utf-8' });
|
|
2959
|
+
res.end(body);
|
|
2960
|
+
}
|
|
2961
|
+
const server = http.createServer((req, res) => {
|
|
2962
|
+
let pathname = '/';
|
|
2963
|
+
try { pathname = decodeURIComponent(new URL(req.url, 'http://localhost').pathname); }
|
|
2964
|
+
catch { return send(res, 400, 'bad request'); }
|
|
2965
|
+
let file = path.normalize(path.join(root, pathname));
|
|
2966
|
+
if (file !== root && !file.startsWith(root + path.sep)) return send(res, 403, 'forbidden');
|
|
2967
|
+
try {
|
|
2968
|
+
const stat = fs.existsSync(file) ? fs.statSync(file) : null;
|
|
2969
|
+
if (stat && stat.isDirectory()) file = path.join(file, 'index.html');
|
|
2970
|
+
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) return send(res, 404, 'not found');
|
|
2971
|
+
res.writeHead(200, { 'content-type': mime[path.extname(file).toLowerCase()] || 'application/octet-stream' });
|
|
2972
|
+
fs.createReadStream(file).pipe(res);
|
|
2973
|
+
} catch (err) {
|
|
2974
|
+
send(res, 500, err.message);
|
|
2975
|
+
}
|
|
2976
|
+
});
|
|
2977
|
+
server.listen(port, '127.0.0.1', () => {
|
|
2978
|
+
const address = server.address();
|
|
2979
|
+
process.stdout.write(JSON.stringify({ port: address.port }) + '\\n');
|
|
2980
|
+
});
|
|
2981
|
+
`;
|
|
2982
|
+
|
|
2983
|
+
async function startStaticServer({ directory, port = 0, route = '/index.html', timeout_ms = 5000, projectRoot } = {}) {
|
|
2984
|
+
let root;
|
|
2985
|
+
try {
|
|
2986
|
+
root = projectRoot
|
|
2987
|
+
? resolveToolPath(directory || '.', projectRoot)
|
|
2988
|
+
: path.resolve(expandUserPath(directory || process.cwd()));
|
|
2989
|
+
} catch (err) {
|
|
2990
|
+
return { ok: false, error: err.message };
|
|
2991
|
+
}
|
|
2992
|
+
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
|
|
2993
|
+
return { ok: false, error: `directory is not a directory: ${root}` };
|
|
2994
|
+
}
|
|
2995
|
+
const stdoutLog = path.join(os.tmpdir(), `walle-static-${Date.now()}-${Math.random().toString(36).slice(2)}.out.log`);
|
|
2996
|
+
const stderrLog = stdoutLog.replace(/\.out\.log$/, '.err.log');
|
|
2997
|
+
fs.writeFileSync(stdoutLog, '');
|
|
2998
|
+
fs.writeFileSync(stderrLog, '');
|
|
2999
|
+
|
|
3000
|
+
const child = require('child_process').spawn(process.execPath, ['-e', STATIC_SERVER_SCRIPT, root, String(port || 0)], {
|
|
3001
|
+
cwd: root,
|
|
3002
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
3003
|
+
detached: false,
|
|
3004
|
+
env: hostAutomationEnv(),
|
|
3005
|
+
});
|
|
3006
|
+
child.stdout.on('data', (chunk) => fs.appendFileSync(stdoutLog, chunk));
|
|
3007
|
+
child.stderr.on('data', (chunk) => fs.appendFileSync(stderrLog, chunk));
|
|
3008
|
+
|
|
3009
|
+
const listenInfo = await new Promise((resolve) => {
|
|
3010
|
+
let buffer = '';
|
|
3011
|
+
const timer = setTimeout(() => resolve({ error: `Static server did not start within ${timeout_ms}ms` }), timeout_ms);
|
|
3012
|
+
child.stdout.on('data', (chunk) => {
|
|
3013
|
+
buffer += chunk.toString('utf8');
|
|
3014
|
+
const line = buffer.split(/\r?\n/).find(Boolean);
|
|
3015
|
+
if (!line) return;
|
|
3016
|
+
try {
|
|
3017
|
+
clearTimeout(timer);
|
|
3018
|
+
resolve(JSON.parse(line));
|
|
3019
|
+
} catch {}
|
|
3020
|
+
});
|
|
3021
|
+
child.once('exit', (code, signal) => {
|
|
3022
|
+
clearTimeout(timer);
|
|
3023
|
+
resolve({ error: `Static server exited before listening: ${code ?? signal}` });
|
|
3024
|
+
});
|
|
3025
|
+
});
|
|
3026
|
+
|
|
3027
|
+
if (!listenInfo || listenInfo.error || !listenInfo.port) {
|
|
3028
|
+
try { child.kill(); } catch {}
|
|
3029
|
+
return { ok: false, error: listenInfo?.error || 'Static server failed to start', stdout_log: stdoutLog, stderr_log: stderrLog };
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
const resourceId = `static-${child.pid}-${listenInfo.port}`;
|
|
3033
|
+
const url = `http://127.0.0.1:${listenInfo.port}${route && route.startsWith('/') ? route : `/${route || ''}`}`;
|
|
3034
|
+
const health = await checkUrl({ url, timeout_ms: Math.min(timeout_ms, 5000) });
|
|
3035
|
+
STATIC_SERVER_PROCESSES.set(resourceId, { child, root, port: listenInfo.port, url, stdoutLog, stderrLog });
|
|
3036
|
+
if (child.unref) child.unref();
|
|
3037
|
+
return {
|
|
3038
|
+
ok: health.ok,
|
|
3039
|
+
resource_id: resourceId,
|
|
3040
|
+
pid: child.pid,
|
|
3041
|
+
port: listenInfo.port,
|
|
3042
|
+
root,
|
|
3043
|
+
url,
|
|
3044
|
+
health,
|
|
3045
|
+
reachability_scope: 'wall-e-host-loopback',
|
|
3046
|
+
user_browser_reachability: 'not_proven',
|
|
3047
|
+
note: 'Managed static servers bind to 127.0.0.1. This verifies Wall-E host loopback only; phone or remote-browser access needs CTM remote/tunnel context.',
|
|
3048
|
+
stdout_log: stdoutLog,
|
|
3049
|
+
stderr_log: stderrLog,
|
|
3050
|
+
};
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
async function stopStaticServer({ resource_id } = {}) {
|
|
3054
|
+
const resource = STATIC_SERVER_PROCESSES.get(resource_id);
|
|
3055
|
+
if (!resource) return { ok: false, error: `No static server resource found for ${resource_id || '(missing id)'}` };
|
|
3056
|
+
STATIC_SERVER_PROCESSES.delete(resource_id);
|
|
3057
|
+
try {
|
|
3058
|
+
resource.child.kill();
|
|
3059
|
+
} catch (err) {
|
|
3060
|
+
return { ok: false, error: err.message, resource_id };
|
|
3061
|
+
}
|
|
3062
|
+
return { ok: true, resource_id, pid: resource.child.pid, url: resource.url };
|
|
3063
|
+
}
|
|
3064
|
+
|
|
3065
|
+
// ── Background shell processes ──
|
|
3066
|
+
// Dev servers, watchers, and long builds: run_shell {background:true}
|
|
3067
|
+
// detaches the command into a session-scoped registry; the agent polls with
|
|
3068
|
+
// bg_output and stops with bg_kill. Non-persistent processes are killed at
|
|
3069
|
+
// the end of the run (cleanupBackgroundProcesses).
|
|
3070
|
+
|
|
3071
|
+
const BACKGROUND_PROCESSES = new Map(); // resourceId -> record
|
|
3072
|
+
const BG_OUTPUT_MAX_BYTES = 512 * 1024;
|
|
3073
|
+
|
|
3074
|
+
async function runShellBackground({ command, cwd, sessionId = '', persist = false } = {}) {
|
|
3075
|
+
if (!command || typeof command !== 'string') return { ok: false, error: 'command is required' };
|
|
3076
|
+
const { analyzeShellCommand, initParser } = require('./shell-analyzer');
|
|
3077
|
+
await initParser();
|
|
3078
|
+
const analysis = await analyzeShellCommand(command, cwd || process.cwd());
|
|
3079
|
+
if (!analysis.allowed) return { ok: false, error: analysis.reason };
|
|
3080
|
+
|
|
3081
|
+
const logFile = path.join(os.tmpdir(), `walle-bg-${Date.now()}-${Math.random().toString(36).slice(2)}.log`);
|
|
3082
|
+
const logFd = fs.openSync(logFile, 'a');
|
|
3083
|
+
let child;
|
|
3084
|
+
try {
|
|
3085
|
+
child = require('child_process').spawn('/bin/bash', ['-c', command], {
|
|
3086
|
+
cwd: cwd || undefined,
|
|
3087
|
+
env: { ...process.env, HOME },
|
|
3088
|
+
stdio: ['ignore', logFd, logFd],
|
|
3089
|
+
detached: true, // own process group so bg_kill can stop child trees
|
|
3090
|
+
});
|
|
3091
|
+
} catch (err) {
|
|
3092
|
+
fs.closeSync(logFd);
|
|
3093
|
+
return { ok: false, error: err.message };
|
|
3094
|
+
}
|
|
3095
|
+
fs.closeSync(logFd);
|
|
3096
|
+
|
|
3097
|
+
const resourceId = `bg-${child.pid}`;
|
|
3098
|
+
const record = {
|
|
3099
|
+
resourceId,
|
|
3100
|
+
pid: child.pid,
|
|
3101
|
+
command,
|
|
3102
|
+
cwd: cwd || process.cwd(),
|
|
3103
|
+
sessionId,
|
|
3104
|
+
persist: Boolean(persist),
|
|
3105
|
+
logFile,
|
|
3106
|
+
startedAt: Date.now(),
|
|
3107
|
+
exited: false,
|
|
3108
|
+
exitCode: null,
|
|
3109
|
+
exitSignal: null,
|
|
3110
|
+
child,
|
|
3111
|
+
};
|
|
3112
|
+
child.once('exit', (code, signal) => {
|
|
3113
|
+
record.exited = true;
|
|
3114
|
+
record.exitCode = code;
|
|
3115
|
+
record.exitSignal = signal;
|
|
3116
|
+
record.endedAt = Date.now();
|
|
3117
|
+
});
|
|
3118
|
+
child.unref();
|
|
3119
|
+
BACKGROUND_PROCESSES.set(resourceId, record);
|
|
3120
|
+
return {
|
|
3121
|
+
ok: true,
|
|
3122
|
+
background: true,
|
|
3123
|
+
resource_id: resourceId,
|
|
3124
|
+
pid: child.pid,
|
|
3125
|
+
log_file: logFile,
|
|
3126
|
+
note: 'Command is running in the background. Poll with bg_output {resource_id}; stop with bg_kill {resource_id}. Non-persistent background processes are stopped when the run ends.',
|
|
3127
|
+
};
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
function _bgStatus(record) {
|
|
3131
|
+
if (!record.exited && !_pidIsAlive(record.pid)) {
|
|
3132
|
+
record.exited = true; // exited while we weren't looking (e.g. after restart)
|
|
3133
|
+
}
|
|
3134
|
+
return record.exited ? 'exited' : 'running';
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3137
|
+
async function bgOutput({ resource_id, tail_lines = 100 } = {}) {
|
|
3138
|
+
const record = BACKGROUND_PROCESSES.get(resource_id);
|
|
3139
|
+
if (!record) return { ok: false, error: `No background process found for ${resource_id || '(missing id)'}` };
|
|
3140
|
+
let output = '';
|
|
3141
|
+
try {
|
|
3142
|
+
const stat = fs.statSync(record.logFile);
|
|
3143
|
+
const start = Math.max(0, stat.size - BG_OUTPUT_MAX_BYTES);
|
|
3144
|
+
const fd = fs.openSync(record.logFile, 'r');
|
|
3145
|
+
try {
|
|
3146
|
+
const buf = Buffer.alloc(stat.size - start);
|
|
3147
|
+
fs.readSync(fd, buf, 0, buf.length, start);
|
|
3148
|
+
output = buf.toString('utf8');
|
|
3149
|
+
} finally {
|
|
3150
|
+
fs.closeSync(fd);
|
|
3151
|
+
}
|
|
3152
|
+
} catch (err) {
|
|
3153
|
+
output = `(could not read log: ${err.message})`;
|
|
3154
|
+
}
|
|
3155
|
+
const lines = output.split('\n');
|
|
3156
|
+
const maxLines = Math.max(1, Math.min(2000, tail_lines || 100));
|
|
3157
|
+
const tail = lines.slice(-maxLines).join('\n');
|
|
3158
|
+
const status = _bgStatus(record);
|
|
3159
|
+
return {
|
|
3160
|
+
ok: true,
|
|
3161
|
+
resource_id,
|
|
3162
|
+
status,
|
|
3163
|
+
exit_code: record.exitCode,
|
|
3164
|
+
exit_signal: record.exitSignal,
|
|
3165
|
+
uptime_ms: (record.endedAt || Date.now()) - record.startedAt,
|
|
3166
|
+
output: tail,
|
|
3167
|
+
truncated: lines.length > maxLines,
|
|
3168
|
+
};
|
|
3169
|
+
}
|
|
3170
|
+
|
|
3171
|
+
async function bgKill({ resource_id } = {}) {
|
|
3172
|
+
const record = BACKGROUND_PROCESSES.get(resource_id);
|
|
3173
|
+
if (!record) return { ok: false, error: `No background process found for ${resource_id || '(missing id)'}` };
|
|
3174
|
+
if (_bgStatus(record) === 'exited') {
|
|
3175
|
+
return { ok: true, resource_id, status: 'exited', exit_code: record.exitCode, note: 'Process had already exited.' };
|
|
3176
|
+
}
|
|
3177
|
+
try {
|
|
3178
|
+
process.kill(-record.pid, 'SIGTERM'); // whole process group
|
|
3179
|
+
} catch {
|
|
3180
|
+
try { record.child.kill('SIGTERM'); } catch {}
|
|
3181
|
+
}
|
|
3182
|
+
return { ok: true, resource_id, status: 'terminating', pid: record.pid };
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
/**
|
|
3186
|
+
* Kill all non-persistent background processes for a session (or all
|
|
3187
|
+
* sessions when sessionId is omitted). Returns what was stopped/leaked so
|
|
3188
|
+
* run summaries can report it.
|
|
3189
|
+
*/
|
|
3190
|
+
function cleanupBackgroundProcesses({ sessionId = null } = {}) {
|
|
3191
|
+
const stopped = [];
|
|
3192
|
+
const persisted = [];
|
|
3193
|
+
for (const [resourceId, record] of BACKGROUND_PROCESSES) {
|
|
3194
|
+
if (sessionId && record.sessionId !== sessionId) continue;
|
|
3195
|
+
if (record.persist) {
|
|
3196
|
+
if (_bgStatus(record) === 'running') persisted.push({ resource_id: resourceId, pid: record.pid, command: record.command });
|
|
3197
|
+
continue;
|
|
3198
|
+
}
|
|
3199
|
+
if (_bgStatus(record) === 'running') {
|
|
3200
|
+
try {
|
|
3201
|
+
process.kill(-record.pid, 'SIGTERM');
|
|
3202
|
+
} catch {
|
|
3203
|
+
try { record.child.kill('SIGTERM'); } catch {}
|
|
3204
|
+
}
|
|
3205
|
+
stopped.push({ resource_id: resourceId, pid: record.pid, command: record.command });
|
|
3206
|
+
}
|
|
3207
|
+
BACKGROUND_PROCESSES.delete(resourceId);
|
|
3208
|
+
}
|
|
3209
|
+
return { stopped, persisted };
|
|
3210
|
+
}
|
|
3211
|
+
|
|
2033
3212
|
async function clipboardRead() {
|
|
2034
3213
|
const { stdout } = await execFileAsync('pbpaste', [], { timeout: 5000 });
|
|
2035
3214
|
return { content: stdout.slice(0, 50000) };
|
|
@@ -2937,7 +4116,8 @@ async function webFetch(url, { method = 'GET', extract_text = true, max_bytes =
|
|
|
2937
4116
|
args.push(url);
|
|
2938
4117
|
const { stdout, stderr } = await execFileAsync('curl', args, { timeout: timeout_ms + 2000, maxBuffer: max_bytes * 2 });
|
|
2939
4118
|
if (stderr && !stdout) throw new Error(stderr.slice(0, 500));
|
|
2940
|
-
|
|
4119
|
+
const rawContent = stdout.slice(0, max_bytes);
|
|
4120
|
+
let content = rawContent;
|
|
2941
4121
|
// Strip HTML tags if requested to get readable text
|
|
2942
4122
|
if (extract_text && content.includes('<')) {
|
|
2943
4123
|
content = content
|
|
@@ -2952,7 +4132,72 @@ async function webFetch(url, { method = 'GET', extract_text = true, max_bytes =
|
|
|
2952
4132
|
.trim()
|
|
2953
4133
|
.slice(0, max_bytes);
|
|
2954
4134
|
}
|
|
2955
|
-
|
|
4135
|
+
const result = { url, content, length: content.length, truncated: stdout.length > max_bytes };
|
|
4136
|
+
if (extract_text && rawContent.includes('<') && content.length <= 40 && /<script[\s>]/i.test(rawContent)) {
|
|
4137
|
+
result.fetch_status = 'needs_browser_rendering';
|
|
4138
|
+
result.warning = 'HTTP fetch succeeded, but extracted text is too small and the page appears to require JavaScript/browser rendering.';
|
|
4139
|
+
}
|
|
4140
|
+
return result;
|
|
4141
|
+
}
|
|
4142
|
+
|
|
4143
|
+
// ── Web search (DuckDuckGo HTML endpoint — no API key) ──
|
|
4144
|
+
|
|
4145
|
+
function parseDuckDuckGoResults(html, maxResults = 8) {
|
|
4146
|
+
const results = [];
|
|
4147
|
+
const linkRe = /<a[^>]*class="[^"]*result__a[^"]*"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
4148
|
+
const snippetRe = /<a[^>]*class="[^"]*result__snippet[^"]*"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
4149
|
+
const snippets = [];
|
|
4150
|
+
let match;
|
|
4151
|
+
while ((match = snippetRe.exec(html))) snippets.push(stripHtmlText(match[1]));
|
|
4152
|
+
let index = 0;
|
|
4153
|
+
while ((match = linkRe.exec(html)) && results.length < maxResults) {
|
|
4154
|
+
let url = match[1];
|
|
4155
|
+
// DDG wraps targets in a redirect: //duckduckgo.com/l/?uddg=<encoded>
|
|
4156
|
+
const uddg = url.match(/[?&]uddg=([^&]+)/);
|
|
4157
|
+
if (uddg) {
|
|
4158
|
+
try { url = decodeURIComponent(uddg[1]); } catch {}
|
|
4159
|
+
}
|
|
4160
|
+
if (!/^https?:\/\//i.test(url)) { index += 1; continue; }
|
|
4161
|
+
results.push({
|
|
4162
|
+
title: stripHtmlText(match[2]),
|
|
4163
|
+
url,
|
|
4164
|
+
snippet: snippets[index] || '',
|
|
4165
|
+
});
|
|
4166
|
+
index += 1;
|
|
4167
|
+
}
|
|
4168
|
+
return results;
|
|
4169
|
+
}
|
|
4170
|
+
|
|
4171
|
+
function stripHtmlText(html) {
|
|
4172
|
+
return String(html || '')
|
|
4173
|
+
.replace(/<[^>]+>/g, ' ')
|
|
4174
|
+
.replace(/ /g, ' ')
|
|
4175
|
+
.replace(/&/g, '&')
|
|
4176
|
+
.replace(/</g, '<')
|
|
4177
|
+
.replace(/>/g, '>')
|
|
4178
|
+
.replace(/'|'/g, "'")
|
|
4179
|
+
.replace(/"/g, '"')
|
|
4180
|
+
.replace(/\s{2,}/g, ' ')
|
|
4181
|
+
.trim();
|
|
4182
|
+
}
|
|
4183
|
+
|
|
4184
|
+
async function webSearch({ query, max_results = 8, timeout_ms = 15000 } = {}) {
|
|
4185
|
+
if (!query || !String(query).trim()) return { ok: false, error: 'query is required' };
|
|
4186
|
+
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(String(query).trim())}`;
|
|
4187
|
+
const { stdout } = await execFileAsync('curl', [
|
|
4188
|
+
'-sS', '-L', '--max-time', String(Math.round(timeout_ms / 1000)),
|
|
4189
|
+
'-A', 'Mozilla/5.0 (compatible; WALL-E/1.0)',
|
|
4190
|
+
url,
|
|
4191
|
+
], { timeout: timeout_ms + 2000, maxBuffer: 4 * 1024 * 1024 });
|
|
4192
|
+
const results = parseDuckDuckGoResults(stdout, Math.max(1, Math.min(20, max_results)));
|
|
4193
|
+
return {
|
|
4194
|
+
ok: true,
|
|
4195
|
+
query,
|
|
4196
|
+
results,
|
|
4197
|
+
note: results.length === 0
|
|
4198
|
+
? 'No results parsed. The query may have no hits, or the search endpoint may have changed.'
|
|
4199
|
+
: 'Use web_fetch on a result URL to read the page.',
|
|
4200
|
+
};
|
|
2956
4201
|
}
|
|
2957
4202
|
|
|
2958
4203
|
// ── Tool definitions for Claude API ──
|
|
@@ -2967,10 +4212,34 @@ const LOCAL_TOOL_DEFINITIONS = [
|
|
|
2967
4212
|
command: { type: 'string', description: 'Complete shell command string to run, e.g. `npm test`, `git status --short`, or `cd app && npm run build`. Include any pipes/redirects/heredocs here.' },
|
|
2968
4213
|
args: { type: 'array', items: { type: 'string' }, description: 'Optional legacy argument array for simple commands only. Do not put `-c` scripts, pipes, redirects, heredocs, `<`, `>`, or `|` here.' },
|
|
2969
4214
|
timeout_ms: { type: 'number', description: 'Timeout in ms (default 15000)' },
|
|
4215
|
+
background: { type: 'boolean', description: 'Run detached in the background (dev servers, watchers, long builds). Returns a resource_id immediately; poll with bg_output, stop with bg_kill. Never use `&` for backgrounding — use this flag.' },
|
|
2970
4216
|
},
|
|
2971
4217
|
required: ['command'],
|
|
2972
4218
|
},
|
|
2973
4219
|
},
|
|
4220
|
+
{
|
|
4221
|
+
name: 'bg_output',
|
|
4222
|
+
description: 'Read the latest output of a background process started with run_shell {background:true}. Returns status (running/exited), exit code, and the log tail.',
|
|
4223
|
+
input_schema: {
|
|
4224
|
+
type: 'object',
|
|
4225
|
+
properties: {
|
|
4226
|
+
resource_id: { type: 'string', description: 'resource_id returned by run_shell {background:true}' },
|
|
4227
|
+
tail_lines: { type: 'number', description: 'How many trailing log lines to return (default 100, max 2000)' },
|
|
4228
|
+
},
|
|
4229
|
+
required: ['resource_id'],
|
|
4230
|
+
},
|
|
4231
|
+
},
|
|
4232
|
+
{
|
|
4233
|
+
name: 'bg_kill',
|
|
4234
|
+
description: 'Stop a background process started with run_shell {background:true}.',
|
|
4235
|
+
input_schema: {
|
|
4236
|
+
type: 'object',
|
|
4237
|
+
properties: {
|
|
4238
|
+
resource_id: { type: 'string', description: 'resource_id returned by run_shell {background:true}' },
|
|
4239
|
+
},
|
|
4240
|
+
required: ['resource_id'],
|
|
4241
|
+
},
|
|
4242
|
+
},
|
|
2974
4243
|
{
|
|
2975
4244
|
name: 'read_file',
|
|
2976
4245
|
description: 'Read a local text file. In coding sessions, files must be inside the current project/cwd; relative paths resolve from that project. Prefer this over `run_shell` with cat/head/tail/sed when the path is known. Supports max_bytes for partial reads.',
|
|
@@ -3022,17 +4291,132 @@ const LOCAL_TOOL_DEFINITIONS = [
|
|
|
3022
4291
|
},
|
|
3023
4292
|
{
|
|
3024
4293
|
name: 'browser_screenshot',
|
|
3025
|
-
description: 'Render a URL (http:// or file://) in headless Chrome and capture a PNG
|
|
4294
|
+
description: 'Render a URL (http:// or file://) in headless Chrome and capture a PNG. Use after building UI to self-critique what you actually built before claiming done. Capture both desktop (1440×900) and mobile (390×844) for any responsive UI. Pass full_page: true to capture EVERYTHING below the fold (galleries, footers, long content) — essential before declaring a multi-section page done. Returns { ok, path, viewport, width, height, full_page? }.',
|
|
3026
4295
|
input_schema: {
|
|
3027
4296
|
type: 'object',
|
|
3028
4297
|
properties: {
|
|
3029
4298
|
url: { type: 'string', description: 'URL to capture — supports file:// for local files (e.g., file:///path/to/index.html) or http://localhost:port for a dev server.' },
|
|
3030
|
-
viewport: { type: 'string', enum: ['desktop', 'mobile', 'tablet'], description: 'Viewport preset: desktop (1440×900), mobile (390×844), tablet (1024×768). Default: desktop.' },
|
|
3031
|
-
output_path: { type: 'string', description: 'Save path. Default: ~/<tmp>/walle-shot-<viewport
|
|
4299
|
+
viewport: { type: 'string', enum: ['desktop', 'mobile', 'tablet'], description: 'Viewport preset: desktop (1440×900), mobile (390×844), tablet (1024×768). Default: desktop. The viewport drives responsive CSS even when full_page is true.' },
|
|
4300
|
+
output_path: { type: 'string', description: 'Save path. Default: ~/<tmp>/walle-shot-<viewport>[-fullpage]-<timestamp>.png' },
|
|
4301
|
+
full_page: { type: 'boolean', description: 'When true, capture the entire scrollable document — not just the viewport. Slower (~3-5s, drives Chrome via CDP) but the only way to see content below the fold. Default: false.' },
|
|
3032
4302
|
},
|
|
3033
4303
|
required: ['url'],
|
|
3034
4304
|
},
|
|
3035
4305
|
},
|
|
4306
|
+
{
|
|
4307
|
+
name: 'browser_smoke_test',
|
|
4308
|
+
description: 'Render a URL in headless Chrome through CDP, capture JavaScript runtime exceptions, console errors, failed requests, and safely click interactive elements such as [onclick], buttons, role=button, and hash links. Use this after frontend/UI changes before claiming completion; it catches broken HTML-to-JS handlers that screenshots can miss. Returns { ok, failures, runtimeExceptions, consoleErrors, failedRequests, clicked }.',
|
|
4309
|
+
input_schema: {
|
|
4310
|
+
type: 'object',
|
|
4311
|
+
properties: {
|
|
4312
|
+
url: { type: 'string', description: 'URL to validate — supports file:// local HTML files or http://localhost URLs from start_static_server.' },
|
|
4313
|
+
viewport: { type: 'string', enum: ['desktop', 'mobile', 'tablet'], description: 'Viewport preset. Default: desktop.' },
|
|
4314
|
+
click_selectors: { type: 'array', items: { type: 'string' }, description: 'Optional selectors to click. Defaults to [onclick], button, [role=button], and hash links.' },
|
|
4315
|
+
max_clicks: { type: 'number', description: 'Maximum interactive elements to click. Default: 20.' },
|
|
4316
|
+
settle_ms: { type: 'number', description: 'Milliseconds to wait after load/clicks. Default: 750.' },
|
|
4317
|
+
timeout_ms: { type: 'number', description: 'Overall timeout. Default: 45000.' },
|
|
4318
|
+
},
|
|
4319
|
+
required: ['url'],
|
|
4320
|
+
},
|
|
4321
|
+
},
|
|
4322
|
+
{
|
|
4323
|
+
name: 'check_url',
|
|
4324
|
+
description: 'Fetch an http:// or https:// URL and report whether it returns a 2xx/3xx response. Use this before claiming a local dev/static server is actually reachable. For localhost/127.0.0.1, this verifies Wall-E host loopback only; do not claim phone or remote-browser access without CTM remote/tunnel evidence.',
|
|
4325
|
+
input_schema: {
|
|
4326
|
+
type: 'object',
|
|
4327
|
+
properties: {
|
|
4328
|
+
url: { type: 'string', description: 'URL to fetch, for example http://127.0.0.1:9090/index.html.' },
|
|
4329
|
+
timeout_ms: { type: 'number', description: 'Timeout in ms (default 5000).' },
|
|
4330
|
+
},
|
|
4331
|
+
required: ['url'],
|
|
4332
|
+
},
|
|
4333
|
+
},
|
|
4334
|
+
{
|
|
4335
|
+
name: 'start_static_server',
|
|
4336
|
+
description: 'Start a managed local static file server for a directory, wait for it to answer HTTP 200/3xx from Wall-E host loopback, and return the verified local URL plus a resource_id. Prefer this over run_shell background commands such as `python3 -m http.server &`. Do not present the returned localhost/127.0.0.1 URL as phone or remote-browser reachable unless CTM remote/tunnel evidence confirms it.',
|
|
4337
|
+
input_schema: {
|
|
4338
|
+
type: 'object',
|
|
4339
|
+
properties: {
|
|
4340
|
+
directory: { type: 'string', description: 'Directory to serve. Defaults to current project directory.' },
|
|
4341
|
+
port: { type: 'number', description: 'Port to bind. Use 0 or omit for an available port.' },
|
|
4342
|
+
route: { type: 'string', description: 'Route to health-check after start. Default: /index.html.' },
|
|
4343
|
+
timeout_ms: { type: 'number', description: 'Startup timeout in ms (default 5000).' },
|
|
4344
|
+
},
|
|
4345
|
+
},
|
|
4346
|
+
},
|
|
4347
|
+
{
|
|
4348
|
+
name: 'stop_static_server',
|
|
4349
|
+
description: 'Stop a static server started by start_static_server using its resource_id.',
|
|
4350
|
+
input_schema: {
|
|
4351
|
+
type: 'object',
|
|
4352
|
+
properties: {
|
|
4353
|
+
resource_id: { type: 'string', description: 'resource_id returned by start_static_server.' },
|
|
4354
|
+
},
|
|
4355
|
+
required: ['resource_id'],
|
|
4356
|
+
},
|
|
4357
|
+
},
|
|
4358
|
+
{
|
|
4359
|
+
name: 'pdf_info',
|
|
4360
|
+
description: 'Validate a PDF and return metadata (bytes, sha256, page count when pdfinfo is available). Use before reading or claiming a PDF artifact is valid.',
|
|
4361
|
+
input_schema: {
|
|
4362
|
+
type: 'object',
|
|
4363
|
+
properties: {
|
|
4364
|
+
file_path: { type: 'string', description: 'PDF file path.' },
|
|
4365
|
+
max_bytes: { type: 'number', description: 'Maximum allowed bytes, default 32MB.' },
|
|
4366
|
+
},
|
|
4367
|
+
required: ['file_path'],
|
|
4368
|
+
},
|
|
4369
|
+
},
|
|
4370
|
+
{
|
|
4371
|
+
name: 'pdf_render_pages',
|
|
4372
|
+
description: 'Render a bounded PDF page range to JPEG previews using pdftoppm. Use previews to inspect generated PDFs visually before claiming success.',
|
|
4373
|
+
input_schema: {
|
|
4374
|
+
type: 'object',
|
|
4375
|
+
properties: {
|
|
4376
|
+
file_path: { type: 'string', description: 'PDF file path.' },
|
|
4377
|
+
pages: { type: 'string', description: 'Page range like "1", "1-3", or "2-". Default: "1". Max 20 pages.' },
|
|
4378
|
+
output_dir: { type: 'string', description: 'Preview output directory. Must be under HOME, temp, or the project.' },
|
|
4379
|
+
dpi: { type: 'number', description: 'Render DPI from 72 to 200. Default: 144.' },
|
|
4380
|
+
},
|
|
4381
|
+
required: ['file_path'],
|
|
4382
|
+
},
|
|
4383
|
+
},
|
|
4384
|
+
{
|
|
4385
|
+
name: 'pdf_read_pages',
|
|
4386
|
+
description: 'Extract text from a bounded PDF page range using pdftotext and return metadata. Use for PDF analysis and summaries.',
|
|
4387
|
+
input_schema: {
|
|
4388
|
+
type: 'object',
|
|
4389
|
+
properties: {
|
|
4390
|
+
file_path: { type: 'string', description: 'PDF file path.' },
|
|
4391
|
+
pages: { type: 'string', description: 'Page range like "1", "1-3", or "2-". Default: "1-5". Max 20 pages.' },
|
|
4392
|
+
max_chars: { type: 'number', description: 'Maximum text chars to return, default 20000.' },
|
|
4393
|
+
},
|
|
4394
|
+
required: ['file_path'],
|
|
4395
|
+
},
|
|
4396
|
+
},
|
|
4397
|
+
{
|
|
4398
|
+
name: 'make_pdf',
|
|
4399
|
+
description: 'Generate a PDF from Markdown/HTML through the configured make-pdf renderer, validate the generated file, and optionally render a page preview. Use for PDF creation/export work.',
|
|
4400
|
+
input_schema: {
|
|
4401
|
+
type: 'object',
|
|
4402
|
+
properties: {
|
|
4403
|
+
input_path: { type: 'string', description: 'Markdown or HTML source file.' },
|
|
4404
|
+
output_path: { type: 'string', description: 'Optional PDF output path.' },
|
|
4405
|
+
title: { type: 'string', description: 'Optional title metadata.' },
|
|
4406
|
+
author: { type: 'string', description: 'Optional author metadata.' },
|
|
4407
|
+
date: { type: 'string', description: 'Optional cover/metadata date.' },
|
|
4408
|
+
page_size: { type: 'string', description: 'Optional page size such as letter or a4.' },
|
|
4409
|
+
margins: { type: 'string', description: 'Optional margin value such as 1in.' },
|
|
4410
|
+
cover: { type: 'boolean', description: 'Generate a cover page when supported.' },
|
|
4411
|
+
toc: { type: 'boolean', description: 'Generate a table of contents when supported.' },
|
|
4412
|
+
watermark: { type: 'string', description: 'Optional watermark text.' },
|
|
4413
|
+
allow_network: { type: 'boolean', description: 'Allow external network assets when supported. Default false.' },
|
|
4414
|
+
no_confidential: { type: 'boolean', description: 'Suppress the default confidential footer when supported.' },
|
|
4415
|
+
render_preview: { type: 'boolean', description: 'Render first-page preview after generation. Default true.' },
|
|
4416
|
+
},
|
|
4417
|
+
required: ['input_path'],
|
|
4418
|
+
},
|
|
4419
|
+
},
|
|
3036
4420
|
{
|
|
3037
4421
|
name: 'clipboard_read',
|
|
3038
4422
|
description: 'Read the current system clipboard contents.',
|
|
@@ -3307,6 +4691,18 @@ const LOCAL_TOOL_DEFINITIONS = [
|
|
|
3307
4691
|
required: ['url'],
|
|
3308
4692
|
},
|
|
3309
4693
|
},
|
|
4694
|
+
{
|
|
4695
|
+
name: 'web_search',
|
|
4696
|
+
description: 'Search the public web (DuckDuckGo) and return result titles, URLs, and snippets. Use to discover documentation or error-message references when you do not know the URL; then read the page with web_fetch.',
|
|
4697
|
+
input_schema: {
|
|
4698
|
+
type: 'object',
|
|
4699
|
+
properties: {
|
|
4700
|
+
query: { type: 'string', description: 'Search query' },
|
|
4701
|
+
max_results: { type: 'number', description: 'Max results to return (default 8, max 20)' },
|
|
4702
|
+
},
|
|
4703
|
+
required: ['query'],
|
|
4704
|
+
},
|
|
4705
|
+
},
|
|
3310
4706
|
{
|
|
3311
4707
|
name: 'glean_search',
|
|
3312
4708
|
description: 'Search company documents, wikis, Google Docs, Confluence, Jira, and other connected sources via Glean. Use when the user asks to "search docs", "find a document", "look up X in our wiki", "search Glean", or needs information from internal company knowledge bases.',
|
|
@@ -3412,12 +4808,22 @@ async function executeLocalTool(name, input) {
|
|
|
3412
4808
|
}
|
|
3413
4809
|
|
|
3414
4810
|
switch (name) {
|
|
3415
|
-
case 'run_shell': return runShell({ command: input.command, args: input.args, timeout_ms: input.timeout_ms, cwd: input.cwd });
|
|
4811
|
+
case 'run_shell': return runShell({ command: input.command, args: input.args, timeout_ms: input.timeout_ms, cwd: input.cwd, background: input.background, sessionId: input.sessionId || input.session_id, persist: input.persist });
|
|
4812
|
+
case 'bg_output': return bgOutput(input);
|
|
4813
|
+
case 'bg_kill': return bgKill(input);
|
|
3416
4814
|
case 'read_file': return readFile(input.file_path, { max_bytes: input.max_bytes, offset: input.offset, limit: input.limit, sessionId: input.sessionId, projectRoot: input.projectRoot });
|
|
3417
4815
|
case 'write_file': return writeFile(input.file_path, input.content, { sessionId: input.sessionId, projectRoot: input.projectRoot });
|
|
3418
4816
|
case 'search_files': return searchFiles(input.query, input);
|
|
3419
4817
|
case 'screenshot': return takeScreenshot(input);
|
|
3420
4818
|
case 'browser_screenshot': return browserScreenshot(input);
|
|
4819
|
+
case 'browser_smoke_test': return browserSmokeTest(input);
|
|
4820
|
+
case 'check_url': return checkUrl(input);
|
|
4821
|
+
case 'start_static_server': return startStaticServer(input);
|
|
4822
|
+
case 'stop_static_server': return stopStaticServer(input);
|
|
4823
|
+
case 'pdf_info': return pdfInfo(input);
|
|
4824
|
+
case 'pdf_render_pages': return pdfRenderPages(input);
|
|
4825
|
+
case 'pdf_read_pages': return pdfReadPages(input);
|
|
4826
|
+
case 'make_pdf': return makePdf(input);
|
|
3421
4827
|
case 'clipboard_read': return clipboardRead();
|
|
3422
4828
|
case 'clipboard_write': return clipboardWrite(input.text);
|
|
3423
4829
|
case 'open_url': return openUrl(input.url);
|
|
@@ -3439,6 +4845,7 @@ async function executeLocalTool(name, input) {
|
|
|
3439
4845
|
case 'drive_read': return driveRead(input);
|
|
3440
4846
|
case 'system_info': return getSystemInfo();
|
|
3441
4847
|
case 'web_fetch': return webFetch(input.url, input);
|
|
4848
|
+
case 'web_search': return webSearch(input);
|
|
3442
4849
|
case 'glean_search': return gleanSearch(input);
|
|
3443
4850
|
case 'glean_people': return gleanPeople(input);
|
|
3444
4851
|
case 'glean_chat': return gleanChat(input);
|
|
@@ -3918,9 +5325,14 @@ module.exports = {
|
|
|
3918
5325
|
executeLocalTool,
|
|
3919
5326
|
LOCAL_TOOL_DEFINITIONS,
|
|
3920
5327
|
// Individual exports for testing
|
|
3921
|
-
runShell,
|
|
5328
|
+
runShell, runShellBackground, bgOutput, bgKill, cleanupBackgroundProcesses,
|
|
5329
|
+
webSearch, parseDuckDuckGoResults,
|
|
5330
|
+
readFile, writeFile, searchFiles,
|
|
3922
5331
|
runAppleScript, sendNotification, takeScreenshot,
|
|
3923
|
-
browserScreenshot, findChromeExecutable, BROWSER_VIEWPORTS, CHROME_CANDIDATES,
|
|
5332
|
+
browserScreenshot, browserSmokeTest, findChromeExecutable, BROWSER_VIEWPORTS, CHROME_CANDIDATES,
|
|
5333
|
+
checkUrl, startStaticServer, stopStaticServer,
|
|
5334
|
+
pdfInfo, pdfRenderPages, pdfReadPages, makePdf,
|
|
5335
|
+
parsePdfPageRange, resolvePdfOutputDir, findMakePdfBinary, makePdfBinaryCandidates,
|
|
3924
5336
|
clipboardRead, clipboardWrite, openUrl, openApp,
|
|
3925
5337
|
getCalendarEvents, listCalendars, createCalendarEvent, createReminder,
|
|
3926
5338
|
getMailMessages, readMailMessage, searchMail, mailAttachments, downloadMailAttachment, sendMail, sendMailReply,
|