@agent-native/core 0.43.0 → 0.44.1
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/dist/chat-threads/store.d.ts.map +1 -1
- package/dist/chat-threads/store.js +71 -10
- package/dist/chat-threads/store.js.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.js +1 -1
- package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
- package/dist/cli/recap.d.ts +23 -0
- package/dist/cli/recap.d.ts.map +1 -1
- package/dist/cli/recap.js +177 -13
- package/dist/cli/recap.js.map +1 -1
- package/dist/cli/skills.d.ts +3 -3
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +67 -20
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +76 -18
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/blocks/index.d.ts +0 -2
- package/dist/client/blocks/index.d.ts.map +1 -1
- package/dist/client/blocks/index.js +0 -2
- package/dist/client/blocks/index.js.map +1 -1
- package/dist/client/blocks/library/AnnotatedCodeBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/AnnotatedCodeBlock.js +22 -9
- package/dist/client/blocks/library/AnnotatedCodeBlock.js.map +1 -1
- package/dist/client/blocks/library/ApiEndpointBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/ApiEndpointBlock.js +113 -13
- package/dist/client/blocks/library/ApiEndpointBlock.js.map +1 -1
- package/dist/client/blocks/library/DiffBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/DiffBlock.js +63 -35
- package/dist/client/blocks/library/DiffBlock.js.map +1 -1
- package/dist/client/blocks/library/FileTreeBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/FileTreeBlock.js +4 -0
- package/dist/client/blocks/library/FileTreeBlock.js.map +1 -1
- package/dist/client/blocks/library/JsonExplorerBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/JsonExplorerBlock.js +1 -1
- package/dist/client/blocks/library/JsonExplorerBlock.js.map +1 -1
- package/dist/client/blocks/library/MermaidBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/MermaidBlock.js +22 -3
- package/dist/client/blocks/library/MermaidBlock.js.map +1 -1
- package/dist/client/blocks/library/annotation-rail.d.ts +85 -19
- package/dist/client/blocks/library/annotation-rail.d.ts.map +1 -1
- package/dist/client/blocks/library/annotation-rail.js +149 -27
- package/dist/client/blocks/library/annotation-rail.js.map +1 -1
- package/dist/client/blocks/library/code-tabs.js +1 -1
- package/dist/client/blocks/library/code-tabs.js.map +1 -1
- package/dist/client/blocks/library/diagram.d.ts +17 -0
- package/dist/client/blocks/library/diagram.d.ts.map +1 -1
- package/dist/client/blocks/library/diagram.js +47 -2
- package/dist/client/blocks/library/diagram.js.map +1 -1
- package/dist/client/blocks/library/server-specs.d.ts.map +1 -1
- package/dist/client/blocks/library/server-specs.js +0 -10
- package/dist/client/blocks/library/server-specs.js.map +1 -1
- package/dist/client/blocks/library/specs.d.ts.map +1 -1
- package/dist/client/blocks/library/specs.js +0 -2
- package/dist/client/blocks/library/specs.js.map +1 -1
- package/dist/client/blocks/library/wireframe.config.d.ts.map +1 -1
- package/dist/client/blocks/library/wireframe.config.js +19 -2
- package/dist/client/blocks/library/wireframe.config.js.map +1 -1
- package/dist/client/blocks/mdx.d.ts.map +1 -1
- package/dist/client/blocks/mdx.js +11 -0
- package/dist/client/blocks/mdx.js.map +1 -1
- package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/client/composer/TiptapComposer.js +13 -8
- package/dist/client/composer/TiptapComposer.js.map +1 -1
- package/dist/client/composer/pasted-text.d.ts +25 -0
- package/dist/client/composer/pasted-text.d.ts.map +1 -1
- package/dist/client/composer/pasted-text.js +86 -4
- package/dist/client/composer/pasted-text.js.map +1 -1
- package/dist/client/rich-markdown-editor/DragHandle.d.ts.map +1 -1
- package/dist/client/rich-markdown-editor/DragHandle.js +35 -72
- package/dist/client/rich-markdown-editor/DragHandle.js.map +1 -1
- package/dist/client/rich-markdown-editor/RegistryBlockNode.d.ts.map +1 -1
- package/dist/client/rich-markdown-editor/RegistryBlockNode.js +1 -1
- package/dist/client/rich-markdown-editor/RegistryBlockNode.js.map +1 -1
- package/dist/client/rich-markdown-editor/SharedRichEditor.d.ts +9 -1
- package/dist/client/rich-markdown-editor/SharedRichEditor.d.ts.map +1 -1
- package/dist/client/rich-markdown-editor/SharedRichEditor.js +3 -1
- package/dist/client/rich-markdown-editor/SharedRichEditor.js.map +1 -1
- package/dist/client/rich-markdown-editor/extensions.d.ts +13 -1
- package/dist/client/rich-markdown-editor/extensions.d.ts.map +1 -1
- package/dist/client/rich-markdown-editor/extensions.js +4 -2
- package/dist/client/rich-markdown-editor/extensions.js.map +1 -1
- package/dist/client/rich-markdown-editor/useCollabReconcile.d.ts.map +1 -1
- package/dist/client/rich-markdown-editor/useCollabReconcile.js +11 -1
- package/dist/client/rich-markdown-editor/useCollabReconcile.js.map +1 -1
- package/dist/db/migrations.d.ts +10 -0
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/migrations.js +32 -0
- package/dist/db/migrations.js.map +1 -1
- package/dist/server/og-fonts-data.d.ts +3 -0
- package/dist/server/og-fonts-data.d.ts.map +1 -0
- package/dist/server/og-fonts-data.js +9 -0
- package/dist/server/og-fonts-data.js.map +1 -0
- package/dist/server/og-fonts.d.ts +10 -0
- package/dist/server/og-fonts.d.ts.map +1 -0
- package/dist/server/og-fonts.js +58 -0
- package/dist/server/og-fonts.js.map +1 -0
- package/dist/server/poll.d.ts.map +1 -1
- package/dist/server/poll.js +30 -14
- package/dist/server/poll.js.map +1 -1
- package/dist/server/social-og-image.d.ts.map +1 -1
- package/dist/server/social-og-image.js +16 -5
- package/dist/server/social-og-image.js.map +1 -1
- package/dist/styles/blocks.css +121 -2
- package/dist/templates/default/.agents/skills/storing-data/SKILL.md +2 -0
- package/dist/templates/workspace-core/.agents/skills/performance/SKILL.md +141 -0
- package/dist/templates/workspace-core/.agents/skills/storing-data/SKILL.md +2 -0
- package/dist/usage/store.d.ts +12 -0
- package/dist/usage/store.d.ts.map +1 -1
- package/dist/usage/store.js +35 -5
- package/dist/usage/store.js.map +1 -1
- package/package.json +1 -1
- package/src/templates/default/.agents/skills/storing-data/SKILL.md +2 -0
- package/src/templates/workspace-core/.agents/skills/performance/SKILL.md +141 -0
- package/src/templates/workspace-core/.agents/skills/storing-data/SKILL.md +2 -0
- package/dist/client/blocks/library/decision.config.d.ts +0 -37
- package/dist/client/blocks/library/decision.config.d.ts.map +0 -1
- package/dist/client/blocks/library/decision.config.js +0 -32
- package/dist/client/blocks/library/decision.config.js.map +0 -1
- package/dist/client/blocks/library/decision.d.ts +0 -19
- package/dist/client/blocks/library/decision.d.ts.map +0 -1
- package/dist/client/blocks/library/decision.js +0 -119
- package/dist/client/blocks/library/decision.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pr-visual-recap-workflow.js","sourceRoot":"","sources":["../../src/cli/pr-visual-recap-workflow.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,CAAC,MAAM,4BAA4B,GACvC,s+mBAAs+mB,CAAC","sourcesContent":["/**\n * Bundled copy of .github/workflows/pr-visual-recap.yml so the CLI can write the\n * PR Visual Recap workflow into a user repo via\n * `agent-native skills add visual-plan --with-github-action`.\n *\n * AUTO-GENERATED — keep byte-identical with the source workflow. A sync test in\n * recap.spec.ts fails if these drift. Regenerate from the YAML with the snippet\n * in recap.spec.ts.\n */\n\nexport const PR_VISUAL_RECAP_WORKFLOW_YML =\n 'name: PR Visual Recap\\n\\n# Turns every PR into a \"visual code review\" — a reverse plan — by letting a real\\n# coding agent RUN THE REPO\\'S visual-recap SKILL against the diff. The agent\\n# (Claude Code by default, or Codex) reads the skill, reasons over the change,\\n# publishes an Agent-Native Plan via the plan MCP tools, and writes the plan URL\\n# to recap-url.txt. The workflow then screenshots that plan in headless Chrome,\\n# uploads the PNG to the plan app\\'s signed public image route, and upserts ONE\\n# sticky PR comment with the inline screenshot + the interactive link.\\n#\\n# Design notes:\\n# - Plain `pull_request` (NOT `pull_request_target`) so fork code can never see\\n# the publish/agent secrets. Fork PRs are a silent no-op.\\n# - The `gate` job is a cheap switch: drafts, forks, bot authors, and the\\n# missing-secret case short-circuit with NO comment and NO compute. Merging\\n# this workflow before the secrets exist is a safe no-op.\\n# - The recap is INFORMATIONAL ONLY. It is not a required check and failures\\n# surface as an explanatory sticky comment, never a red X on unrelated code.\\n# - Backend is selectable with the `VISUAL_RECAP_AGENT` repo variable\\n# (claude | codex; default claude). Model and reasoning depth are tunable with\\n# `VISUAL_RECAP_MODEL` (e.g. gpt-5.5) and `VISUAL_RECAP_REASONING`\\n# (none|minimal|low|medium|high|xhigh; Codex only). The CLI invocation is\\n# auto-detected: local source inside this monorepo, the published\\n# @agent-native/core elsewhere — no repo variable needed.\\n# - Only two secrets are required: PLAN_RECAP_TOKEN (publish) and the chosen\\n# backend\\'s API key. PLAN_RECAP_APP_URL defaults to the hosted plan app.\\n# - Nothing here is deterministic: the skill\\'s instructions drive the recap.\\n\\non:\\n # Run on PRs into any base branch — the generated workflow ships to repos whose\\n # default branch may not be `main`. The gate job below still no-ops drafts,\\n # forks, bots, and the missing-secret case, so this stays cheap.\\n pull_request:\\n types: [opened, synchronize, reopened, ready_for_review]\\n\\npermissions:\\n contents: read\\n issues: write\\n pull-requests: write\\n\\nconcurrency:\\n group: pr-visual-recap-${{ github.event.pull_request.number }}\\n cancel-in-progress: true\\n\\nenv:\\n VISUAL_RECAP_AGENT: ${{ vars.VISUAL_RECAP_AGENT || \\'claude\\' }}\\n\\njobs:\\n # --------------------------------------------------------------------------\\n # Cheap gate: decide whether to do any work at all. Sets run=false (silent\\n # no-op) for drafts, forks, bot authors, or when the publish secret / the\\n # chosen backend\\'s API key is absent.\\n # --------------------------------------------------------------------------\\n gate:\\n name: Gate\\n runs-on: ubuntu-latest\\n outputs:\\n run: ${{ steps.decide.outputs.run }}\\n steps:\\n - id: decide\\n uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7\\n env:\\n # Presence-only signals — we never expose the secret VALUES to the gate.\\n # PLAN_RECAP_APP_URL defaults to the hosted app, so only the token is required.\\n HAS_PLAN: ${{ secrets.PLAN_RECAP_TOKEN != \\'\\' }}\\n HAS_ANTHROPIC: ${{ secrets.ANTHROPIC_API_KEY != \\'\\' }}\\n HAS_OPENAI: ${{ secrets.OPENAI_API_KEY != \\'\\' }}\\n AGENT: ${{ env.VISUAL_RECAP_AGENT }}\\n with:\\n script: |\\n const pr = context.payload.pull_request;\\n const reasons = [];\\n\\n if (!pr) reasons.push(\\'no pull_request payload\\');\\n if (pr && pr.draft) reasons.push(\\'draft PR\\');\\n\\n // Fork PRs: head repo differs from this repo. Plain pull_request runs\\n // fork code with NO secrets, so publishing would fail anyway — skip.\\n const headRepo = pr && pr.head && pr.head.repo && pr.head.repo.full_name;\\n if (pr && headRepo && headRepo !== process.env.GITHUB_REPOSITORY) {\\n reasons.push(`fork PR (${headRepo})`);\\n }\\n\\n // Skip noisy automated authors.\\n const login = (pr && pr.user && pr.user.login || \\'\\').toLowerCase();\\n const botAuthors = [\\'dependabot[bot]\\', \\'dependabot\\', \\'renovate[bot]\\', \\'renovate\\'];\\n if (botAuthors.includes(login)) reasons.push(`bot author (${login})`);\\n if (pr && pr.user && pr.user.type === \\'Bot\\') reasons.push(\\'bot author (type=Bot)\\');\\n\\n // Publish secret must be configured — otherwise this is a no-op so the\\n // workflow can be merged before secrets exist.\\n if (process.env.HAS_PLAN !== \\'true\\') reasons.push(\\'PLAN_RECAP_TOKEN not configured\\');\\n\\n // The chosen backend\\'s API key must be present.\\n const agent = (process.env.AGENT || \\'claude\\').toLowerCase();\\n if (agent === \\'codex\\') {\\n if (process.env.HAS_OPENAI !== \\'true\\') reasons.push(\\'OPENAI_API_KEY not configured (codex backend)\\');\\n } else {\\n if (process.env.HAS_ANTHROPIC !== \\'true\\') reasons.push(\\'ANTHROPIC_API_KEY not configured (claude backend)\\');\\n }\\n\\n const run = reasons.length === 0;\\n core.setOutput(\\'run\\', run ? \\'true\\' : \\'false\\');\\n core.info(run ? `Visual recap will run (${agent}).` : `Visual recap skipped: ${reasons.join(\\'; \\')}`);\\n\\n # --------------------------------------------------------------------------\\n # Recap: collect the diff, let the agent run the skill + publish, screenshot\\n # the result, and upsert the sticky comment.\\n # --------------------------------------------------------------------------\\n recap:\\n name: Generate visual recap\\n needs: gate\\n if: needs.gate.outputs.run == \\'true\\'\\n runs-on: ubuntu-latest\\n env:\\n PLAN_RECAP_APP_URL: ${{ secrets.PLAN_RECAP_APP_URL || \\'https://plan.agent-native.com\\' }}\\n PLAN_RECAP_TOKEN: ${{ secrets.PLAN_RECAP_TOKEN }}\\n GH_TOKEN: ${{ github.token }}\\n PR_NUMBER: ${{ github.event.pull_request.number }}\\n HEAD_SHA: ${{ github.event.pull_request.head.sha }}\\n VISUAL_RECAP_MODEL: ${{ vars.VISUAL_RECAP_MODEL }}\\n VISUAL_RECAP_REASONING: ${{ vars.VISUAL_RECAP_REASONING }}\\n steps:\\n - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1\\n with:\\n fetch-depth: 0\\n\\n # Resolve the CLI invocation once: dogfood local source inside this\\n # monorepo, otherwise the published package. No repo variable needed. The\\n # pnpm setup/install steps below run ONLY for the local-source path, so the\\n # generated workflow works out-of-box in npm/yarn consumer repos (which\\n # have no pnpm-lock.yaml) by falling back to `npx @agent-native/core`.\\n - name: Resolve recap CLI\\n id: cli\\n run: |\\n if [ -f packages/core/src/cli/index.ts ]; then\\n echo \"RECAP_CLI=pnpm exec tsx packages/core/src/cli/index.ts\" >> \"$GITHUB_ENV\"\\n echo \"local=true\" >> \"$GITHUB_OUTPUT\"\\n else\\n echo \"RECAP_CLI=npx -y @agent-native/core@latest\" >> \"$GITHUB_ENV\"\\n echo \"local=false\" >> \"$GITHUB_OUTPUT\"\\n fi\\n\\n - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0\\n if: steps.cli.outputs.local == \\'true\\'\\n\\n - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0\\n with:\\n node-version: \"22\"\\n cache: ${{ steps.cli.outputs.local == \\'true\\' && \\'pnpm\\' || \\'\\' }}\\n\\n - name: Install workspace (local source only)\\n if: steps.cli.outputs.local == \\'true\\'\\n run: pnpm install --frozen-lockfile\\n\\n # Collect a BOUNDED diff between the PR base and head. We exclude lockfiles,\\n # build output, and snapshots (noise), and cap the byte size — over the cap\\n # we set `huge=true` so the agent is told to produce a summarized recap.\\n - name: Collect bounded diff\\n id: diff\\n env:\\n BASE_SHA: ${{ github.event.pull_request.base.sha }}\\n run: |\\n set -euo pipefail\\n git diff --no-color \"$BASE_SHA\"...\"$HEAD_SHA\" -- \\\\\\n . \\\\\\n \\':(exclude)pnpm-lock.yaml\\' \\\\\\n \\':(exclude)**/dist/**\\' \\\\\\n \\':(exclude)**/*.snap\\' \\\\\\n \\':(exclude)**/*.lock\\' \\\\\\n > recap.diff || true\\n git diff --stat --no-color \"$BASE_SHA\"...\"$HEAD_SHA\" -- \\\\\\n . \\\\\\n \\':(exclude)pnpm-lock.yaml\\' \\\\\\n \\':(exclude)**/dist/**\\' \\\\\\n \\':(exclude)**/*.snap\\' \\\\\\n \\':(exclude)**/*.lock\\' \\\\\\n > recap.stat || true\\n\\n BYTES=$(wc -c < recap.diff | tr -d \\' \\')\\n CHANGED=$(git diff --name-only \"$BASE_SHA\"...\"$HEAD_SHA\" -- \\\\\\n . \\\\\\n \\':(exclude)pnpm-lock.yaml\\' \\\\\\n \\':(exclude)**/dist/**\\' \\\\\\n \\':(exclude)**/*.snap\\' \\\\\\n \\':(exclude)**/*.lock\\' \\\\\\n | wc -l | tr -d \\' \\')\\n echo \"bytes=$BYTES\" >> \"$GITHUB_OUTPUT\"\\n echo \"changed=$CHANGED\" >> \"$GITHUB_OUTPUT\"\\n\\n # ~600KB cap.\\n if [ \"$BYTES\" -gt 614400 ]; then\\n echo \"huge=true\" >> \"$GITHUB_OUTPUT\"\\n else\\n echo \"huge=false\" >> \"$GITHUB_OUTPUT\"\\n fi\\n\\n # Tiny diffs (<= 1 changed file AND <= 8 changed lines) aren\\'t worth a\\n # recap — skip generation cleanly.\\n LINES=$(grep -cE \\'^[+-]\\' recap.diff || true)\\n if [ \"$CHANGED\" -le 1 ] && [ \"${LINES:-0}\" -le 8 ]; then\\n echo \"tiny=true\" >> \"$GITHUB_OUTPUT\"\\n else\\n echo \"tiny=false\" >> \"$GITHUB_OUTPUT\"\\n fi\\n\\n # Secret pre-scan: refuse to hand a diff that looks like it leaks\\n # credentials to the agent. Prints { suppressed, reason } and always exits 0.\\n - name: Secret scan\\n id: scan\\n if: steps.diff.outputs.tiny != \\'true\\'\\n run: |\\n set -uo pipefail\\n SCAN_JSON=\"$($RECAP_CLI recap scan --diff recap.diff || echo \\'{}\\')\"\\n echo \"json=$SCAN_JSON\" >> \"$GITHUB_OUTPUT\"\\n SUPPRESSED=$(node -e \\'try{process.stdout.write(JSON.parse(process.argv[1]).suppressed?\"true\":\"false\")}catch{process.stdout.write(\"false\")}\\' \"$SCAN_JSON\")\\n echo \"suppressed=$SUPPRESSED\" >> \"$GITHUB_OUTPUT\"\\n\\n # Find the planId from the previous sticky comment so a re-push REPLACES the\\n # same hosted plan (synchronize updates in place, no orphaned plans).\\n - name: Read previous plan id\\n id: prev\\n continue-on-error: true\\n run: |\\n set -euo pipefail\\n PLAN_ID=\"$($RECAP_CLI recap comment find-plan-id --repo \"$GITHUB_REPOSITORY\" --issue \"$PR_NUMBER\" --token \"$GH_TOKEN\")\"\\n echo \"plan_id=$PLAN_ID\" >> \"$GITHUB_OUTPUT\"\\n\\n # Build the agent prompt = the repo\\'s visual-recap SKILL.md + a task wrapper.\\n - name: Build recap prompt\\n id: prompt\\n if: steps.diff.outputs.tiny != \\'true\\' && steps.scan.outputs.suppressed != \\'true\\'\\n run: |\\n set -euo pipefail\\n PREV=\"\"\\n if [ -n \"${{ steps.prev.outputs.plan_id }}\" ]; then PREV=\"--prev-plan-id ${{ steps.prev.outputs.plan_id }}\"; fi\\n HUGE=\"\"\\n if [ \"${{ steps.diff.outputs.huge }}\" = \"true\" ]; then HUGE=\"--huge\"; fi\\n $RECAP_CLI recap build-prompt \\\\\\n --diff recap.diff --stat recap.stat \\\\\\n --pr \"$PR_NUMBER\" --head \"$HEAD_SHA\" \\\\\\n --app-url \"$PLAN_RECAP_APP_URL\" \\\\\\n --out recap-prompt.md \\\\\\n $HUGE $PREV\\n\\n # Wire the plan MCP server for the chosen backend, then run the agent. The\\n # agent follows the skill, calls create-visual-recap + set-resource-visibility,\\n # and writes the published plan URL to recap-url.txt. continue-on-error so a\\n # failed agent run becomes an explanatory comment, not a red X.\\n - name: Run agent (Claude Code)\\n id: claude\\n if: env.VISUAL_RECAP_AGENT == \\'claude\\' && steps.diff.outputs.tiny != \\'true\\' && steps.scan.outputs.suppressed != \\'true\\'\\n continue-on-error: true\\n env:\\n ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}\\n run: |\\n set -uo pipefail\\n MCP_CONFIG=\"$RUNNER_TEMP/plan-mcp.json\"\\n node -e \\'const fs=require(\"fs\");fs.writeFileSync(process.argv[1],JSON.stringify({mcpServers:{plan:{type:\"http\",url:process.env.PLAN_RECAP_APP_URL.replace(/\\\\/$/,\"\")+\"/_agent-native/mcp\",headers:{Authorization:\"Bearer \"+process.env.PLAN_RECAP_TOKEN}}}}))\\' \"$MCP_CONFIG\"\\n # VISUAL_RECAP_MODEL picks the Claude model; reasoning depth is model-driven\\n # for Claude Code, so VISUAL_RECAP_REASONING only applies to the Codex backend.\\n CLAUDE_ARGS=(-p \"$(cat recap-prompt.md)\" --mcp-config \"$MCP_CONFIG\" --allowedTools \"Read,Write,Bash(git diff:*),mcp__plan__create-visual-recap,mcp__plan__set-resource-visibility\" --permission-mode dontAsk)\\n if [ -n \"${VISUAL_RECAP_MODEL:-}\" ]; then CLAUDE_ARGS+=(--model \"$VISUAL_RECAP_MODEL\"); fi\\n npx -y @anthropic-ai/claude-code@2 \"${CLAUDE_ARGS[@]}\" || true\\n rm -f \"$MCP_CONFIG\" || true\\n\\n - name: Run agent (Codex)\\n id: codex\\n if: env.VISUAL_RECAP_AGENT == \\'codex\\' && steps.diff.outputs.tiny != \\'true\\' && steps.scan.outputs.suppressed != \\'true\\'\\n continue-on-error: true\\n env:\\n OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}\\n run: |\\n set -uo pipefail\\n mkdir -p \"$HOME/.codex\"\\n # JSON.stringify the URL into the TOML value so a stray quote/newline\\n # in PLAN_RECAP_APP_URL can\\'t break out of the string (TOML basic\\n # strings share JSON\\'s escaping); the key/env name stay literal.\\n node -e \\'const fs=require(\"fs\");const url=process.env.PLAN_RECAP_APP_URL.replace(/\\\\/$/,\"\")+\"/_agent-native/mcp\";fs.writeFileSync(process.env.HOME+\"/.codex/config.toml\",\"[mcp_servers.plan]\\\\nurl = \"+JSON.stringify(url)+\"\\\\nbearer_token_env_var = \\\\\"PLAN_RECAP_TOKEN\\\\\"\\\\n\")\\'\\n # Authenticate with the API key explicitly. Relying on the bare\\n # OPENAI_API_KEY env var alone is unreliable on the gpt-5.5 WebSocket\\n # transport: the Authorization header is dropped on the wss path and\\n # its HTTPS fallback, surfacing as `401 Missing bearer or basic\\n # authentication in header` (openai/codex#15492). `codex login\\n # --with-api-key` reads the key from stdin and writes ~/.codex/auth.json,\\n # which the exec path reads reliably; piping via stdin keeps the key out\\n # of the process args. Non-fatal so a login hiccup still yields the\\n # explanatory recap comment rather than a red X.\\n printenv OPENAI_API_KEY | npx -y @openai/codex@0 login --with-api-key || true\\n # VISUAL_RECAP_MODEL (e.g. gpt-5.5) and VISUAL_RECAP_REASONING\\n # (none|minimal|low|medium|high|xhigh) tune the Codex run.\\n #\\n # The GitHub runner is itself an ephemeral, throwaway sandbox, so run\\n # Codex with sandboxing and approvals disabled. Codex\\'s own bubblewrap\\n # sandbox cannot initialize on the runner (\"could not find bubblewrap\\n # on PATH\"), which makes every shell command fail at startup so the\\n # agent cannot even read recap.diff; and under an approval gate the\\n # write-side plan MCP call (create-visual-recap) is auto-cancelled\\n # (\"user cancelled MCP tool call\"). --dangerously-bypass-approvals-and-sandbox\\n # is the documented invocation for externally-sandboxed CI and clears both.\\n CODEX_ARGS=(exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check)\\n if [ -n \"${VISUAL_RECAP_MODEL:-}\" ]; then CODEX_ARGS+=(--model \"$VISUAL_RECAP_MODEL\"); fi\\n # Validate reasoning against the known enum before embedding it in the\\n # codex `-c` TOML override, so an unexpected value can\\'t alter the config.\\n case \"${VISUAL_RECAP_REASONING:-}\" in\\n none|minimal|low|medium|high|xhigh)\\n CODEX_ARGS+=(-c \"model_reasoning_effort=\\\\\"$VISUAL_RECAP_REASONING\\\\\"\") ;;\\n \"\") ;;\\n *) echo \"Ignoring invalid VISUAL_RECAP_REASONING: $VISUAL_RECAP_REASONING\" ;;\\n esac\\n npx -y @openai/codex@0 \"${CODEX_ARGS[@]}\" \"$(cat recap-prompt.md)\" || true\\n\\n # The agent\\'s only hand-off: recap-url.txt with the published plan URL.\\n - name: Read plan URL\\n id: url\\n if: steps.diff.outputs.tiny != \\'true\\' && steps.scan.outputs.suppressed != \\'true\\'\\n run: |\\n set -uo pipefail\\n PLAN_URL=\"\"\\n if [ -f recap-url.txt ]; then PLAN_URL=\"$(tr -d \\'\\\\r\\\\n\\' < recap-url.txt | tr -d \\' \\')\"; fi\\n echo \"plan_url=$PLAN_URL\" >> \"$GITHUB_OUTPUT\"\\n if [ -n \"$PLAN_URL\" ]; then echo \"ok=true\" >> \"$GITHUB_OUTPUT\"; else echo \"ok=false\" >> \"$GITHUB_OUTPUT\"; fi\\n\\n # Screenshot the published plan in headless Chrome and upload the PNG to the\\n # plan app\\'s signed public image route. Best-effort: never fails the job.\\n - name: Screenshot + upload\\n id: shot\\n if: steps.url.outputs.ok == \\'true\\'\\n continue-on-error: true\\n env:\\n # Pass the agent-produced plan URL through the environment, never via\\n # ${{ }} interpolation into the run script: recap-url.txt is untrusted\\n # agent output, so inlining it would be a shell-injection vector.\\n PLAN_URL: ${{ steps.url.outputs.plan_url }}\\n run: |\\n set -uo pipefail\\n pnpm exec playwright install --with-deps chromium 2>/dev/null || npx -y playwright@1 install --with-deps chromium || true\\n SHOT_JSON=\"$($RECAP_CLI recap shot --url \"$PLAN_URL\" --token \"$PLAN_RECAP_TOKEN\" --app-url \"$PLAN_RECAP_APP_URL\" --out recap.png || echo \\'{}\\')\"\\n IMAGE_URL=$(node -e \\'try{process.stdout.write(JSON.parse(process.argv[1]).imageUrl||\"\")}catch{process.stdout.write(\"\")}\\' \"$SHOT_JSON\")\\n echo \"image_url=$IMAGE_URL\" >> \"$GITHUB_OUTPUT\"\\n if [ -f recap.png ]; then echo \"captured=true\" >> \"$GITHUB_OUTPUT\"; else echo \"captured=false\" >> \"$GITHUB_OUTPUT\"; fi\\n\\n - name: Upload recap screenshot artifact\\n if: steps.shot.outputs.captured == \\'true\\'\\n uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2\\n with:\\n name: pr-visual-recap-${{ github.event.pull_request.number }}\\n path: recap.png\\n if-no-files-found: ignore\\n retention-days: 14\\n\\n # Upsert the single sticky comment: inline screenshot + link on success,\\n # suppressed / failed / tiny variants otherwise. Runs even on a tiny diff\\n # so a prior recap comment is refreshed (not left pointing at a stale SHA).\\n - name: Upsert sticky comment\\n if: always()\\n env:\\n PLAN_URL: ${{ steps.url.outputs.plan_url }}\\n RECAP_IMAGE_URL: ${{ steps.shot.outputs.image_url }}\\n SUPPRESSED: ${{ steps.scan.outputs.suppressed }}\\n SUPPRESSED_JSON: ${{ steps.scan.outputs.json }}\\n DIFF_HUGE: ${{ steps.diff.outputs.huge }}\\n DIFF_TINY: ${{ steps.diff.outputs.tiny }}\\n run: |\\n set -euo pipefail\\n ARGS=(recap comment upsert --repo \"$GITHUB_REPOSITORY\" --issue \"$PR_NUMBER\" --token \"$GH_TOKEN\")\\n # On a tiny diff only REFRESH an existing recap comment — never create a\\n # new one — so a tiny push doesn\\'t add noise but also can\\'t leave a\\n # stale prior recap behind.\\n if [ \"${DIFF_TINY:-}\" = \"true\" ]; then ARGS+=(--update-only); fi\\n $RECAP_CLI \"${ARGS[@]}\"\\n';\n"]}
|
|
1
|
+
{"version":3,"file":"pr-visual-recap-workflow.js","sourceRoot":"","sources":["../../src/cli/pr-visual-recap-workflow.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,CAAC,MAAM,4BAA4B,GACvC,q0wBAAq0wB,CAAC","sourcesContent":["/**\n * Bundled copy of .github/workflows/pr-visual-recap.yml so the CLI can write the\n * PR Visual Recap workflow into a user repo via\n * `agent-native skills add visual-plan --with-github-action`.\n *\n * AUTO-GENERATED — keep byte-identical with the source workflow. A sync test in\n * recap.spec.ts fails if these drift. Regenerate from the YAML with the snippet\n * in recap.spec.ts.\n */\n\nexport const PR_VISUAL_RECAP_WORKFLOW_YML =\n 'name: PR Visual Recap\\n\\n# Turns every PR into a \"visual code review\" — a reverse plan — by letting a real\\n# coding agent RUN THE REPO\\'S visual-recap SKILL against the diff. The agent\\n# (Claude Code by default, or Codex) reads the skill, reasons over the change,\\n# publishes an Agent-Native Plan via the plan MCP tools, and writes the plan URL\\n# to recap-url.txt. The workflow then screenshots that plan in headless Chrome,\\n# uploads the PNG to the plan app\\'s signed public image route, and upserts ONE\\n# sticky PR comment with the inline screenshot + the interactive link.\\n#\\n# Design notes:\\n# - Plain `pull_request` (NOT `pull_request_target`) so fork code can never see\\n# the publish/agent secrets. Fork PRs are a silent no-op.\\n# - The `gate` job is a cheap switch: drafts, forks, bot authors, and the\\n# missing-secret case short-circuit with NO comment and NO compute. Merging\\n# this workflow before the secrets exist is a safe no-op.\\n# - The recap is INFORMATIONAL ONLY. It is not a required check and failures\\n# surface as an explanatory sticky comment, never a red X on unrelated code.\\n# - Backend is selectable with the `VISUAL_RECAP_AGENT` repo variable\\n# (claude | codex; default claude). Model and reasoning depth are tunable with\\n# `VISUAL_RECAP_MODEL` (e.g. gpt-5.5) and `VISUAL_RECAP_REASONING`\\n# (none|minimal|low|medium|high|xhigh; Codex only). The CLI invocation is\\n# auto-detected: local source inside this monorepo, the published\\n# @agent-native/core elsewhere — no repo variable needed.\\n# - Only two secrets are required: PLAN_RECAP_TOKEN (publish) and the chosen\\n# backend\\'s API key. PLAN_RECAP_APP_URL defaults to the hosted plan app.\\n# - Nothing here is deterministic: the skill\\'s instructions drive the recap.\\n\\non:\\n # Run on PRs into any base branch — the generated workflow ships to repos whose\\n # default branch may not be `main`. The gate job below still no-ops drafts,\\n # forks, bots, and the missing-secret case, so this stays cheap.\\n pull_request:\\n types: [opened, synchronize, reopened, ready_for_review]\\n\\npermissions:\\n contents: read\\n issues: write\\n pull-requests: write\\n\\nconcurrency:\\n group: pr-visual-recap-${{ github.event.pull_request.number }}\\n cancel-in-progress: true\\n\\nenv:\\n VISUAL_RECAP_AGENT: ${{ vars.VISUAL_RECAP_AGENT || \\'claude\\' }}\\n\\njobs:\\n # --------------------------------------------------------------------------\\n # Cheap gate: decide whether to do any work at all. Sets run=false (silent\\n # no-op) for drafts, forks, bot authors, or when the publish secret / the\\n # chosen backend\\'s API key is absent.\\n # --------------------------------------------------------------------------\\n gate:\\n name: Gate\\n runs-on: ubuntu-latest\\n outputs:\\n run: ${{ steps.decide.outputs.run }}\\n agent: ${{ steps.decide.outputs.agent }}\\n steps:\\n - id: decide\\n uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7\\n env:\\n # Presence-only signals — we never expose the secret VALUES to the gate.\\n # PLAN_RECAP_APP_URL defaults to the hosted app, so only the token is required.\\n HAS_PLAN: ${{ secrets.PLAN_RECAP_TOKEN != \\'\\' }}\\n HAS_ANTHROPIC: ${{ secrets.ANTHROPIC_API_KEY != \\'\\' }}\\n HAS_OPENAI: ${{ secrets.OPENAI_API_KEY != \\'\\' }}\\n AGENT: ${{ env.VISUAL_RECAP_AGENT }}\\n with:\\n script: |\\n const pr = context.payload.pull_request;\\n const reasons = [];\\n\\n if (!pr) reasons.push(\\'no pull_request payload\\');\\n if (pr && pr.draft) reasons.push(\\'draft PR\\');\\n\\n // Fork PRs: head repo differs from this repo. Plain pull_request runs\\n // fork code with NO secrets, so publishing would fail anyway — skip.\\n const headRepo = pr && pr.head && pr.head.repo && pr.head.repo.full_name;\\n if (pr && headRepo && headRepo !== process.env.GITHUB_REPOSITORY) {\\n reasons.push(`fork PR (${headRepo})`);\\n }\\n\\n // Skip noisy automated authors.\\n const login = (pr && pr.user && pr.user.login || \\'\\').toLowerCase();\\n const botAuthors = [\\'dependabot[bot]\\', \\'dependabot\\', \\'renovate[bot]\\', \\'renovate\\'];\\n if (botAuthors.includes(login)) reasons.push(`bot author (${login})`);\\n if (pr && pr.user && pr.user.type === \\'Bot\\') reasons.push(\\'bot author (type=Bot)\\');\\n\\n // Publish secret must be configured — otherwise this is a no-op so the\\n // workflow can be merged before secrets exist.\\n if (process.env.HAS_PLAN !== \\'true\\') reasons.push(\\'PLAN_RECAP_TOKEN not configured\\');\\n\\n // The chosen backend\\'s API key must be present. Normalize the agent\\n // value once here and validate it: an unknown or mis-cased value\\n // (e.g. \"Claude\", \"gpt\") must NOT silently pass the gate and then\\n // match neither agent step below.\\n const agent = (process.env.AGENT || \\'claude\\').toLowerCase();\\n if (agent !== \\'claude\\' && agent !== \\'codex\\') {\\n reasons.push(`unsupported VISUAL_RECAP_AGENT \"${process.env.AGENT}\" (expected \"claude\" or \"codex\")`);\\n } else if (agent === \\'codex\\') {\\n if (process.env.HAS_OPENAI !== \\'true\\') reasons.push(\\'OPENAI_API_KEY not configured (codex backend)\\');\\n } else {\\n if (process.env.HAS_ANTHROPIC !== \\'true\\') reasons.push(\\'ANTHROPIC_API_KEY not configured (claude backend)\\');\\n }\\n\\n // Self-modifying guard, evaluated in the GATE (trusted github-script\\n // that runs NO PR-checked-out code): if this PR changes the workflow,\\n // the visual-recap/visual-plan skill, the local CLI (packages/core),\\n // or any agent config the runner would load (.claude/**, CLAUDE.md,\\n // .mcp.json), skip the ENTIRE job — not just the agent — so a PR can\\n // never rewrite what runs (skill, hooks, settings, CLI) and exfiltrate\\n // the publish/API secrets.\\n if (pr) {\\n try {\\n const files = await github.paginate(github.rest.pulls.listFiles, {\\n owner: context.repo.owner,\\n repo: context.repo.repo,\\n pull_number: pr.number,\\n per_page: 100,\\n });\\n const isSensitive = (p) =>\\n p === \\'.github/workflows/pr-visual-recap.yml\\' ||\\n /(^|\\\\/)skills\\\\/visual-(recap|plan|plans)\\\\//.test(p) ||\\n /(^|\\\\/)\\\\.claude\\\\//.test(p) ||\\n /(^|\\\\/)CLAUDE\\\\.md$/.test(p) ||\\n /(^|\\\\/)AGENTS\\\\.md$/.test(p) ||\\n /(^|\\\\/)\\\\.mcp\\\\.json$/.test(p) ||\\n /(^|\\\\/)packages\\\\/core\\\\//.test(p);\\n const hits = files.map((f) => f.filename).filter(isSensitive);\\n if (hits.length) {\\n reasons.push(`PR modifies recap-control files (${hits.slice(0, 3).join(\\', \\')}${hits.length > 3 ? \\', …\\' : \\'\\'}) — skipping so untrusted PR code never runs with secrets`);\\n }\\n } catch (e) {\\n reasons.push(`could not list PR files for the self-modifying guard (${e.message}); skipping to be safe`);\\n }\\n }\\n\\n const run = reasons.length === 0;\\n core.setOutput(\\'run\\', run ? \\'true\\' : \\'false\\');\\n // Export the NORMALIZED agent so the recap job\\'s step conditions match\\n // case-insensitively via needs.gate.outputs.agent.\\n core.setOutput(\\'agent\\', agent);\\n core.info(run ? `Visual recap will run (${agent}).` : `Visual recap skipped: ${reasons.join(\\'; \\')}`);\\n\\n # --------------------------------------------------------------------------\\n # Recap: collect the diff, let the agent run the skill + publish, screenshot\\n # the result, and upsert the sticky comment.\\n # --------------------------------------------------------------------------\\n recap:\\n name: Generate visual recap\\n needs: gate\\n if: needs.gate.outputs.run == \\'true\\'\\n runs-on: ubuntu-latest\\n env:\\n PLAN_RECAP_APP_URL: ${{ secrets.PLAN_RECAP_APP_URL || \\'https://plan.agent-native.com\\' }}\\n PLAN_RECAP_TOKEN: ${{ secrets.PLAN_RECAP_TOKEN }}\\n GH_TOKEN: ${{ github.token }}\\n PR_NUMBER: ${{ github.event.pull_request.number }}\\n HEAD_SHA: ${{ github.event.pull_request.head.sha }}\\n VISUAL_RECAP_MODEL: ${{ vars.VISUAL_RECAP_MODEL }}\\n VISUAL_RECAP_REASONING: ${{ vars.VISUAL_RECAP_REASONING }}\\n steps:\\n - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1\\n with:\\n fetch-depth: 0\\n\\n # Resolve the CLI invocation once: dogfood local source inside this\\n # monorepo, otherwise the published package. No repo variable needed. The\\n # pnpm setup/install steps below run ONLY for the local-source path, so the\\n # generated workflow works out-of-box in npm/yarn consumer repos (which\\n # have no pnpm-lock.yaml) by falling back to `npx @agent-native/core`.\\n - name: Resolve recap CLI\\n id: cli\\n run: |\\n if [ -f packages/core/src/cli/index.ts ]; then\\n echo \"RECAP_CLI=pnpm exec tsx packages/core/src/cli/index.ts\" >> \"$GITHUB_ENV\"\\n echo \"local=true\" >> \"$GITHUB_OUTPUT\"\\n else\\n echo \"RECAP_CLI=npx -y @agent-native/core@latest\" >> \"$GITHUB_ENV\"\\n echo \"local=false\" >> \"$GITHUB_OUTPUT\"\\n fi\\n\\n - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0\\n if: steps.cli.outputs.local == \\'true\\'\\n\\n - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0\\n with:\\n node-version: \"22\"\\n cache: ${{ steps.cli.outputs.local == \\'true\\' && \\'pnpm\\' || \\'\\' }}\\n\\n - name: Install workspace (local source only)\\n if: steps.cli.outputs.local == \\'true\\'\\n run: pnpm install --frozen-lockfile --ignore-scripts\\n\\n # Collect a BOUNDED diff between the PR base and head. We exclude lockfiles,\\n # build output, and snapshots (noise), and cap the byte size — over the cap\\n # we set `huge=true` so the agent is told to produce a summarized recap.\\n - name: Collect bounded diff\\n id: diff\\n env:\\n BASE_SHA: ${{ github.event.pull_request.base.sha }}\\n run: |\\n set -euo pipefail\\n git diff --no-color \"$BASE_SHA\"...\"$HEAD_SHA\" -- \\\\\\n . \\\\\\n \\':(exclude)pnpm-lock.yaml\\' \\\\\\n \\':(exclude)**/dist/**\\' \\\\\\n \\':(exclude)**/*.snap\\' \\\\\\n \\':(exclude)**/*.lock\\' \\\\\\n > recap.diff || true\\n git diff --stat --no-color \"$BASE_SHA\"...\"$HEAD_SHA\" -- \\\\\\n . \\\\\\n \\':(exclude)pnpm-lock.yaml\\' \\\\\\n \\':(exclude)**/dist/**\\' \\\\\\n \\':(exclude)**/*.snap\\' \\\\\\n \\':(exclude)**/*.lock\\' \\\\\\n > recap.stat || true\\n\\n BYTES=$(wc -c < recap.diff | tr -d \\' \\')\\n CHANGED=$(git diff --name-only \"$BASE_SHA\"...\"$HEAD_SHA\" -- \\\\\\n . \\\\\\n \\':(exclude)pnpm-lock.yaml\\' \\\\\\n \\':(exclude)**/dist/**\\' \\\\\\n \\':(exclude)**/*.snap\\' \\\\\\n \\':(exclude)**/*.lock\\' \\\\\\n | wc -l | tr -d \\' \\')\\n echo \"bytes=$BYTES\" >> \"$GITHUB_OUTPUT\"\\n echo \"changed=$CHANGED\" >> \"$GITHUB_OUTPUT\"\\n\\n # ~600KB cap. Over the cap we both flag `huge` (so the agent is told to\\n # summarize) AND physically truncate recap.diff, so an oversized diff\\n # cannot overflow the agent\\'s prompt budget when it reads the file.\\n # Truncate at a COMPLETE LINE boundary (`sed \\'$d\\'` drops the last,\\n # possibly-partial, line) so the byte cap can never cut a multi-byte\\n # UTF-8 char or a diff line mid-way and corrupt the agent\\'s input.\\n if [ \"$BYTES\" -gt 614400 ]; then\\n echo \"huge=true\" >> \"$GITHUB_OUTPUT\"\\n head -c 614400 recap.diff | sed \\'$d\\' > recap.diff.capped && mv recap.diff.capped recap.diff\\n printf \\'\\\\n\\\\n[diff truncated at 600KB for the recap agent]\\\\n\\' >> recap.diff\\n else\\n echo \"huge=false\" >> \"$GITHUB_OUTPUT\"\\n fi\\n\\n # Tiny diffs (<= 1 changed file AND <= 8 changed lines) aren\\'t worth a\\n # recap — skip generation cleanly.\\n LINES=$(grep -cE \\'^[+-]\\' recap.diff || true)\\n if [ \"$CHANGED\" -le 1 ] && [ \"${LINES:-0}\" -le 8 ]; then\\n echo \"tiny=true\" >> \"$GITHUB_OUTPUT\"\\n else\\n echo \"tiny=false\" >> \"$GITHUB_OUTPUT\"\\n fi\\n\\n # Secret pre-scan: refuse to hand a diff that looks like it leaks\\n # credentials to the agent. Prints { suppressed, reason } and always exits 0.\\n - name: Secret scan\\n id: scan\\n if: steps.diff.outputs.tiny != \\'true\\'\\n run: |\\n set -uo pipefail\\n # Fail CLOSED: if the scanner errors or emits invalid JSON, treat the\\n # diff as suppressed, so a scan failure can never hand a possibly\\n # credential-bearing diff to the agent / plan service.\\n if ! SCAN_JSON=\"$($RECAP_CLI recap scan --diff recap.diff)\"; then\\n SCAN_JSON=\\'{\"suppressed\":true,\"reason\":\"secret scan failed to run; failing closed\"}\\'\\n fi\\n # Multi-line-safe write: SCAN_JSON could contain newlines, which would\\n # otherwise corrupt $GITHUB_OUTPUT or inject extra keys.\\n {\\n echo \\'json<<__RECAP_SCAN_EOF__\\'\\n echo \"$SCAN_JSON\"\\n echo \\'__RECAP_SCAN_EOF__\\'\\n } >> \"$GITHUB_OUTPUT\"\\n SUPPRESSED=$(node -e \\'try{process.stdout.write(JSON.parse(process.argv[1]).suppressed?\"true\":\"false\")}catch{process.stdout.write(\"true\")}\\' \"$SCAN_JSON\")\\n echo \"suppressed=$SUPPRESSED\" >> \"$GITHUB_OUTPUT\"\\n\\n # Find the planId from the previous sticky comment so a re-push REPLACES the\\n # same hosted plan (synchronize updates in place, no orphaned plans).\\n - name: Read previous plan id\\n id: prev\\n continue-on-error: true\\n run: |\\n set -euo pipefail\\n PLAN_ID=\"$($RECAP_CLI recap comment find-plan-id --repo \"$GITHUB_REPOSITORY\" --issue \"$PR_NUMBER\" --token \"$GH_TOKEN\")\"\\n echo \"plan_id=$PLAN_ID\" >> \"$GITHUB_OUTPUT\"\\n\\n # Build the agent prompt = the repo\\'s visual-recap SKILL.md + a task wrapper.\\n - name: Build recap prompt\\n id: prompt\\n if: steps.diff.outputs.tiny != \\'true\\' && steps.scan.outputs.suppressed != \\'true\\'\\n run: |\\n set -euo pipefail\\n PREV=\"\"\\n if [ -n \"${{ steps.prev.outputs.plan_id }}\" ]; then PREV=\"--prev-plan-id ${{ steps.prev.outputs.plan_id }}\"; fi\\n HUGE=\"\"\\n if [ \"${{ steps.diff.outputs.huge }}\" = \"true\" ]; then HUGE=\"--huge\"; fi\\n $RECAP_CLI recap build-prompt \\\\\\n --diff recap.diff --stat recap.stat \\\\\\n --pr \"$PR_NUMBER\" --head \"$HEAD_SHA\" \\\\\\n --app-url \"$PLAN_RECAP_APP_URL\" \\\\\\n --out recap-prompt.md \\\\\\n $HUGE $PREV\\n\\n # Wire the plan MCP server for the chosen backend, then run the agent. The\\n # agent follows the skill, calls create-visual-recap + set-resource-visibility,\\n # and writes the published plan URL to recap-url.txt. continue-on-error so a\\n # failed agent run becomes an explanatory comment, not a red X.\\n - name: Run agent (Claude Code)\\n id: claude\\n if: needs.gate.outputs.agent == \\'claude\\' && steps.diff.outputs.tiny != \\'true\\' && steps.scan.outputs.suppressed != \\'true\\'\\n continue-on-error: true\\n env:\\n ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}\\n run: |\\n set -uo pipefail\\n MCP_CONFIG=\"$RUNNER_TEMP/plan-mcp.json\"\\n node -e \\'const fs=require(\"fs\");fs.writeFileSync(process.argv[1],JSON.stringify({mcpServers:{plan:{type:\"http\",url:process.env.PLAN_RECAP_APP_URL.replace(/\\\\/$/,\"\")+\"/_agent-native/mcp\",headers:{Authorization:\"Bearer \"+process.env.PLAN_RECAP_TOKEN}}}}))\\' \"$MCP_CONFIG\"\\n # VISUAL_RECAP_MODEL picks the Claude model; reasoning depth is model-driven\\n # for Claude Code, so VISUAL_RECAP_REASONING only applies to the Codex backend.\\n CLAUDE_ARGS=(-p \"$(cat recap-prompt.md)\" --mcp-config \"$MCP_CONFIG\" --allowedTools \"Read,Write,Bash(git diff:*),mcp__plan__create-visual-recap,mcp__plan__set-resource-visibility\" --permission-mode dontAsk --output-format json)\\n if [ -n \"${VISUAL_RECAP_MODEL:-}\" ]; then CLAUDE_ARGS+=(--model \"$VISUAL_RECAP_MODEL\"); fi\\n # Capture the final JSON result (usage + total_cost_usd) for the usage step.\\n npx -y @anthropic-ai/claude-code@2 \"${CLAUDE_ARGS[@]}\" > claude-result.json || true\\n rm -f \"$MCP_CONFIG\" || true\\n\\n - name: Run agent (Codex)\\n id: codex\\n if: needs.gate.outputs.agent == \\'codex\\' && steps.diff.outputs.tiny != \\'true\\' && steps.scan.outputs.suppressed != \\'true\\'\\n continue-on-error: true\\n env:\\n OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}\\n run: |\\n set -uo pipefail\\n mkdir -p \"$HOME/.codex\"\\n # JSON.stringify the URL into the TOML value so a stray quote/newline\\n # in PLAN_RECAP_APP_URL can\\'t break out of the string (TOML basic\\n # strings share JSON\\'s escaping); the key/env name stay literal.\\n node -e \\'const fs=require(\"fs\");const url=process.env.PLAN_RECAP_APP_URL.replace(/\\\\/$/,\"\")+\"/_agent-native/mcp\";fs.writeFileSync(process.env.HOME+\"/.codex/config.toml\",\"[mcp_servers.plan]\\\\nurl = \"+JSON.stringify(url)+\"\\\\nbearer_token_env_var = \\\\\"PLAN_RECAP_TOKEN\\\\\"\\\\n\")\\'\\n # Authenticate with the API key explicitly. Relying on the bare\\n # OPENAI_API_KEY env var alone is unreliable on the gpt-5.5 WebSocket\\n # transport: the Authorization header is dropped on the wss path and\\n # its HTTPS fallback, surfacing as `401 Missing bearer or basic\\n # authentication in header` (openai/codex#15492). `codex login\\n # --with-api-key` reads the key from stdin and writes ~/.codex/auth.json,\\n # which the exec path reads reliably; piping via stdin keeps the key out\\n # of the process args. Non-fatal so a login hiccup still yields the\\n # explanatory recap comment rather than a red X.\\n printenv OPENAI_API_KEY | npx -y @openai/codex@0 login --with-api-key || true\\n # VISUAL_RECAP_MODEL (e.g. gpt-5.5) and VISUAL_RECAP_REASONING\\n # (none|minimal|low|medium|high|xhigh) tune the Codex run.\\n #\\n # The GitHub runner is itself an ephemeral, throwaway sandbox, so run\\n # Codex with sandboxing and approvals disabled. Codex\\'s own bubblewrap\\n # sandbox cannot initialize on the runner (\"could not find bubblewrap\\n # on PATH\"), which makes every shell command fail at startup so the\\n # agent cannot even read recap.diff; and under an approval gate the\\n # write-side plan MCP call (create-visual-recap) is auto-cancelled\\n # (\"user cancelled MCP tool call\"). --dangerously-bypass-approvals-and-sandbox\\n # is the documented invocation for externally-sandboxed CI and clears both.\\n CODEX_ARGS=(exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check)\\n if [ -n \"${VISUAL_RECAP_MODEL:-}\" ]; then CODEX_ARGS+=(--model \"$VISUAL_RECAP_MODEL\"); fi\\n # Validate reasoning against the known enum before embedding it in the\\n # codex `-c` TOML override, so an unexpected value can\\'t alter the config.\\n case \"${VISUAL_RECAP_REASONING:-}\" in\\n none|minimal|low|medium|high|xhigh)\\n CODEX_ARGS+=(-c \"model_reasoning_effort=\\\\\"$VISUAL_RECAP_REASONING\\\\\"\") ;;\\n \"\") ;;\\n *) echo \"Ignoring invalid VISUAL_RECAP_REASONING: $VISUAL_RECAP_REASONING\" ;;\\n esac\\n npx -y @openai/codex@0 \"${CODEX_ARGS[@]}\" --json \"$(cat recap-prompt.md)\" | tee codex-events.jsonl || true\\n\\n # The agent\\'s only hand-off: recap-url.txt with the published plan URL.\\n - name: Read plan URL\\n id: url\\n if: steps.diff.outputs.tiny != \\'true\\' && steps.scan.outputs.suppressed != \\'true\\'\\n run: |\\n set -uo pipefail\\n PLAN_URL=\"\"\\n if [ -f recap-url.txt ]; then PLAN_URL=\"$(tr -d \\'\\\\r\\\\n\\' < recap-url.txt | tr -d \\' \\')\"; fi\\n echo \"plan_url=$PLAN_URL\" >> \"$GITHUB_OUTPUT\"\\n if [ -n \"$PLAN_URL\" ]; then echo \"ok=true\" >> \"$GITHUB_OUTPUT\"; else echo \"ok=false\" >> \"$GITHUB_OUTPUT\"; fi\\n\\n # Capture the agent run token usage and attach it to the published recap\\n # so the recap row carries input/output/cached tokens, the model, and a\\n # cost estimate. Informational and best-effort: never fails the job.\\n - name: Attach usage\\n if: steps.url.outputs.ok == \\'true\\'\\n continue-on-error: true\\n env:\\n PLAN_URL: ${{ steps.url.outputs.plan_url }}\\n run: |\\n set -uo pipefail\\n RESULT=claude-result.json\\n if [ \"$VISUAL_RECAP_AGENT\" = \"codex\" ]; then RESULT=codex-events.jsonl; fi\\n if [ -f \"$RESULT\" ]; then $RECAP_CLI recap usage --plan-url \"$PLAN_URL\" --agent \"$VISUAL_RECAP_AGENT\" --result-file \"$RESULT\" --model \"${VISUAL_RECAP_MODEL:-}\" --app-url \"$PLAN_RECAP_APP_URL\" --token \"$PLAN_RECAP_TOKEN\" || true; fi\\n\\n # Screenshot the published plan in headless Chrome and upload the PNG to the\\n # plan app\\'s signed public image route. Best-effort: never fails the job.\\n - name: Screenshot + upload\\n id: shot\\n if: steps.url.outputs.ok == \\'true\\'\\n continue-on-error: true\\n env:\\n # Pass the agent-produced plan URL through the environment, never via\\n # ${{ }} interpolation into the run script: recap-url.txt is untrusted\\n # agent output, so inlining it would be a shell-injection vector.\\n PLAN_URL: ${{ steps.url.outputs.plan_url }}\\n run: |\\n set -uo pipefail\\n pnpm exec playwright install --with-deps chromium 2>/dev/null || npx -y playwright@1 install --with-deps chromium || true\\n SHOT_JSON=\"$($RECAP_CLI recap shot --url \"$PLAN_URL\" --token \"$PLAN_RECAP_TOKEN\" --app-url \"$PLAN_RECAP_APP_URL\" --out recap.png || echo \\'{}\\')\"\\n IMAGE_URL=$(node -e \\'try{process.stdout.write(JSON.parse(process.argv[1]).imageUrl||\"\")}catch{process.stdout.write(\"\")}\\' \"$SHOT_JSON\")\\n echo \"image_url=$IMAGE_URL\" >> \"$GITHUB_OUTPUT\"\\n if [ -f recap.png ]; then echo \"captured=true\" >> \"$GITHUB_OUTPUT\"; else echo \"captured=false\" >> \"$GITHUB_OUTPUT\"; fi\\n\\n - name: Upload recap screenshot artifact\\n if: steps.shot.outputs.captured == \\'true\\'\\n uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2\\n with:\\n name: pr-visual-recap-${{ github.event.pull_request.number }}\\n path: recap.png\\n if-no-files-found: ignore\\n retention-days: 14\\n\\n # Upsert the single sticky comment: inline screenshot + link on success,\\n # suppressed / failed / tiny variants otherwise. Runs even on a tiny diff\\n # so a prior recap comment is refreshed (not left pointing at a stale SHA).\\n - name: Upsert sticky comment\\n if: always()\\n # The recap is informational/non-blocking: a failed comment upsert must\\n # not turn the whole job red (the agent + screenshot steps are already\\n # continue-on-error).\\n continue-on-error: true\\n env:\\n PLAN_URL: ${{ steps.url.outputs.plan_url }}\\n RECAP_IMAGE_URL: ${{ steps.shot.outputs.image_url }}\\n SUPPRESSED: ${{ steps.scan.outputs.suppressed }}\\n SUPPRESSED_JSON: ${{ steps.scan.outputs.json }}\\n DIFF_HUGE: ${{ steps.diff.outputs.huge }}\\n DIFF_TINY: ${{ steps.diff.outputs.tiny }}\\n run: |\\n set -euo pipefail\\n ARGS=(recap comment upsert --repo \"$GITHUB_REPOSITORY\" --issue \"$PR_NUMBER\" --token \"$GH_TOKEN\")\\n # On a tiny diff, only REFRESH an existing recap comment — never create\\n # a new one — so we add no noise but also can\\'t leave a stale prior\\n # recap behind.\\n if [ \"${DIFF_TINY:-}\" = \"true\" ]; then ARGS+=(--update-only); fi\\n $RECAP_CLI \"${ARGS[@]}\"\\n';\n"]}
|
package/dist/cli/recap.d.ts
CHANGED
|
@@ -48,5 +48,28 @@ export declare function buildRecapPrompt(input: {
|
|
|
48
48
|
}): string;
|
|
49
49
|
/** Build the sticky comment body from the workflow's environment. */
|
|
50
50
|
export declare function buildCommentBody(env?: NodeJS.ProcessEnv): string;
|
|
51
|
+
interface ParsedUsage {
|
|
52
|
+
inputTokens: number;
|
|
53
|
+
outputTokens: number;
|
|
54
|
+
cacheReadTokens: number;
|
|
55
|
+
cacheWriteTokens: number;
|
|
56
|
+
model?: string;
|
|
57
|
+
reportedCostUsd?: number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Claude Code `-p --output-format json` prints one final result object with a
|
|
61
|
+
* `usage` block and `total_cost_usd`. Anthropic's `input_tokens` already
|
|
62
|
+
* EXCLUDES cache tokens, so no normalization is needed here.
|
|
63
|
+
*/
|
|
64
|
+
export declare function parseClaudeUsage(stdout: string): ParsedUsage | null;
|
|
65
|
+
/**
|
|
66
|
+
* Codex `exec --json` reports `input_tokens` INCLUSIVE of `cached_input_tokens`
|
|
67
|
+
* (OpenAI counts cached as a subset of prompt tokens) and bills
|
|
68
|
+
* `reasoning_output_tokens` separately. Normalize to the cache-exclusive shape
|
|
69
|
+
* `calculateCost` expects: strip cached out of input, fold reasoning into
|
|
70
|
+
* output. Without this, cached tokens are billed twice and reasoning is dropped.
|
|
71
|
+
*/
|
|
72
|
+
export declare function parseCodexUsage(jsonl: string): ParsedUsage | null;
|
|
51
73
|
export declare function runRecap(argv: string[]): Promise<void>;
|
|
74
|
+
export {};
|
|
52
75
|
//# sourceMappingURL=recap.d.ts.map
|
package/dist/cli/recap.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recap.d.ts","sourceRoot":"","sources":["../../src/cli/recap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAkDH,mEAAmE;AACnE,eAAO,MAAM,qBAAqB,EAAE,MAAM,EAQzC,CAAC;AAEF,+DAA+D;AAC/D,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;CAClB,CAOA;AA6BD,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAErD;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAa5D;AAMD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAsB,GAAG;IAC5D,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,CAgBA;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,GAAG,MAAM,CAkDT;AAgJD,qEAAqE;AACrE,wBAAgB,gBAAgB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAwF7E;
|
|
1
|
+
{"version":3,"file":"recap.d.ts","sourceRoot":"","sources":["../../src/cli/recap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAkDH,mEAAmE;AACnE,eAAO,MAAM,qBAAqB,EAAE,MAAM,EAQzC,CAAC;AAEF,+DAA+D;AAC/D,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;CAClB,CAOA;AA6BD,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAErD;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAa5D;AAMD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAsB,GAAG;IAC5D,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,CAgBA;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,GAAG,MAAM,CAkDT;AAgJD,qEAAqE;AACrE,wBAAgB,gBAAgB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAwF7E;AAmPD,UAAU,WAAW;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAwBD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAmBnE;AA2BD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAajE;AAmGD,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA6B5D"}
|
package/dist/cli/recap.js
CHANGED
|
@@ -498,22 +498,11 @@ async function runShot(args) {
|
|
|
498
498
|
}
|
|
499
499
|
}
|
|
500
500
|
await page.waitForTimeout(matched ? 1_200 : 500);
|
|
501
|
-
// Zoom out slightly so more content fits
|
|
501
|
+
// Zoom out slightly so more content fits. Keep the plan title (h1) in frame:
|
|
502
|
+
// the recap reads better led by its own title than cropped to the body.
|
|
502
503
|
await page.evaluate(() => {
|
|
503
504
|
document.documentElement.style.zoom = "80%";
|
|
504
505
|
});
|
|
505
|
-
await page.evaluate(() => {
|
|
506
|
-
const firstH2 = document.querySelector("h2");
|
|
507
|
-
if (firstH2) {
|
|
508
|
-
firstH2.scrollIntoView({ block: "start" });
|
|
509
|
-
window.scrollBy(0, -24);
|
|
510
|
-
}
|
|
511
|
-
else {
|
|
512
|
-
const h1 = document.querySelector("h1");
|
|
513
|
-
if (h1)
|
|
514
|
-
window.scrollBy(0, h1.getBoundingClientRect().bottom + 32);
|
|
515
|
-
}
|
|
516
|
-
});
|
|
517
506
|
await page.screenshot({ path: out });
|
|
518
507
|
captured = true;
|
|
519
508
|
await browser.close();
|
|
@@ -565,12 +554,184 @@ async function runComment(args, sub) {
|
|
|
565
554
|
}
|
|
566
555
|
throw new Error("Usage: agent-native recap comment <find-plan-id|upsert> --repo owner/name --issue n --token token");
|
|
567
556
|
}
|
|
557
|
+
/** Parse the last top-level JSON object from a possibly-noisy stdout dump. */
|
|
558
|
+
function parseLastJsonObject(text) {
|
|
559
|
+
const trimmed = text.trim();
|
|
560
|
+
if (!trimmed)
|
|
561
|
+
return null;
|
|
562
|
+
try {
|
|
563
|
+
return JSON.parse(trimmed);
|
|
564
|
+
}
|
|
565
|
+
catch {
|
|
566
|
+
/* fall through to line-by-line */
|
|
567
|
+
}
|
|
568
|
+
const lines = trimmed.split("\n");
|
|
569
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
570
|
+
const line = lines[i].trim();
|
|
571
|
+
if (!line.startsWith("{"))
|
|
572
|
+
continue;
|
|
573
|
+
try {
|
|
574
|
+
return JSON.parse(line);
|
|
575
|
+
}
|
|
576
|
+
catch {
|
|
577
|
+
/* keep scanning earlier lines */
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Claude Code `-p --output-format json` prints one final result object with a
|
|
584
|
+
* `usage` block and `total_cost_usd`. Anthropic's `input_tokens` already
|
|
585
|
+
* EXCLUDES cache tokens, so no normalization is needed here.
|
|
586
|
+
*/
|
|
587
|
+
export function parseClaudeUsage(stdout) {
|
|
588
|
+
const obj = parseLastJsonObject(stdout);
|
|
589
|
+
const u = obj?.usage;
|
|
590
|
+
if (!u)
|
|
591
|
+
return null;
|
|
592
|
+
const model = typeof obj?.model === "string"
|
|
593
|
+
? obj.model
|
|
594
|
+
: obj?.modelUsage && typeof obj.modelUsage === "object"
|
|
595
|
+
? Object.keys(obj.modelUsage)[0]
|
|
596
|
+
: undefined;
|
|
597
|
+
return {
|
|
598
|
+
inputTokens: Number(u.input_tokens ?? 0),
|
|
599
|
+
outputTokens: Number(u.output_tokens ?? 0),
|
|
600
|
+
cacheReadTokens: Number(u.cache_read_input_tokens ?? 0),
|
|
601
|
+
cacheWriteTokens: Number(u.cache_creation_input_tokens ?? 0),
|
|
602
|
+
model,
|
|
603
|
+
reportedCostUsd: typeof obj?.total_cost_usd === "number" ? obj.total_cost_usd : undefined,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
/** Pull the last usage object out of a Codex `exec --json` JSONL stream. */
|
|
607
|
+
function lastCodexUsage(jsonl) {
|
|
608
|
+
let last;
|
|
609
|
+
for (const line of jsonl.split("\n")) {
|
|
610
|
+
const trimmed = line.trim();
|
|
611
|
+
if (!trimmed.startsWith("{"))
|
|
612
|
+
continue;
|
|
613
|
+
let obj;
|
|
614
|
+
try {
|
|
615
|
+
obj = JSON.parse(trimmed);
|
|
616
|
+
}
|
|
617
|
+
catch {
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
// turn.completed carries `usage`; token_count events nest it under
|
|
621
|
+
// `info.total_token_usage`. Accept whichever the pinned Codex emits.
|
|
622
|
+
const u = obj?.usage ??
|
|
623
|
+
obj?.turn?.usage ??
|
|
624
|
+
obj?.msg?.usage ??
|
|
625
|
+
obj?.info?.total_token_usage ??
|
|
626
|
+
obj?.payload?.info?.total_token_usage;
|
|
627
|
+
if (u && (u.input_tokens != null || u.total_tokens != null))
|
|
628
|
+
last = u;
|
|
629
|
+
}
|
|
630
|
+
return last;
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Codex `exec --json` reports `input_tokens` INCLUSIVE of `cached_input_tokens`
|
|
634
|
+
* (OpenAI counts cached as a subset of prompt tokens) and bills
|
|
635
|
+
* `reasoning_output_tokens` separately. Normalize to the cache-exclusive shape
|
|
636
|
+
* `calculateCost` expects: strip cached out of input, fold reasoning into
|
|
637
|
+
* output. Without this, cached tokens are billed twice and reasoning is dropped.
|
|
638
|
+
*/
|
|
639
|
+
export function parseCodexUsage(jsonl) {
|
|
640
|
+
const u = lastCodexUsage(jsonl);
|
|
641
|
+
if (!u)
|
|
642
|
+
return null;
|
|
643
|
+
const cached = Number(u.cached_input_tokens ?? 0);
|
|
644
|
+
const input = Number(u.input_tokens ?? 0) - cached;
|
|
645
|
+
return {
|
|
646
|
+
inputTokens: Math.max(0, input),
|
|
647
|
+
outputTokens: Number(u.output_tokens ?? 0) + Number(u.reasoning_output_tokens ?? 0),
|
|
648
|
+
cacheReadTokens: cached,
|
|
649
|
+
cacheWriteTokens: 0, // Codex has no separate cache-write token charge
|
|
650
|
+
model: typeof u.model === "string" ? u.model : undefined,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* `recap usage` — parse the agent's run output for token usage and POST it to
|
|
655
|
+
* the plan app's record-recap-usage action so the recap row carries cost. The
|
|
656
|
+
* publish token is only ever sent to the trusted --app-url origin (the plan id
|
|
657
|
+
* is parsed from the untrusted agent-written plan URL but never forwarded).
|
|
658
|
+
*/
|
|
659
|
+
async function runUsage(args) {
|
|
660
|
+
const done = (obj) => process.stdout.write(`${JSON.stringify(obj)}\n`);
|
|
661
|
+
const planUrl = stringArg(args, "plan-url");
|
|
662
|
+
const planId = planIdFromUrl(planUrl);
|
|
663
|
+
const agent = (optionalArg(args, "agent") ?? "claude").toLowerCase();
|
|
664
|
+
const appUrl = optionalArg(args, "app-url");
|
|
665
|
+
const token = optionalArg(args, "token");
|
|
666
|
+
if (!planId) {
|
|
667
|
+
done({ ok: false, reason: `could not parse plan id from ${planUrl}` });
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (!appUrl || !token) {
|
|
671
|
+
done({ ok: false, reason: "missing --app-url or --token" });
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
let parsed = null;
|
|
675
|
+
try {
|
|
676
|
+
const raw = fs.readFileSync(path.resolve(stringArg(args, "result-file")), "utf8");
|
|
677
|
+
parsed = agent === "codex" ? parseCodexUsage(raw) : parseClaudeUsage(raw);
|
|
678
|
+
}
|
|
679
|
+
catch (err) {
|
|
680
|
+
done({ ok: false, reason: `could not read/parse usage: ${String(err)}` });
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
if (!parsed) {
|
|
684
|
+
done({ ok: false, reason: "no usage found in agent output" });
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
// The Claude result carries the model; Codex usually does not, so fall back to
|
|
688
|
+
// the pinned --model (VISUAL_RECAP_MODEL) and finally the documented default.
|
|
689
|
+
const model = parsed.model ??
|
|
690
|
+
optionalArg(args, "model") ??
|
|
691
|
+
(agent === "codex" ? "gpt-5.5" : "claude");
|
|
692
|
+
const body = {
|
|
693
|
+
planId,
|
|
694
|
+
...(agent === "codex" || agent === "claude" ? { agent } : {}),
|
|
695
|
+
model,
|
|
696
|
+
inputTokens: parsed.inputTokens,
|
|
697
|
+
outputTokens: parsed.outputTokens,
|
|
698
|
+
cacheReadTokens: parsed.cacheReadTokens,
|
|
699
|
+
cacheWriteTokens: parsed.cacheWriteTokens,
|
|
700
|
+
...(parsed.reportedCostUsd != null
|
|
701
|
+
? { reportedCostUsd: parsed.reportedCostUsd }
|
|
702
|
+
: {}),
|
|
703
|
+
};
|
|
704
|
+
try {
|
|
705
|
+
const base = appUrl.replace(/\/$/, "");
|
|
706
|
+
const res = await fetch(`${base}/_agent-native/actions/record-recap-usage`, {
|
|
707
|
+
method: "POST",
|
|
708
|
+
headers: {
|
|
709
|
+
"content-type": "application/json",
|
|
710
|
+
authorization: `Bearer ${token}`,
|
|
711
|
+
},
|
|
712
|
+
body: JSON.stringify(body),
|
|
713
|
+
});
|
|
714
|
+
if (!res.ok) {
|
|
715
|
+
const detail = await res.text().catch(() => "");
|
|
716
|
+
done({
|
|
717
|
+
ok: false,
|
|
718
|
+
reason: `record-recap-usage failed ${res.status}: ${detail.slice(0, 300)}`,
|
|
719
|
+
});
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
done({ ok: true, planId, ...body });
|
|
723
|
+
}
|
|
724
|
+
catch (err) {
|
|
725
|
+
done({ ok: false, reason: `record-recap-usage error: ${String(err)}` });
|
|
726
|
+
}
|
|
727
|
+
}
|
|
568
728
|
const HELP = `agent-native recap — PR visual recap helpers (used by the GitHub Action)
|
|
569
729
|
|
|
570
730
|
Usage:
|
|
571
731
|
agent-native recap scan --diff <path>
|
|
572
732
|
agent-native recap build-prompt --pr <n> [--head <sha>] [--app-url <url>] [--diff <path>] [--stat <path>] [--prev-plan-id <id>] [--huge] [--out <path>]
|
|
573
733
|
agent-native recap shot --url <planUrl> [--token <planToken>] [--app-url <url>] [--out recap.png]
|
|
734
|
+
agent-native recap usage --plan-url <planUrl> --result-file <path> --app-url <url> --token <planToken> [--agent claude|codex] [--model <id>]
|
|
574
735
|
agent-native recap comment <find-plan-id|upsert> --repo owner/name --issue <n> --token <github-token>
|
|
575
736
|
`;
|
|
576
737
|
export async function runRecap(argv) {
|
|
@@ -586,6 +747,9 @@ export async function runRecap(argv) {
|
|
|
586
747
|
case "shot":
|
|
587
748
|
await runShot(args);
|
|
588
749
|
return;
|
|
750
|
+
case "usage":
|
|
751
|
+
await runUsage(args);
|
|
752
|
+
return;
|
|
589
753
|
case "comment":
|
|
590
754
|
await runComment(parseArgs(rest.slice(1)), rest[0] ?? "");
|
|
591
755
|
return;
|