ashlr-plugin 1.36.4
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/.agents/plugins/marketplace.json +20 -0
- package/.claude-plugin/marketplace.json +39 -0
- package/.claude-plugin/plugin.json +36 -0
- package/.codex/agents/README.md +9 -0
- package/.codex/agents/ashlr-explorer.md +36 -0
- package/.codex/agents/ashlr-worker.md +38 -0
- package/.codex-plugin/plugin.json +45 -0
- package/.mcp.json +14 -0
- package/LICENSE +21 -0
- package/README.md +311 -0
- package/agents/ashlr-code.md +69 -0
- package/agents/ashlr-explore.md +44 -0
- package/agents/ashlr-plan.md +54 -0
- package/bench/fixtures/notebook-large.ipynb +660 -0
- package/bench/fixtures/notebook-small.ipynb +39 -0
- package/bench/fixtures/tasklist-closed.json +62 -0
- package/bench/fixtures/tasklist-large-mixed.json +27 -0
- package/bench/fixtures/tasklist-sprint-open.json +79 -0
- package/bench/fixtures/websearch-bun-runtime.json +32 -0
- package/bench/fixtures/websearch-react-hooks-patterns.json +32 -0
- package/bench/fixtures/websearch-typescript-generics.json +38 -0
- package/bench/refs/node-sdk/.refrev +1 -0
- package/bench/refs/node-sdk/README.md +17 -0
- package/bench/refs/node-sdk/src/agent/agent.ts +163 -0
- package/bench/refs/node-sdk/src/agent/create-agent-ui-stream.ts +83 -0
- package/bench/refs/node-sdk/src/agent/tool-loop-agent.ts +238 -0
- package/bench/refs/node-sdk/src/embed/embed-many.ts +409 -0
- package/bench/refs/node-sdk/src/embed/embed-result.ts +50 -0
- package/bench/refs/node-sdk/src/embed/embed.ts +257 -0
- package/bench/refs/node-sdk/src/error/invalid-argument-error.ts +34 -0
- package/bench/refs/node-sdk/src/error/invalid-tool-input-error.ts +33 -0
- package/bench/refs/node-sdk/src/error/no-object-generated-error.ts +70 -0
- package/bench/refs/node-sdk/src/generate-image/generate-image-result.ts +42 -0
- package/bench/refs/node-sdk/src/generate-image/generate-image.ts +362 -0
- package/bench/refs/node-sdk/src/generate-object/generate-object-result.ts +67 -0
- package/bench/refs/node-sdk/src/generate-object/generate-object.ts +501 -0
- package/bench/refs/node-sdk/src/generate-object/inject-json-instruction.ts +30 -0
- package/bench/refs/node-sdk/src/generate-object/output-strategy.ts +415 -0
- package/bench/refs/node-sdk/src/generate-object/repair-text.ts +12 -0
- package/bench/refs/node-sdk/src/generate-object/stream-object.ts +960 -0
- package/bench/refs/node-sdk/src/generate-speech/generate-speech.ts +191 -0
- package/bench/refs/node-sdk/src/generate-text/generate-text.ts +1484 -0
- package/bench/refs/node-sdk/src/generate-text/stream-text.ts +2708 -0
- package/bench/refs/node-sdk/src/generate-text/tool-call.ts +59 -0
- package/bench/refs/node-sdk/src/generate-text/tool-result.ts +36 -0
- package/bench/refs/node-sdk/src/telemetry/create-telemetry-dispatcher.ts +146 -0
- package/bench/refs/node-sdk/src/telemetry/telemetry-registry.ts +15 -0
- package/bench/refs/node-sdk/src/telemetry/telemetry.ts +223 -0
- package/bench/refs/node-sdk/src/util/async-iterable-stream.ts +94 -0
- package/bench/refs/node-sdk/src/util/consume-stream.ts +31 -0
- package/bench/refs/node-sdk/src/util/create-stitchable-stream.ts +112 -0
- package/bench/refs/node-sdk/src/util/download/download.ts +74 -0
- package/bench/refs/node-sdk/src/util/error-handler.ts +1 -0
- package/bench/refs/node-sdk/src/util/fix-json.ts +401 -0
- package/bench/refs/node-sdk/src/util/merge-objects.ts +79 -0
- package/bench/refs/python-lib/.refrev +1 -0
- package/bench/refs/python-lib/README.md +18 -0
- package/bench/refs/python-lib/pandas/core/_numba/executor.py +249 -0
- package/bench/refs/python-lib/pandas/core/_numba/extensions.py +586 -0
- package/bench/refs/python-lib/pandas/core/_numba/kernels/mean_.py +198 -0
- package/bench/refs/python-lib/pandas/core/_numba/kernels/min_max_.py +178 -0
- package/bench/refs/python-lib/pandas/core/_numba/kernels/sum_.py +255 -0
- package/bench/refs/python-lib/pandas/core/_numba/kernels/var_.py +251 -0
- package/bench/refs/python-lib/pandas/core/accessor.py +520 -0
- package/bench/refs/python-lib/pandas/core/api.py +138 -0
- package/bench/refs/python-lib/pandas/core/array_algos/masked_accumulations.py +97 -0
- package/bench/refs/python-lib/pandas/core/array_algos/masked_reductions.py +252 -0
- package/bench/refs/python-lib/pandas/core/array_algos/putmask.py +150 -0
- package/bench/refs/python-lib/pandas/core/array_algos/quantile.py +232 -0
- package/bench/refs/python-lib/pandas/core/array_algos/replace.py +159 -0
- package/bench/refs/python-lib/pandas/core/array_algos/take.py +609 -0
- package/bench/refs/python-lib/pandas/core/arraylike.py +534 -0
- package/bench/refs/python-lib/pandas/core/arrays/_arrow_string_mixins.py +434 -0
- package/bench/refs/python-lib/pandas/core/arrays/_mixins.py +630 -0
- package/bench/refs/python-lib/pandas/core/arrays/_ranges.py +276 -0
- package/bench/refs/python-lib/pandas/core/arrays/_utils.py +77 -0
- package/bench/refs/python-lib/pandas/core/arrays/arrow/accessors.py +521 -0
- package/bench/refs/python-lib/pandas/core/arrays/arrow/extension_types.py +174 -0
- package/bench/refs/python-lib/pandas/core/arrays/boolean.py +454 -0
- package/bench/refs/python-lib/pandas/core/arrays/floating.py +192 -0
- package/bench/refs/python-lib/pandas/core/arrays/integer.py +296 -0
- package/bench/refs/python-lib/pandas/core/arrays/masked.py +2140 -0
- package/bench/refs/python-lib/pandas/core/arrays/numeric.py +305 -0
- package/bench/refs/python-lib/pandas/core/arrays/numpy_.py +701 -0
- package/bench/refs/python-lib/pandas/core/arrays/period.py +1586 -0
- package/bench/refs/python-lib/pandas/core/arrays/sparse/accessor.py +505 -0
- package/bench/refs/python-lib/pandas/core/arrays/sparse/scipy_sparse.py +208 -0
- package/bench/refs/rust-project/.refrev +1 -0
- package/bench/refs/rust-project/README.md +17 -0
- package/bench/refs/rust-project/tokio/src/doc/os.rs +68 -0
- package/bench/refs/rust-project/tokio/src/fs/dir_builder.rs +129 -0
- package/bench/refs/rust-project/tokio/src/fs/file.rs +1113 -0
- package/bench/refs/rust-project/tokio/src/fs/mocks.rs +176 -0
- package/bench/refs/rust-project/tokio/src/fs/mod.rs +324 -0
- package/bench/refs/rust-project/tokio/src/fs/open_options.rs +856 -0
- package/bench/refs/rust-project/tokio/src/fs/read.rs +80 -0
- package/bench/refs/rust-project/tokio/src/fs/read_dir.rs +357 -0
- package/bench/refs/rust-project/tokio/src/fs/write.rs +99 -0
- package/bench/refs/rust-project/tokio/src/future/maybe_done.rs +123 -0
- package/bench/refs/rust-project/tokio/src/future/try_join.rs +82 -0
- package/bench/refs/rust-project/tokio/src/io/async_buf_read.rs +117 -0
- package/bench/refs/rust-project/tokio/src/io/async_read.rs +133 -0
- package/bench/refs/rust-project/tokio/src/io/async_seek.rs +96 -0
- package/bench/refs/rust-project/tokio/src/io/async_write.rs +402 -0
- package/bench/refs/rust-project/tokio/src/io/blocking.rs +339 -0
- package/bench/refs/rust-project/tokio/src/io/interest.rs +345 -0
- package/bench/refs/rust-project/tokio/src/io/join.rs +130 -0
- package/bench/refs/rust-project/tokio/src/io/mod.rs +308 -0
- package/bench/refs/rust-project/tokio/src/io/poll_evented.rs +319 -0
- package/bench/refs/rust-project/tokio/src/io/read_buf.rs +344 -0
- package/bench/refs/rust-project/tokio/src/io/ready.rs +322 -0
- package/bench/refs/rust-project/tokio/src/io/split.rs +166 -0
- package/bench/refs/rust-project/tokio/src/io/stderr.rs +148 -0
- package/bench/refs/rust-project/tokio/src/io/stdin.rs +98 -0
- package/bench/refs/rust-project/tokio/src/io/stdio_common.rs +222 -0
- package/bench/refs/rust-project/tokio/src/io/stdout.rs +197 -0
- package/bench/refs/rust-project/tokio/src/io/util/buf_reader.rs +311 -0
- package/bench/refs/rust-project/tokio/src/io/util/buf_stream.rs +207 -0
- package/bench/refs/rust-project/tokio/src/io/util/buf_writer.rs +310 -0
- package/bench/refs/rust-project/tokio/src/io/util/chain.rs +144 -0
- package/commands/_spawn-patterns.json +32 -0
- package/commands/ashlr-allow.md +21 -0
- package/commands/ashlr-benchmark.md +53 -0
- package/commands/ashlr-brief.md +97 -0
- package/commands/ashlr-budget.md +76 -0
- package/commands/ashlr-compact.md +54 -0
- package/commands/ashlr-dashboard.md +37 -0
- package/commands/ashlr-doctor.md +56 -0
- package/commands/ashlr-eco-mode.md +92 -0
- package/commands/ashlr-genome-init.md +24 -0
- package/commands/ashlr-genome-keygen.md +36 -0
- package/commands/ashlr-genome-loop.md +118 -0
- package/commands/ashlr-genome-push.md +36 -0
- package/commands/ashlr-genome-rewrap.md +48 -0
- package/commands/ashlr-genome-team-init.md +67 -0
- package/commands/ashlr-handoff.md +23 -0
- package/commands/ashlr-help.md +86 -0
- package/commands/ashlr-hook-timings.md +30 -0
- package/commands/ashlr-leaderboard.md +50 -0
- package/commands/ashlr-ollama-setup.md +39 -0
- package/commands/ashlr-orchestrate-status.md +72 -0
- package/commands/ashlr-orchestrate.md +83 -0
- package/commands/ashlr-parallelize.md +63 -0
- package/commands/ashlr-report-crash.md +33 -0
- package/commands/ashlr-resume.md +72 -0
- package/commands/ashlr-savings.md +24 -0
- package/commands/ashlr-settings.md +50 -0
- package/commands/ashlr-spawn.md +61 -0
- package/commands/ashlr-start.md +60 -0
- package/commands/ashlr-status.md +64 -0
- package/commands/ashlr-team-invite.md +47 -0
- package/commands/ashlr-tier.md +122 -0
- package/commands/ashlr-tour.md +60 -0
- package/commands/ashlr-update.md +155 -0
- package/commands/ashlr-upgrade.md +96 -0
- package/commands/goal.md +58 -0
- package/commands/loop.md +40 -0
- package/docs/CNAME +1 -0
- package/docs/PRO_TIER.md +104 -0
- package/docs/README.md +21 -0
- package/docs/architecture.md +415 -0
- package/docs/ashlr-pipe.md +161 -0
- package/docs/assets/logo.svg +19 -0
- package/docs/assets/og.png +0 -0
- package/docs/assets/og.svg +41 -0
- package/docs/benchmarks-v2.json +355 -0
- package/docs/benchmarks.json +102 -0
- package/docs/benchmarks.md +211 -0
- package/docs/billing.md +185 -0
- package/docs/cloud-genome.md +155 -0
- package/docs/competitive-wozcode.md +92 -0
- package/docs/deploy.md +325 -0
- package/docs/email-templates.md +113 -0
- package/docs/embed-tuning.md +103 -0
- package/docs/genome.md +120 -0
- package/docs/github-oauth-onboarding.md +127 -0
- package/docs/hero-video-script.md +129 -0
- package/docs/index.html +2262 -0
- package/docs/install-prompt.md +78 -0
- package/docs/install-windows.md +132 -0
- package/docs/install.ps1 +171 -0
- package/docs/install.sh +146 -0
- package/docs/launch/README.md +114 -0
- package/docs/launch/hn.md +49 -0
- package/docs/launch/reddit.md +45 -0
- package/docs/launch/x.md +65 -0
- package/docs/launch-post.md +112 -0
- package/docs/legal.md +78 -0
- package/docs/llm-providers.md +99 -0
- package/docs/multi-host-mcp.md +217 -0
- package/docs/multi-turn-architecture.md +108 -0
- package/docs/operations/deployment.md +108 -0
- package/docs/operations/error-monitoring.md +70 -0
- package/docs/operations/weekly-digest.md +70 -0
- package/docs/operations.md +494 -0
- package/docs/platform-support.md +133 -0
- package/docs/policy-packs.md +188 -0
- package/docs/press-kit/fact-sheet.md +78 -0
- package/docs/press-kit/founder-bio.md +3 -0
- package/docs/press-kit/logo.svg +33 -0
- package/docs/press-kit/screenshots.md +146 -0
- package/docs/pricing.md +251 -0
- package/docs/pro-backend-architecture.md +523 -0
- package/docs/producthunt.md +52 -0
- package/docs/router-migration.md +110 -0
- package/docs/session-log-schema.md +68 -0
- package/docs/social.md +95 -0
- package/docs/team-genome.md +528 -0
- package/docs/telemetry.md +194 -0
- package/docs/test-isolation.md +100 -0
- package/docs/upgrade.md +159 -0
- package/hooks/_hook-errors.ts +87 -0
- package/hooks/_hook-perf-emit.ts +113 -0
- package/hooks/_hook-timing-ledger.ts +1133 -0
- package/hooks/_nudge-throttle.ts +127 -0
- package/hooks/_recent-blocks.ts +127 -0
- package/hooks/audit-upload.ts +86 -0
- package/hooks/codex-hooks.json +171 -0
- package/hooks/commit-attribution.ts +179 -0
- package/hooks/edit-batching-nudge.ts +154 -0
- package/hooks/genome-scribe-hook.ts +219 -0
- package/hooks/hooks.json +294 -0
- package/hooks/orient-nudge-hook.ts +242 -0
- package/hooks/policy-enforce.ts +214 -0
- package/hooks/post-tool-use-embedding.ts +93 -0
- package/hooks/post-tool-use-genome.sh +37 -0
- package/hooks/post-tool-use-genome.ts +69 -0
- package/hooks/posttooluse-correlate.ts +122 -0
- package/hooks/posttooluse-dedupe-output.ts +241 -0
- package/hooks/posttooluse-genome-refresh.ts +223 -0
- package/hooks/posttooluse-native-nudge.ts +205 -0
- package/hooks/posttooluse-stale-result.ts +277 -0
- package/hooks/pretooluse-bash.ts +63 -0
- package/hooks/pretooluse-budget-guard.ts +318 -0
- package/hooks/pretooluse-common.ts +985 -0
- package/hooks/pretooluse-eco-router.ts +196 -0
- package/hooks/pretooluse-edit.sh +93 -0
- package/hooks/pretooluse-edit.ts +197 -0
- package/hooks/pretooluse-glob.ts +70 -0
- package/hooks/pretooluse-grep.sh +85 -0
- package/hooks/pretooluse-grep.ts +96 -0
- package/hooks/pretooluse-notebookedit.ts +121 -0
- package/hooks/pretooluse-read.sh +118 -0
- package/hooks/pretooluse-read.ts +118 -0
- package/hooks/pretooluse-task.ts +130 -0
- package/hooks/pretooluse-webfetch.ts +66 -0
- package/hooks/pretooluse-websearch.ts +85 -0
- package/hooks/pretooluse-write.ts +128 -0
- package/hooks/pulse-emit.ts +197 -0
- package/hooks/session-end-consolidate.sh +35 -0
- package/hooks/session-end-consolidate.ts +188 -0
- package/hooks/session-end-stats-gc.ts +80 -0
- package/hooks/session-log-append.sh +94 -0
- package/hooks/session-log-append.ts +116 -0
- package/hooks/session-start.ts +993 -0
- package/hooks/sessionend-hook-health-nudge.ts +451 -0
- package/hooks/sessionend-savings-nudge.ts +318 -0
- package/hooks/sessionend-status-line-nudge.ts +296 -0
- package/hooks/sessionstart-brief.ts +140 -0
- package/hooks/sessionstart-cost-refactor.ts +95 -0
- package/hooks/sessionstart-efficient.ts +110 -0
- package/hooks/sessionstart-genome-author.ts +93 -0
- package/hooks/sessionstart-lean-tools.ts +98 -0
- package/hooks/sessionstart-search.ts +97 -0
- package/hooks/stop-accounting.ts +152 -0
- package/hooks/subagent-stop-rollup.ts +162 -0
- package/hooks/userpromptsubmit-brief-trigger.ts +156 -0
- package/package.json +56 -0
- package/ports/README.md +108 -0
- package/ports/cursor/mcp.json +11 -0
- package/ports/goose/recipe.yaml +26 -0
- package/scripts/ashlr-mcp.ts +40 -0
- package/scripts/auto-update.ts +177 -0
- package/scripts/baseline-scan.ts +831 -0
- package/scripts/bench-orchestrate.ts +377 -0
- package/scripts/benchmark-refs.ts +292 -0
- package/scripts/bootstrap.mjs +100 -0
- package/scripts/brief-eval.ts +234 -0
- package/scripts/bump-version.ts +82 -0
- package/scripts/bun-resolve.d.mts +11 -0
- package/scripts/bun-resolve.mjs +59 -0
- package/scripts/calibrate-grep.ts +467 -0
- package/scripts/capture-grep-workload.ts +260 -0
- package/scripts/check-tier.ts +61 -0
- package/scripts/check-version-sync.ts +97 -0
- package/scripts/cli-orchestrate-status.ts +386 -0
- package/scripts/cli-orchestrate.ts +303 -0
- package/scripts/cli.ts +478 -0
- package/scripts/cloud-smoke-test.ts +313 -0
- package/scripts/coach-report.ts +373 -0
- package/scripts/context-db-migrate.ts +137 -0
- package/scripts/context-status.ts +43 -0
- package/scripts/demo-run.ts +255 -0
- package/scripts/deploy-smoke.sh +140 -0
- package/scripts/doctor.ts +992 -0
- package/scripts/embed-file-worker.ts +104 -0
- package/scripts/embed-tune.ts +308 -0
- package/scripts/errors-report.ts +428 -0
- package/scripts/find-test-leak.ts +69 -0
- package/scripts/generate-badge.ts +357 -0
- package/scripts/genome-auto-consolidate.ts +533 -0
- package/scripts/genome-auto-propose.ts +578 -0
- package/scripts/genome-cloud-pull.ts +363 -0
- package/scripts/genome-cloud-push.ts +382 -0
- package/scripts/genome-cloud-sync.ts +587 -0
- package/scripts/genome-commit-watcher.ts +295 -0
- package/scripts/genome-init.ts +986 -0
- package/scripts/genome-key.ts +233 -0
- package/scripts/genome-keygen.ts +168 -0
- package/scripts/genome-link.ts +59 -0
- package/scripts/genome-refresh-worker.ts +364 -0
- package/scripts/genome-rewrap.ts +90 -0
- package/scripts/genome-synthesizer.ts +679 -0
- package/scripts/genome-team-init.ts +312 -0
- package/scripts/handoff-pack.ts +438 -0
- package/scripts/hook-bootstrap.mjs +75 -0
- package/scripts/hook-timing-reader.ts +371 -0
- package/scripts/hook-timings-report.ts +554 -0
- package/scripts/install-genome-hooks.ts +196 -0
- package/scripts/install-onnx-model.ts +154 -0
- package/scripts/install-permissions.ts +276 -0
- package/scripts/install-status-line.ts +150 -0
- package/scripts/mcp-entrypoint.sh +78 -0
- package/scripts/mcp-entrypoint.ts +130 -0
- package/scripts/mdx-angle-bracket-lint.ts +152 -0
- package/scripts/measure-cold-start.ts +126 -0
- package/scripts/migrate-stats-to-sqlite.ts +86 -0
- package/scripts/ollama-setup.ts +498 -0
- package/scripts/onboarding-wizard.ts +1691 -0
- package/scripts/orchestrate-emit-telemetry.ts +177 -0
- package/scripts/orchestrate-expand.ts +517 -0
- package/scripts/orchestrate-run.ts +744 -0
- package/scripts/print-legend.ts +50 -0
- package/scripts/product-metadata.ts +38 -0
- package/scripts/prune-old-memories.ts +208 -0
- package/scripts/publish.sh +88 -0
- package/scripts/read-calibration.ts +122 -0
- package/scripts/report-crash.ts +226 -0
- package/scripts/run-benchmark.ts +750 -0
- package/scripts/run-root-tests.ts +28 -0
- package/scripts/savings-dashboard.ts +1258 -0
- package/scripts/savings-report-extras.ts +447 -0
- package/scripts/savings-status-line.ts +873 -0
- package/scripts/session-event-emit.ts +266 -0
- package/scripts/session-greet.ts +686 -0
- package/scripts/session-log-report.ts +551 -0
- package/scripts/session-resume.ts +614 -0
- package/scripts/set-hook-mode.ts +83 -0
- package/scripts/smoke-cross-platform.ts +117 -0
- package/scripts/smoke-realtime.ts +257 -0
- package/scripts/smoke-tools.ts +21 -0
- package/scripts/stats-cloud-pull.ts +213 -0
- package/scripts/stats-cloud-sync.ts +261 -0
- package/scripts/telemetry-flush.ts +160 -0
- package/scripts/telemetry-status.ts +266 -0
- package/scripts/timing-budget.ts +35 -0
- package/scripts/ui-animation.ts +120 -0
- package/scripts/ui.ts +431 -0
- package/scripts/upgrade-flow.ts +775 -0
- package/scripts/weekly-digest.ts +394 -0
- package/servers/_accounting.ts +78 -0
- package/servers/_ask-router.ts +84 -0
- package/servers/_ast-chunker.ts +434 -0
- package/servers/_ast-helpers.ts +183 -0
- package/servers/_ast-languages.ts +153 -0
- package/servers/_ast-refactor/_shared.ts +221 -0
- package/servers/_ast-refactor/cross-file-rename.ts +560 -0
- package/servers/_ast-refactor/extract-function.ts +357 -0
- package/servers/_ast-refactor/file-local-rename.ts +172 -0
- package/servers/_ast-refactor.ts +41 -0
- package/servers/_ast-skeleton.ts +107 -0
- package/servers/_bash-summarizers-registry.ts +1096 -0
- package/servers/_crash-dump.ts +210 -0
- package/servers/_cwd-clamp.ts +342 -0
- package/servers/_edit-log.ts +28 -0
- package/servers/_edit-match.ts +236 -0
- package/servers/_edit-validate.ts +196 -0
- package/servers/_embed-calibration.ts +123 -0
- package/servers/_embedding-cache.ts +401 -0
- package/servers/_embedding-model.ts +329 -0
- package/servers/_events.ts +113 -0
- package/servers/_genome-cache.ts +154 -0
- package/servers/_genome-commits.ts +172 -0
- package/servers/_genome-crypto-v2.ts +242 -0
- package/servers/_genome-crypto.ts +259 -0
- package/servers/_genome-discoveries.ts +140 -0
- package/servers/_genome-embed-populator.ts +216 -0
- package/servers/_genome-freshness.ts +134 -0
- package/servers/_genome-issues.ts +171 -0
- package/servers/_genome-live.ts +263 -0
- package/servers/_genome-prs.ts +188 -0
- package/servers/_genome-search.ts +479 -0
- package/servers/_genome-sync.ts +297 -0
- package/servers/_history-tracker.ts +279 -0
- package/servers/_http-helpers.ts +151 -0
- package/servers/_identity-hash.ts +151 -0
- package/servers/_llm-providers/anthropic.ts +132 -0
- package/servers/_llm-providers/cloud.ts +195 -0
- package/servers/_llm-providers/index.ts +350 -0
- package/servers/_llm-providers/local.ts +127 -0
- package/servers/_llm-providers/onnx.ts +393 -0
- package/servers/_llm-providers/types.ts +27 -0
- package/servers/_manifest-v2.ts +447 -0
- package/servers/_mcp-host.ts +93 -0
- package/servers/_nudge-events.ts +451 -0
- package/servers/_orchestrate-executor.ts +316 -0
- package/servers/_prefetch.ts +435 -0
- package/servers/_pricing.ts +256 -0
- package/servers/_pro.ts +381 -0
- package/servers/_read-cache.ts +44 -0
- package/servers/_router-handlers.ts +46 -0
- package/servers/_router.ts +55 -0
- package/servers/_run-with-timeout.ts +173 -0
- package/servers/_savings-render.ts +214 -0
- package/servers/_stats-sqlite.ts +640 -0
- package/servers/_stats.ts +1183 -0
- package/servers/_status-line-cells.ts +469 -0
- package/servers/_streaks.ts +162 -0
- package/servers/_summarize.ts +208 -0
- package/servers/_task-graph-render.ts +217 -0
- package/servers/_task-graph.ts +545 -0
- package/servers/_telemetry.ts +719 -0
- package/servers/_test-parsers.ts +530 -0
- package/servers/_test-watch.ts +626 -0
- package/servers/_text-helpers.ts +37 -0
- package/servers/_token-measure.ts +160 -0
- package/servers/_tool-base.ts +248 -0
- package/servers/_weekly-digest.ts +391 -0
- package/servers/ask-server-handlers.ts +49 -0
- package/servers/ask-server.ts +174 -0
- package/servers/bash-server-handlers.ts +138 -0
- package/servers/bash-server.ts +1136 -0
- package/servers/bench.ts +186 -0
- package/servers/diff-semantic-server-handlers.ts +39 -0
- package/servers/diff-semantic-server.ts +566 -0
- package/servers/diff-server-handlers.ts +317 -0
- package/servers/diff-server.ts +20 -0
- package/servers/edit-server-handlers.ts +31 -0
- package/servers/edit-server.ts +209 -0
- package/servers/edit-structural-server-handlers.ts +322 -0
- package/servers/efficiency-server-handlers.ts +161 -0
- package/servers/efficiency-server.ts +1135 -0
- package/servers/flush-server-handlers.ts +21 -0
- package/servers/flush-server.ts +19 -0
- package/servers/genome-server-handlers.ts +118 -0
- package/servers/genome-server.ts +397 -0
- package/servers/github-server-handlers.ts +187 -0
- package/servers/github-server.ts +856 -0
- package/servers/glob-server-handlers.ts +233 -0
- package/servers/glob-server.ts +20 -0
- package/servers/grep-server-handlers.ts +61 -0
- package/servers/grep-server.ts +564 -0
- package/servers/http-server-handlers.ts +36 -0
- package/servers/http-server.ts +149 -0
- package/servers/logs-server-handlers.ts +42 -0
- package/servers/logs-server.ts +359 -0
- package/servers/ls-server-handlers.ts +166 -0
- package/servers/ls-server.ts +20 -0
- package/servers/multi-edit-server-handlers.ts +51 -0
- package/servers/multi-edit-server.ts +422 -0
- package/servers/notebook-edit-server-handlers.ts +34 -0
- package/servers/notebook-edit-server.ts +181 -0
- package/servers/orient-server-handlers.ts +52 -0
- package/servers/orient-server.ts +436 -0
- package/servers/pipe-server-handlers.ts +68 -0
- package/servers/pipe-server.ts +300 -0
- package/servers/read-server-handlers.ts +42 -0
- package/servers/read-server.ts +206 -0
- package/servers/rename-file-server-handlers.ts +67 -0
- package/servers/rename-file-server.ts +699 -0
- package/servers/savings-server-handlers.ts +27 -0
- package/servers/savings-server.ts +54 -0
- package/servers/search-replace-regex-server-handlers.ts +96 -0
- package/servers/search-replace-regex-server.ts +751 -0
- package/servers/sql-server-handlers.ts +58 -0
- package/servers/sql-server.ts +505 -0
- package/servers/task-server-handlers.ts +358 -0
- package/servers/task-server.ts +24 -0
- package/servers/test-server-handlers.ts +351 -0
- package/servers/test-server.ts +20 -0
- package/servers/tree-server-handlers.ts +503 -0
- package/servers/tree-server.ts +20 -0
- package/servers/webfetch-server-handlers.ts +166 -0
- package/servers/webfetch-server.ts +20 -0
- package/servers/websearch-server-handlers.ts +215 -0
- package/servers/websearch-server.ts +19 -0
- package/servers/write-server-handlers.ts +27 -0
- package/servers/write-server.ts +97 -0
- package/skills/ashlr-brief/SKILL.md +80 -0
- package/skills/ashlr-codex-doctor/SKILL.md +21 -0
- package/skills/ashlr-codex-efficient-tools/SKILL.md +17 -0
- package/skills/ashlr-codex-genome-refresh/SKILL.md +20 -0
- package/skills/ashlr-codex-parallel-explore/SKILL.md +20 -0
- package/skills/ashlr-codex-savings/SKILL.md +17 -0
- package/skills/ashlr-cost-refactor/SKILL.md +50 -0
- package/skills/ashlr-efficient/SKILL.md +64 -0
- package/skills/ashlr-genome-author/SKILL.md +43 -0
- package/skills/ashlr-lean-tools/SKILL.md +43 -0
- package/skills/ashlr-search/SKILL.md +46 -0
|
@@ -0,0 +1,856 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* ashlr-github MCP server.
|
|
4
|
+
*
|
|
5
|
+
* Exposes two read-only tools that compress GitHub PR / issue API output so
|
|
6
|
+
* reviewer agents don't burn 10-30K tokens on raw `gh` JSON dumps:
|
|
7
|
+
*
|
|
8
|
+
* - ashlr__pr — compact PR header, reviews, unresolved comments, checks
|
|
9
|
+
* - ashlr__issue — compact issue header, body, and comment list
|
|
10
|
+
*
|
|
11
|
+
* Never mutates. Shells out to `gh` and times out at 15s per call. Savings
|
|
12
|
+
* are persisted to the shared ~/.ashlr/stats.json.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
16
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
17
|
+
import {
|
|
18
|
+
CallToolRequestSchema,
|
|
19
|
+
ListToolsRequestSchema,
|
|
20
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
21
|
+
import { spawnSync } from "child_process";
|
|
22
|
+
|
|
23
|
+
import { snipCompact } from "@ashlr/core-efficiency/compression";
|
|
24
|
+
import type { Message } from "@ashlr/core-efficiency";
|
|
25
|
+
import { recordSaving as recordSavingCore } from "./_stats";
|
|
26
|
+
import { logEvent } from "./_events";
|
|
27
|
+
import { summarizeIfLarge, PROMPTS } from "./_summarize";
|
|
28
|
+
|
|
29
|
+
// PR diffs ≥ this size are routed through the LLM summarizer (PROMPTS.diff)
|
|
30
|
+
// with a ~2 KB budget. Smaller diffs pass through unchanged. Chosen to catch
|
|
31
|
+
// 50+ file PRs (typically 40-200 KB of diff text) without paying the LLM
|
|
32
|
+
// roundtrip on small 1-2 file PRs.
|
|
33
|
+
const PR_DIFF_LLM_THRESHOLD_BYTES = 16 * 1024;
|
|
34
|
+
const PR_DIFF_LLM_BUDGET_BYTES = 2 * 1024;
|
|
35
|
+
|
|
36
|
+
// Cap on `gh` JSON payloads we'll attempt to parse. Real PR/issue JSON weighs
|
|
37
|
+
// in well under this; anything larger is pathological and parsing it would
|
|
38
|
+
// burn memory for no useful render.
|
|
39
|
+
const MAX_GH_JSON_BYTES = 4 * 1024 * 1024;
|
|
40
|
+
|
|
41
|
+
function safeParseGhJson<T>(raw: string, tool: string, kind: string): T {
|
|
42
|
+
if (raw.length > MAX_GH_JSON_BYTES) {
|
|
43
|
+
void logEvent("tool_error", {
|
|
44
|
+
tool,
|
|
45
|
+
reason: `${kind} payload ${raw.length} bytes exceeds cap`,
|
|
46
|
+
});
|
|
47
|
+
throw new Error(`${tool}: ${kind} payload too large (${raw.length} bytes, cap ${MAX_GH_JSON_BYTES})`);
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(raw) as T;
|
|
51
|
+
} catch (err) {
|
|
52
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
53
|
+
void logEvent("tool_error", {
|
|
54
|
+
tool,
|
|
55
|
+
reason: `malformed ${kind} JSON: ${msg}`,
|
|
56
|
+
});
|
|
57
|
+
throw new Error(`${tool}: malformed ${kind} JSON from gh CLI`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type ToolName =
|
|
62
|
+
| "ashlr__pr"
|
|
63
|
+
| "ashlr__issue"
|
|
64
|
+
| "ashlr__pr_comment"
|
|
65
|
+
| "ashlr__pr_approve"
|
|
66
|
+
| "ashlr__issue_create"
|
|
67
|
+
| "ashlr__issue_close";
|
|
68
|
+
|
|
69
|
+
async function recordSaving(rawChars: number, compactChars: number, tool: ToolName): Promise<void> {
|
|
70
|
+
await recordSavingCore(rawChars, compactChars, tool);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// gh runner
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
const GH_TIMEOUT_MS = 15_000;
|
|
78
|
+
|
|
79
|
+
function ghOnPath(): boolean {
|
|
80
|
+
// Bun.which is cross-platform and avoids shelling out to `sh`.
|
|
81
|
+
if (typeof (globalThis as { Bun?: { which(b: string): string | null } }).Bun !== "undefined") {
|
|
82
|
+
return !!(globalThis as { Bun: { which(b: string): string | null } }).Bun.which("gh");
|
|
83
|
+
}
|
|
84
|
+
// Fallback for non-Bun runtimes.
|
|
85
|
+
const which = spawnSync(process.platform === "win32" ? "where" : "which", ["gh"], {
|
|
86
|
+
encoding: "utf-8",
|
|
87
|
+
});
|
|
88
|
+
return which.status === 0 && !!which.stdout.trim();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Run `gh` with given args. Throws on non-zero exit or missing binary. */
|
|
92
|
+
function runGh(args: string[], cwd?: string): string {
|
|
93
|
+
if (!ghOnPath()) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
"gh CLI not found on PATH. Install with `brew install gh` (macOS) or see https://cli.github.com",
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
const res = spawnSync("gh", args, {
|
|
99
|
+
encoding: "utf-8",
|
|
100
|
+
timeout: GH_TIMEOUT_MS,
|
|
101
|
+
cwd,
|
|
102
|
+
// Inherit env so gh picks up GH_TOKEN / credential helpers.
|
|
103
|
+
env: process.env,
|
|
104
|
+
});
|
|
105
|
+
if (res.status !== 0) {
|
|
106
|
+
const err = (res.stderr || "").trim();
|
|
107
|
+
// Don't leak a token if gh ever echoed one — just show the first line.
|
|
108
|
+
const firstLine = err.split("\n")[0] ?? "";
|
|
109
|
+
if (/not logged in|authentication required/i.test(err)) {
|
|
110
|
+
throw new Error("gh not authenticated. Run `gh auth login` first.");
|
|
111
|
+
}
|
|
112
|
+
throw new Error(`gh ${args[0]} failed: ${firstLine || "exit " + res.status}`);
|
|
113
|
+
}
|
|
114
|
+
return res.stdout;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Repo detection
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
function detectRepo(cwd = process.cwd()): string {
|
|
122
|
+
// Try `gh repo view` first (works even when remote is a short form).
|
|
123
|
+
try {
|
|
124
|
+
const out = runGh(["repo", "view", "--json", "nameWithOwner"], cwd);
|
|
125
|
+
const parsed = safeParseGhJson<{ nameWithOwner?: string }>(out, "ashlr__pr", "repo view");
|
|
126
|
+
if (parsed.nameWithOwner) return parsed.nameWithOwner;
|
|
127
|
+
} catch { /* fall through */ }
|
|
128
|
+
|
|
129
|
+
// Fallback: parse `git remote get-url origin`.
|
|
130
|
+
const git = spawnSync("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf-8" });
|
|
131
|
+
if (git.status === 0 && git.stdout) {
|
|
132
|
+
const url = git.stdout.trim();
|
|
133
|
+
// git@github.com:owner/repo(.git)? OR https://github.com/owner/repo(.git)?
|
|
134
|
+
const m = url.match(/github\.com[:/]([^/]+\/[^/\s]+?)(?:\.git)?$/);
|
|
135
|
+
if (m) return m[1]!;
|
|
136
|
+
}
|
|
137
|
+
throw new Error(
|
|
138
|
+
"Could not detect GitHub repo from cwd. Pass `repo: \"owner/name\"` explicitly.",
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Compression helper — wrap string through snipCompact using the tool_result
|
|
144
|
+
// trick (same approach efficiency-server uses). Keeps compression logic
|
|
145
|
+
// consolidated in core-efficiency instead of re-implementing head/tail here.
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
function snipText(s: string, minLen = 500): string {
|
|
149
|
+
if (s.length <= minLen) return s;
|
|
150
|
+
// snipCompact's internal threshold is 2KB. For bodies/comments just over our
|
|
151
|
+
// callsite threshold we still want compression, so do a simple head/tail fold
|
|
152
|
+
// ourselves in the 500–2048 range; above 2KB defer to snipCompact.
|
|
153
|
+
if (s.length <= 2048) {
|
|
154
|
+
const keep = 250;
|
|
155
|
+
return s.slice(0, keep) + `\n[... ${s.length - 2 * keep} chars elided ...]\n` + s.slice(-keep);
|
|
156
|
+
}
|
|
157
|
+
const msgs: Message[] = [
|
|
158
|
+
{
|
|
159
|
+
role: "user",
|
|
160
|
+
content: [{ type: "tool_result", tool_use_id: "ashlr-gh", content: s }],
|
|
161
|
+
},
|
|
162
|
+
];
|
|
163
|
+
const out = snipCompact(msgs);
|
|
164
|
+
const block = (out[0]!.content as { type: string; content: string }[])[0]!;
|
|
165
|
+
return (block as { content: string }).content;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Compact a string to at most `max` chars with a trailing ellipsis marker. */
|
|
169
|
+
function cap(s: string, max: number): string {
|
|
170
|
+
if (s.length <= max) return s;
|
|
171
|
+
return s.slice(0, max - 1) + "…";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** One-line flattening of whitespace for table-style rendering. */
|
|
175
|
+
function flat(s: string): string {
|
|
176
|
+
return s.replace(/\s+/g, " ").trim();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// Types we care about in the gh JSON (partial — only fields we read).
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
interface Review {
|
|
184
|
+
author?: { login?: string };
|
|
185
|
+
state?: string; // APPROVED | CHANGES_REQUESTED | COMMENTED | PENDING | DISMISSED
|
|
186
|
+
body?: string;
|
|
187
|
+
submittedAt?: string;
|
|
188
|
+
}
|
|
189
|
+
interface PRComment {
|
|
190
|
+
author?: { login?: string };
|
|
191
|
+
body?: string;
|
|
192
|
+
path?: string;
|
|
193
|
+
line?: number;
|
|
194
|
+
createdAt?: string;
|
|
195
|
+
isResolved?: boolean;
|
|
196
|
+
}
|
|
197
|
+
interface CheckRun {
|
|
198
|
+
name?: string;
|
|
199
|
+
status?: string;
|
|
200
|
+
conclusion?: string; // SUCCESS | FAILURE | NEUTRAL | CANCELLED | SKIPPED | TIMED_OUT | ACTION_REQUIRED
|
|
201
|
+
state?: string; // status-context style (older)
|
|
202
|
+
}
|
|
203
|
+
interface PRFile { path?: string; additions?: number; deletions?: number }
|
|
204
|
+
interface PRData {
|
|
205
|
+
number: number;
|
|
206
|
+
title: string;
|
|
207
|
+
state: string;
|
|
208
|
+
author?: { login?: string };
|
|
209
|
+
createdAt?: string;
|
|
210
|
+
updatedAt?: string;
|
|
211
|
+
mergeable?: string;
|
|
212
|
+
reviewDecision?: string; // APPROVED | CHANGES_REQUESTED | REVIEW_REQUIRED | null
|
|
213
|
+
additions?: number;
|
|
214
|
+
deletions?: number;
|
|
215
|
+
changedFiles?: number;
|
|
216
|
+
baseRefName?: string;
|
|
217
|
+
headRefName?: string;
|
|
218
|
+
body?: string;
|
|
219
|
+
labels?: Array<{ name?: string }>;
|
|
220
|
+
reviews?: Review[];
|
|
221
|
+
comments?: PRComment[];
|
|
222
|
+
files?: PRFile[];
|
|
223
|
+
statusCheckRollup?: CheckRun[];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
interface IssueComment { author?: { login?: string }; body?: string; createdAt?: string }
|
|
227
|
+
interface IssueData {
|
|
228
|
+
number: number;
|
|
229
|
+
title: string;
|
|
230
|
+
state: string;
|
|
231
|
+
author?: { login?: string };
|
|
232
|
+
createdAt?: string;
|
|
233
|
+
updatedAt?: string;
|
|
234
|
+
body?: string;
|
|
235
|
+
labels?: Array<{ name?: string }>;
|
|
236
|
+
comments?: IssueComment[];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// PR renderer
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
function shortDate(iso?: string): string {
|
|
244
|
+
if (!iso) return "?";
|
|
245
|
+
return iso.slice(0, 10);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function decisionBadge(decision?: string, reviews?: Review[]): string {
|
|
249
|
+
// reviewDecision can be null for REVIEW_REQUIRED when no one's reviewed yet;
|
|
250
|
+
// when present it's the authoritative signal.
|
|
251
|
+
if (decision === "APPROVED") return "APPROVED";
|
|
252
|
+
if (decision === "CHANGES_REQUESTED") return "CHANGES_REQUESTED";
|
|
253
|
+
if (decision === "REVIEW_REQUIRED") return "REVIEW_REQUIRED";
|
|
254
|
+
// Fall back to review shape: if at least one COMMENTED and no approval/changes,
|
|
255
|
+
// call it "COMMENTED". Otherwise "no-decision".
|
|
256
|
+
const states = (reviews ?? []).map((r) => r.state).filter(Boolean);
|
|
257
|
+
if (states.includes("CHANGES_REQUESTED")) return "CHANGES_REQUESTED";
|
|
258
|
+
if (states.includes("APPROVED")) return "APPROVED";
|
|
259
|
+
if (states.includes("COMMENTED")) return "COMMENTED";
|
|
260
|
+
return "REVIEW_REQUIRED";
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function summarizeChecks(rollup?: CheckRun[]): string {
|
|
264
|
+
if (!rollup || rollup.length === 0) return "checks: (none)";
|
|
265
|
+
let pass = 0;
|
|
266
|
+
let fail = 0;
|
|
267
|
+
let pending = 0;
|
|
268
|
+
const failures: string[] = [];
|
|
269
|
+
for (const c of rollup) {
|
|
270
|
+
const conc = (c.conclusion || c.state || "").toUpperCase();
|
|
271
|
+
if (conc === "SUCCESS" || conc === "NEUTRAL" || conc === "SKIPPED") pass++;
|
|
272
|
+
else if (conc === "FAILURE" || conc === "TIMED_OUT" || conc === "ACTION_REQUIRED" || conc === "CANCELLED" || conc === "ERROR") {
|
|
273
|
+
fail++;
|
|
274
|
+
if (c.name) failures.push(c.name);
|
|
275
|
+
} else {
|
|
276
|
+
pending++;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const parts: string[] = [];
|
|
280
|
+
if (pass) parts.push(`✓ ${pass} pass`);
|
|
281
|
+
if (fail) {
|
|
282
|
+
const fnames = failures.slice(0, 3).join(", ") + (failures.length > 3 ? "…" : "");
|
|
283
|
+
parts.push(`✗ ${fail} fail (${fnames})`);
|
|
284
|
+
}
|
|
285
|
+
if (pending) parts.push(`⋯ ${pending} pending`);
|
|
286
|
+
return "checks: " + (parts.join(" · ") || "(none)");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Compress a PR diff. For diffs < PR_DIFF_LLM_THRESHOLD_BYTES, pass through
|
|
291
|
+
* unchanged. For larger diffs, route through `summarizeIfLarge` with the
|
|
292
|
+
* shared diff prompt and a ~2KB output budget. Falls back to snipCompact on
|
|
293
|
+
* LLM failure (handled inside summarizeIfLarge).
|
|
294
|
+
*/
|
|
295
|
+
async function compressPRDiff(diff: string): Promise<string> {
|
|
296
|
+
const bytes = Buffer.byteLength(diff, "utf-8");
|
|
297
|
+
if (bytes < PR_DIFF_LLM_THRESHOLD_BYTES) return diff;
|
|
298
|
+
const r = await summarizeIfLarge(diff, {
|
|
299
|
+
toolName: "ashlr__pr",
|
|
300
|
+
systemPrompt: PROMPTS.diff,
|
|
301
|
+
thresholdBytes: PR_DIFF_LLM_BUDGET_BYTES,
|
|
302
|
+
});
|
|
303
|
+
return r.text;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function renderPR(pr: PRData, mode: "summary" | "full" | "thread", diff?: string): Promise<string> {
|
|
307
|
+
const lines: string[] = [];
|
|
308
|
+
const author = pr.author?.login ?? "?";
|
|
309
|
+
const decision = decisionBadge(pr.reviewDecision, pr.reviews);
|
|
310
|
+
const lbls = (pr.labels ?? []).map((l) => l.name).filter(Boolean).join(", ");
|
|
311
|
+
|
|
312
|
+
// Header row — dense, one line.
|
|
313
|
+
lines.push(
|
|
314
|
+
`PR #${pr.number} · ${pr.state} · ${decision} · by ${author} · ${shortDate(pr.createdAt)} → ${shortDate(pr.updatedAt)} · +${pr.additions ?? 0} −${pr.deletions ?? 0} · ${pr.changedFiles ?? 0} files${pr.mergeable ? " · mergeable:" + pr.mergeable : ""}`,
|
|
315
|
+
);
|
|
316
|
+
lines.push(`title: ${pr.title}`);
|
|
317
|
+
lines.push(`branch: ${pr.headRefName ?? "?"} → ${pr.baseRefName ?? "?"}`);
|
|
318
|
+
if (lbls) lines.push(`labels: ${lbls}`);
|
|
319
|
+
|
|
320
|
+
if (mode !== "thread") {
|
|
321
|
+
const body = flat(pr.body ?? "");
|
|
322
|
+
if (body) lines.push(`body: ${cap(body, 300)}`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Reviews (ordered chronologically; one line each).
|
|
326
|
+
const reviews = (pr.reviews ?? []).filter((r) => r.state && r.state !== "PENDING");
|
|
327
|
+
if (reviews.length) {
|
|
328
|
+
lines.push(`reviews (${reviews.length}):`);
|
|
329
|
+
for (const r of reviews) {
|
|
330
|
+
const who = r.author?.login ?? "?";
|
|
331
|
+
const st = r.state ?? "?";
|
|
332
|
+
const snippet = cap(flat(r.body ?? ""), 80);
|
|
333
|
+
lines.push(` · ${who} · ${st}${snippet ? ' · "' + snippet + '"' : ""}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Unresolved review comments (inline comments). `gh pr view` returns top-level
|
|
338
|
+
// discussion comments as `comments`; for inline we rely on body-less review
|
|
339
|
+
// comments that show up here with `path`/`line`. Respect `isResolved` when set.
|
|
340
|
+
const comments = (pr.comments ?? []).filter((c) => c.isResolved !== true);
|
|
341
|
+
if (comments.length) {
|
|
342
|
+
const label = mode === "thread" ? "comments" : "unresolved comments";
|
|
343
|
+
lines.push(`${label} (${comments.length}):`);
|
|
344
|
+
const take = mode === "thread" ? comments : comments.slice(0, 10);
|
|
345
|
+
for (const c of take) {
|
|
346
|
+
const who = c.author?.login ?? "?";
|
|
347
|
+
const where = c.path ? `${c.path}${c.line ? ":" + c.line : ""}` : "";
|
|
348
|
+
const snippet = cap(flat(c.body ?? ""), 80);
|
|
349
|
+
lines.push(` · ${who}${where ? " · " + where : ""}${snippet ? ' · "' + snippet + '"' : ""}`);
|
|
350
|
+
}
|
|
351
|
+
if (mode !== "thread" && comments.length > 10) {
|
|
352
|
+
lines.push(` · (+${comments.length - 10} more — pass mode:"thread" to see all)`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Checks (always; they're the highest-signal compression win).
|
|
357
|
+
lines.push(summarizeChecks(pr.statusCheckRollup));
|
|
358
|
+
|
|
359
|
+
if (mode === "full" && diff !== undefined) {
|
|
360
|
+
// For large diffs (>= 16 KB) route through the LLM summarizer with the
|
|
361
|
+
// PROMPTS.diff prompt. For smaller diffs, keep the existing snipText path
|
|
362
|
+
// (head/tail fold for < 2 KB, snipCompact for 2-16 KB).
|
|
363
|
+
const diffBytes = Buffer.byteLength(diff, "utf-8");
|
|
364
|
+
const rendered = diffBytes >= PR_DIFF_LLM_THRESHOLD_BYTES
|
|
365
|
+
? await compressPRDiff(diff)
|
|
366
|
+
: snipText(diff);
|
|
367
|
+
lines.push("");
|
|
368
|
+
lines.push("diff:");
|
|
369
|
+
lines.push(rendered);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return lines.join("\n");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ---------------------------------------------------------------------------
|
|
376
|
+
// Issue renderer
|
|
377
|
+
// ---------------------------------------------------------------------------
|
|
378
|
+
|
|
379
|
+
function renderIssue(iss: IssueData, mode: "summary" | "thread"): string {
|
|
380
|
+
const lines: string[] = [];
|
|
381
|
+
const author = iss.author?.login ?? "?";
|
|
382
|
+
const lbls = (iss.labels ?? []).map((l) => l.name).filter(Boolean).join(", ");
|
|
383
|
+
lines.push(
|
|
384
|
+
`Issue #${iss.number} · ${iss.state} · by ${author} · ${shortDate(iss.createdAt)} → ${shortDate(iss.updatedAt)}`,
|
|
385
|
+
);
|
|
386
|
+
lines.push(`title: ${iss.title}`);
|
|
387
|
+
if (lbls) lines.push(`labels: ${lbls}`);
|
|
388
|
+
|
|
389
|
+
const body = iss.body ?? "";
|
|
390
|
+
if (body) {
|
|
391
|
+
const rendered = body.length > 500 ? snipText(body) : body;
|
|
392
|
+
lines.push("body:");
|
|
393
|
+
lines.push(rendered);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const comments = iss.comments ?? [];
|
|
397
|
+
if (comments.length) {
|
|
398
|
+
lines.push(`comments (${comments.length}):`);
|
|
399
|
+
if (mode === "thread") {
|
|
400
|
+
for (const c of comments) {
|
|
401
|
+
const who = c.author?.login ?? "?";
|
|
402
|
+
const when = shortDate(c.createdAt);
|
|
403
|
+
const body = c.body ?? "";
|
|
404
|
+
const rendered = body.length > 500 ? snipText(body) : body;
|
|
405
|
+
lines.push(` — ${who} · ${when}`);
|
|
406
|
+
for (const l of rendered.split("\n")) lines.push(` ${l}`);
|
|
407
|
+
}
|
|
408
|
+
} else {
|
|
409
|
+
for (const c of comments.slice(0, 10)) {
|
|
410
|
+
const who = c.author?.login ?? "?";
|
|
411
|
+
const snippet = cap(flat(c.body ?? ""), 100);
|
|
412
|
+
lines.push(` · ${who} · "${snippet}"`);
|
|
413
|
+
}
|
|
414
|
+
if (comments.length > 10) {
|
|
415
|
+
lines.push(` · (+${comments.length - 10} more — pass mode:"thread" to see all)`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return lines.join("\n");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ---------------------------------------------------------------------------
|
|
424
|
+
// Tool impls
|
|
425
|
+
// ---------------------------------------------------------------------------
|
|
426
|
+
|
|
427
|
+
const PR_JSON_FIELDS =
|
|
428
|
+
"number,title,state,author,createdAt,updatedAt,mergeable,reviewDecision,additions,deletions,changedFiles,baseRefName,headRefName,body,labels,reviews,comments,files,statusCheckRollup";
|
|
429
|
+
|
|
430
|
+
const ISSUE_JSON_FIELDS =
|
|
431
|
+
"number,title,state,author,createdAt,updatedAt,body,labels,comments";
|
|
432
|
+
|
|
433
|
+
// Guard against argv-injection: `gh` accepts many flag-like positionals
|
|
434
|
+
// (e.g. `--config path`), so we require the strict owner/name shape.
|
|
435
|
+
const REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
436
|
+
function validateRepo(repo: string, tool: string): string {
|
|
437
|
+
if (!REPO_RE.test(repo)) {
|
|
438
|
+
throw new Error(`${tool}: repo must match 'owner/name' (got '${repo}')`);
|
|
439
|
+
}
|
|
440
|
+
return repo;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export async function ashlrPr(input: { number: number; repo?: string; mode?: string }): Promise<string> {
|
|
444
|
+
const n = Number(input.number);
|
|
445
|
+
if (!Number.isFinite(n) || n <= 0) throw new Error("ashlr__pr: `number` must be a positive integer");
|
|
446
|
+
const mode = (input.mode ?? "summary") as "summary" | "full" | "thread";
|
|
447
|
+
if (!["summary", "full", "thread"].includes(mode)) {
|
|
448
|
+
throw new Error(`ashlr__pr: invalid mode '${mode}' (expected summary|full|thread)`);
|
|
449
|
+
}
|
|
450
|
+
const repo = validateRepo(input.repo ?? detectRepo(), "ashlr__pr");
|
|
451
|
+
|
|
452
|
+
const args = ["pr", "view", String(n), "--repo", repo, "--json", PR_JSON_FIELDS];
|
|
453
|
+
const rawJson = runGh(args);
|
|
454
|
+
const pr = safeParseGhJson<PRData>(rawJson, "ashlr__pr", "pr view");
|
|
455
|
+
|
|
456
|
+
let diff: string | undefined;
|
|
457
|
+
let rawTotal = rawJson.length;
|
|
458
|
+
if (mode === "full") {
|
|
459
|
+
diff = runGh(["pr", "diff", String(n), "--repo", repo]);
|
|
460
|
+
rawTotal += diff.length;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const compact = await renderPR(pr, mode, diff);
|
|
464
|
+
await recordSaving(rawTotal, compact.length, "ashlr__pr");
|
|
465
|
+
return compact;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export async function ashlrIssue(input: { number: number; repo?: string; mode?: string }): Promise<string> {
|
|
469
|
+
const n = Number(input.number);
|
|
470
|
+
if (!Number.isFinite(n) || n <= 0) throw new Error("ashlr__issue: `number` must be a positive integer");
|
|
471
|
+
const mode = (input.mode ?? "summary") as "summary" | "thread";
|
|
472
|
+
if (!["summary", "thread"].includes(mode)) {
|
|
473
|
+
throw new Error(`ashlr__issue: invalid mode '${mode}' (expected summary|thread)`);
|
|
474
|
+
}
|
|
475
|
+
const repo = validateRepo(input.repo ?? detectRepo(), "ashlr__issue");
|
|
476
|
+
const args = ["issue", "view", String(n), "--repo", repo, "--json", ISSUE_JSON_FIELDS];
|
|
477
|
+
const rawJson = runGh(args);
|
|
478
|
+
const iss = safeParseGhJson<IssueData>(rawJson, "ashlr__issue", "issue view");
|
|
479
|
+
const compact = renderIssue(iss, mode);
|
|
480
|
+
await recordSaving(rawJson.length, compact.length, "ashlr__issue");
|
|
481
|
+
return compact;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ---------------------------------------------------------------------------
|
|
485
|
+
// Write-op helpers (shared across pr_comment / pr_approve / issue_* tools).
|
|
486
|
+
// ---------------------------------------------------------------------------
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Guard every write op: when ASHLR_REQUIRE_GH_CONFIRM is "1", the caller must
|
|
490
|
+
* pass `{ confirm: true }` explicitly or we refuse. Future-proofing hatch for
|
|
491
|
+
* environments that want a second-factor (CI, shared terminals, etc).
|
|
492
|
+
*/
|
|
493
|
+
function enforceConfirm(tool: ToolName, confirm: unknown): void {
|
|
494
|
+
if (process.env.ASHLR_REQUIRE_GH_CONFIRM === "1" && confirm !== true) {
|
|
495
|
+
throw new Error(
|
|
496
|
+
`${tool}: write ops require explicit confirmation when ASHLR_REQUIRE_GH_CONFIRM=1. Pass { confirm: true }.`,
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function requireString(tool: ToolName, field: string, value: unknown): string {
|
|
502
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
503
|
+
throw new Error(`${tool}: \`${field}\` is required and must be a non-empty string`);
|
|
504
|
+
}
|
|
505
|
+
return value;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/** Resolve `pr: number | "current"` to a concrete PR number using gh. */
|
|
509
|
+
function resolvePrNumber(input: number | string, repo: string, tool: ToolName): number {
|
|
510
|
+
if (typeof input === "number") {
|
|
511
|
+
if (!Number.isFinite(input) || input <= 0) {
|
|
512
|
+
throw new Error(`${tool}: \`pr\` must be a positive integer or "current"`);
|
|
513
|
+
}
|
|
514
|
+
return input;
|
|
515
|
+
}
|
|
516
|
+
if (input === "current") {
|
|
517
|
+
// gh pr view with no argument uses the current branch; json omits branch ambiguity.
|
|
518
|
+
const out = runGh(["pr", "view", "--repo", repo, "--json", "number,author"]);
|
|
519
|
+
const parsed = safeParseGhJson<{ number?: number; author?: { login?: string } }>(
|
|
520
|
+
out,
|
|
521
|
+
tool,
|
|
522
|
+
"pr view (current)",
|
|
523
|
+
);
|
|
524
|
+
if (typeof parsed.number !== "number") {
|
|
525
|
+
throw new Error(`${tool}: could not resolve current branch's PR (is there one open?)`);
|
|
526
|
+
}
|
|
527
|
+
return parsed.number;
|
|
528
|
+
}
|
|
529
|
+
throw new Error(`${tool}: \`pr\` must be a positive integer or "current"`);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Resolve the current-viewer's login, used for self-approval check. */
|
|
533
|
+
function currentViewerLogin(tool: ToolName): string | undefined {
|
|
534
|
+
try {
|
|
535
|
+
const out = runGh(["api", "user", "--jq", ".login"]);
|
|
536
|
+
const login = out.trim();
|
|
537
|
+
return login || undefined;
|
|
538
|
+
} catch (err) {
|
|
539
|
+
// Non-fatal: we'd rather skip the self-approval check than fail the call
|
|
540
|
+
// if `gh api user` misbehaves. Caller can still hit the github-side guard.
|
|
541
|
+
void logEvent("tool_error", {
|
|
542
|
+
tool,
|
|
543
|
+
reason: `viewer lookup failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
544
|
+
});
|
|
545
|
+
return undefined;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** Fetch `author.login` for a PR — used only for the self-approval guard. */
|
|
550
|
+
function prAuthorLogin(prNum: number, repo: string, tool: ToolName): string | undefined {
|
|
551
|
+
try {
|
|
552
|
+
const out = runGh(["pr", "view", String(prNum), "--repo", repo, "--json", "author"]);
|
|
553
|
+
const parsed = safeParseGhJson<{ author?: { login?: string } }>(out, tool, "pr view (author)");
|
|
554
|
+
return parsed.author?.login;
|
|
555
|
+
} catch {
|
|
556
|
+
return undefined;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// ---------------------------------------------------------------------------
|
|
561
|
+
// Write-op 1: post a comment on a PR
|
|
562
|
+
// ---------------------------------------------------------------------------
|
|
563
|
+
|
|
564
|
+
export async function ashlrPrComment(input: {
|
|
565
|
+
pr: number | string;
|
|
566
|
+
body: string;
|
|
567
|
+
repo?: string;
|
|
568
|
+
confirm?: boolean;
|
|
569
|
+
}): Promise<string> {
|
|
570
|
+
enforceConfirm("ashlr__pr_comment", input.confirm);
|
|
571
|
+
const body = requireString("ashlr__pr_comment", "body", input.body);
|
|
572
|
+
const repo = validateRepo(input.repo ?? detectRepo(), "ashlr__pr_comment");
|
|
573
|
+
const prNum = resolvePrNumber(input.pr, repo, "ashlr__pr_comment");
|
|
574
|
+
|
|
575
|
+
// `gh pr comment <n> --body <body> --repo <repo>` prints the new comment URL on success.
|
|
576
|
+
const out = runGh(["pr", "comment", String(prNum), "--body", body, "--repo", repo]);
|
|
577
|
+
const url = out.trim().split("\n").find((l) => l.startsWith("http")) ?? out.trim();
|
|
578
|
+
const compact = `commented on PR #${prNum} (${repo}) · ${url}`;
|
|
579
|
+
await recordSaving(out.length, compact.length, "ashlr__pr_comment");
|
|
580
|
+
return compact;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// ---------------------------------------------------------------------------
|
|
584
|
+
// Write-op 2: approve a PR (with optional comment body)
|
|
585
|
+
// ---------------------------------------------------------------------------
|
|
586
|
+
|
|
587
|
+
export async function ashlrPrApprove(input: {
|
|
588
|
+
pr: number | string;
|
|
589
|
+
body?: string;
|
|
590
|
+
repo?: string;
|
|
591
|
+
confirm?: boolean;
|
|
592
|
+
}): Promise<string> {
|
|
593
|
+
enforceConfirm("ashlr__pr_approve", input.confirm);
|
|
594
|
+
const repo = validateRepo(input.repo ?? detectRepo(), "ashlr__pr_approve");
|
|
595
|
+
const prNum = resolvePrNumber(input.pr, repo, "ashlr__pr_approve");
|
|
596
|
+
|
|
597
|
+
// Self-approval guard: only enforced when we can cheaply determine both
|
|
598
|
+
// viewer and author. GitHub will also reject this server-side, but a
|
|
599
|
+
// caller-side error is faster + clearer.
|
|
600
|
+
const viewer = currentViewerLogin("ashlr__pr_approve");
|
|
601
|
+
const author = prAuthorLogin(prNum, repo, "ashlr__pr_approve");
|
|
602
|
+
if (viewer && author && viewer === author) {
|
|
603
|
+
throw new Error(
|
|
604
|
+
`ashlr__pr_approve: cannot approve your own PR (#${prNum} authored by ${author})`,
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const args = ["pr", "review", String(prNum), "--approve", "--repo", repo];
|
|
609
|
+
if (typeof input.body === "string" && input.body.trim() !== "") {
|
|
610
|
+
args.push("--body", input.body);
|
|
611
|
+
}
|
|
612
|
+
const out = runGh(args);
|
|
613
|
+
const compact = `approved PR #${prNum} (${repo})${input.body ? ' · "' + cap(flat(input.body), 80) + '"' : ""}`;
|
|
614
|
+
await recordSaving(out.length || compact.length, compact.length, "ashlr__pr_approve");
|
|
615
|
+
return compact;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// ---------------------------------------------------------------------------
|
|
619
|
+
// Write-op 3: create a new issue
|
|
620
|
+
// ---------------------------------------------------------------------------
|
|
621
|
+
|
|
622
|
+
export async function ashlrIssueCreate(input: {
|
|
623
|
+
title: string;
|
|
624
|
+
body: string;
|
|
625
|
+
labels?: string[];
|
|
626
|
+
assignees?: string[];
|
|
627
|
+
repo?: string;
|
|
628
|
+
confirm?: boolean;
|
|
629
|
+
}): Promise<string> {
|
|
630
|
+
enforceConfirm("ashlr__issue_create", input.confirm);
|
|
631
|
+
const title = requireString("ashlr__issue_create", "title", input.title);
|
|
632
|
+
const body = requireString("ashlr__issue_create", "body", input.body);
|
|
633
|
+
const repo = validateRepo(input.repo ?? detectRepo(), "ashlr__issue_create");
|
|
634
|
+
|
|
635
|
+
const args = ["issue", "create", "--title", title, "--body", body, "--repo", repo];
|
|
636
|
+
if (Array.isArray(input.labels) && input.labels.length) {
|
|
637
|
+
args.push("--label", input.labels.join(","));
|
|
638
|
+
}
|
|
639
|
+
if (Array.isArray(input.assignees) && input.assignees.length) {
|
|
640
|
+
args.push("--assignee", input.assignees.join(","));
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// `gh issue create` prints the new issue URL on success.
|
|
644
|
+
const out = runGh(args);
|
|
645
|
+
const url = out.trim().split("\n").find((l) => l.startsWith("http")) ?? out.trim();
|
|
646
|
+
const numMatch = url.match(/\/issues\/(\d+)/);
|
|
647
|
+
const num = numMatch ? Number(numMatch[1]) : undefined;
|
|
648
|
+
const compact = `created issue${num ? " #" + num : ""} (${repo}) · ${url}`;
|
|
649
|
+
await recordSaving(out.length, compact.length, "ashlr__issue_create");
|
|
650
|
+
return compact;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// ---------------------------------------------------------------------------
|
|
654
|
+
// Write-op 4: close an issue (optionally comment + reason)
|
|
655
|
+
// ---------------------------------------------------------------------------
|
|
656
|
+
|
|
657
|
+
export async function ashlrIssueClose(input: {
|
|
658
|
+
issue: number;
|
|
659
|
+
comment?: string;
|
|
660
|
+
reason?: string;
|
|
661
|
+
repo?: string;
|
|
662
|
+
confirm?: boolean;
|
|
663
|
+
}): Promise<string> {
|
|
664
|
+
enforceConfirm("ashlr__issue_close", input.confirm);
|
|
665
|
+
const n = Number(input.issue);
|
|
666
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
667
|
+
throw new Error("ashlr__issue_close: `issue` must be a positive integer");
|
|
668
|
+
}
|
|
669
|
+
const repo = validateRepo(input.repo ?? detectRepo(), "ashlr__issue_close");
|
|
670
|
+
|
|
671
|
+
const args = ["issue", "close", String(n), "--repo", repo];
|
|
672
|
+
if (typeof input.comment === "string" && input.comment.trim() !== "") {
|
|
673
|
+
args.push("--comment", input.comment);
|
|
674
|
+
}
|
|
675
|
+
if (typeof input.reason === "string" && input.reason.trim() !== "") {
|
|
676
|
+
if (!["completed", "not_planned"].includes(input.reason)) {
|
|
677
|
+
throw new Error(
|
|
678
|
+
`ashlr__issue_close: invalid reason '${input.reason}' (expected completed|not_planned)`,
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
args.push("--reason", input.reason);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const out = runGh(args);
|
|
685
|
+
const compact = `closed issue #${n} (${repo})${input.reason ? " · " + input.reason : ""}${input.comment ? ' · "' + cap(flat(input.comment), 80) + '"' : ""}`;
|
|
686
|
+
await recordSaving(out.length || compact.length, compact.length, "ashlr__issue_close");
|
|
687
|
+
return compact;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// ---------------------------------------------------------------------------
|
|
691
|
+
// MCP wiring
|
|
692
|
+
// ---------------------------------------------------------------------------
|
|
693
|
+
|
|
694
|
+
const server = new Server(
|
|
695
|
+
{ name: "ashlr-github", version: "0.1.0" },
|
|
696
|
+
{ capabilities: { tools: {} } },
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
700
|
+
tools: [
|
|
701
|
+
{
|
|
702
|
+
name: "ashlr__pr",
|
|
703
|
+
description:
|
|
704
|
+
"Fetch a GitHub PR and return a compact review-ready summary (header, reviews, unresolved comments, status checks). Read-only — never approves, comments, or merges. Saves 60-90% of the tokens a raw `gh pr view` dump would cost.",
|
|
705
|
+
inputSchema: {
|
|
706
|
+
type: "object",
|
|
707
|
+
properties: {
|
|
708
|
+
number: { type: "number", description: "PR number" },
|
|
709
|
+
repo: { type: "string", description: "owner/repo (default: auto-detect from cwd git remote)" },
|
|
710
|
+
mode: { type: "string", description: "'summary' (default: decisions + unresolved + checks) | 'full' (adds diff summary) | 'thread' (just comments)" },
|
|
711
|
+
},
|
|
712
|
+
required: ["number"],
|
|
713
|
+
},
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
name: "ashlr__issue",
|
|
717
|
+
description:
|
|
718
|
+
"Fetch a GitHub issue and return a compact header + body + comment list. In 'thread' mode, each comment is rendered with snipCompact on > 500 char bodies. Read-only.",
|
|
719
|
+
inputSchema: {
|
|
720
|
+
type: "object",
|
|
721
|
+
properties: {
|
|
722
|
+
number: { type: "number", description: "Issue number" },
|
|
723
|
+
repo: { type: "string", description: "owner/repo (default: auto-detect from cwd git remote)" },
|
|
724
|
+
mode: { type: "string", description: "'summary' (default) | 'thread' (full comments with snipCompact on each)" },
|
|
725
|
+
},
|
|
726
|
+
required: ["number"],
|
|
727
|
+
},
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
name: "ashlr__pr_comment",
|
|
731
|
+
description:
|
|
732
|
+
"Post a comment on a GitHub PR. Pass pr:\"current\" to target the PR for the checked-out branch. Returns the new comment URL.",
|
|
733
|
+
inputSchema: {
|
|
734
|
+
type: "object",
|
|
735
|
+
properties: {
|
|
736
|
+
pr: { description: "PR number or the string \"current\" to target the current branch's PR", oneOf: [{ type: "number" }, { type: "string", enum: ["current"] }] },
|
|
737
|
+
body: { type: "string", description: "Comment body (markdown supported)" },
|
|
738
|
+
repo: { type: "string", description: "owner/repo (default: auto-detect from cwd git remote)" },
|
|
739
|
+
confirm: { type: "boolean", description: "Required when ASHLR_REQUIRE_GH_CONFIRM=1" },
|
|
740
|
+
},
|
|
741
|
+
required: ["pr", "body"],
|
|
742
|
+
},
|
|
743
|
+
},
|
|
744
|
+
{
|
|
745
|
+
name: "ashlr__pr_approve",
|
|
746
|
+
description:
|
|
747
|
+
"Approve a GitHub PR with an optional review body. Refuses to approve your own PR. Pass pr:\"current\" to target the PR for the checked-out branch.",
|
|
748
|
+
inputSchema: {
|
|
749
|
+
type: "object",
|
|
750
|
+
properties: {
|
|
751
|
+
pr: { description: "PR number or the string \"current\" to target the current branch's PR", oneOf: [{ type: "number" }, { type: "string", enum: ["current"] }] },
|
|
752
|
+
body: { type: "string", description: "Optional review comment body" },
|
|
753
|
+
repo: { type: "string", description: "owner/repo (default: auto-detect from cwd git remote)" },
|
|
754
|
+
confirm: { type: "boolean", description: "Required when ASHLR_REQUIRE_GH_CONFIRM=1" },
|
|
755
|
+
},
|
|
756
|
+
required: ["pr"],
|
|
757
|
+
},
|
|
758
|
+
},
|
|
759
|
+
{
|
|
760
|
+
name: "ashlr__issue_create",
|
|
761
|
+
description:
|
|
762
|
+
"Create a new GitHub issue with title + body and optional labels/assignees. Returns the new issue number and URL.",
|
|
763
|
+
inputSchema: {
|
|
764
|
+
type: "object",
|
|
765
|
+
properties: {
|
|
766
|
+
title: { type: "string", description: "Issue title" },
|
|
767
|
+
body: { type: "string", description: "Issue body (markdown supported)" },
|
|
768
|
+
labels: { type: "array", items: { type: "string" }, description: "Optional label names to apply" },
|
|
769
|
+
assignees: { type: "array", items: { type: "string" }, description: "Optional GitHub logins to assign" },
|
|
770
|
+
repo: { type: "string", description: "owner/repo (default: auto-detect from cwd git remote)" },
|
|
771
|
+
confirm: { type: "boolean", description: "Required when ASHLR_REQUIRE_GH_CONFIRM=1" },
|
|
772
|
+
},
|
|
773
|
+
required: ["title", "body"],
|
|
774
|
+
},
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
name: "ashlr__issue_close",
|
|
778
|
+
description:
|
|
779
|
+
"Close a GitHub issue with an optional closing comment and reason (completed|not_planned).",
|
|
780
|
+
inputSchema: {
|
|
781
|
+
type: "object",
|
|
782
|
+
properties: {
|
|
783
|
+
issue: { type: "number", description: "Issue number" },
|
|
784
|
+
comment: { type: "string", description: "Optional closing comment" },
|
|
785
|
+
reason: { type: "string", enum: ["completed", "not_planned"], description: "Optional close reason" },
|
|
786
|
+
repo: { type: "string", description: "owner/repo (default: auto-detect from cwd git remote)" },
|
|
787
|
+
confirm: { type: "boolean", description: "Required when ASHLR_REQUIRE_GH_CONFIRM=1" },
|
|
788
|
+
},
|
|
789
|
+
required: ["issue"],
|
|
790
|
+
},
|
|
791
|
+
},
|
|
792
|
+
],
|
|
793
|
+
}));
|
|
794
|
+
|
|
795
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
796
|
+
const { name, arguments: args } = req.params;
|
|
797
|
+
try {
|
|
798
|
+
switch (name) {
|
|
799
|
+
case "ashlr__pr": {
|
|
800
|
+
const text = await ashlrPr(args as { number: number; repo?: string; mode?: string });
|
|
801
|
+
return { content: [{ type: "text", text }] };
|
|
802
|
+
}
|
|
803
|
+
case "ashlr__issue": {
|
|
804
|
+
const text = await ashlrIssue(args as { number: number; repo?: string; mode?: string });
|
|
805
|
+
return { content: [{ type: "text", text }] };
|
|
806
|
+
}
|
|
807
|
+
case "ashlr__pr_comment": {
|
|
808
|
+
const text = await ashlrPrComment(
|
|
809
|
+
args as { pr: number | string; body: string; repo?: string; confirm?: boolean },
|
|
810
|
+
);
|
|
811
|
+
return { content: [{ type: "text", text }] };
|
|
812
|
+
}
|
|
813
|
+
case "ashlr__pr_approve": {
|
|
814
|
+
const text = await ashlrPrApprove(
|
|
815
|
+
args as { pr: number | string; body?: string; repo?: string; confirm?: boolean },
|
|
816
|
+
);
|
|
817
|
+
return { content: [{ type: "text", text }] };
|
|
818
|
+
}
|
|
819
|
+
case "ashlr__issue_create": {
|
|
820
|
+
const text = await ashlrIssueCreate(
|
|
821
|
+
args as {
|
|
822
|
+
title: string;
|
|
823
|
+
body: string;
|
|
824
|
+
labels?: string[];
|
|
825
|
+
assignees?: string[];
|
|
826
|
+
repo?: string;
|
|
827
|
+
confirm?: boolean;
|
|
828
|
+
},
|
|
829
|
+
);
|
|
830
|
+
return { content: [{ type: "text", text }] };
|
|
831
|
+
}
|
|
832
|
+
case "ashlr__issue_close": {
|
|
833
|
+
const text = await ashlrIssueClose(
|
|
834
|
+
args as {
|
|
835
|
+
issue: number;
|
|
836
|
+
comment?: string;
|
|
837
|
+
reason?: string;
|
|
838
|
+
repo?: string;
|
|
839
|
+
confirm?: boolean;
|
|
840
|
+
},
|
|
841
|
+
);
|
|
842
|
+
return { content: [{ type: "text", text }] };
|
|
843
|
+
}
|
|
844
|
+
default:
|
|
845
|
+
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
846
|
+
}
|
|
847
|
+
} catch (err) {
|
|
848
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
849
|
+
return { content: [{ type: "text", text: `ashlr error: ${message}` }], isError: true };
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
if (import.meta.main) {
|
|
854
|
+
const transport = new StdioServerTransport();
|
|
855
|
+
await server.connect(transport);
|
|
856
|
+
}
|