Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
485e0a7
fix: disable auto-translation on workflow editor to prevent React rec…
joelorzet Apr 15, 2026
e5a3fef
fix: guard wallet RPC calls against undefined result and retry transi…
joelorzet Apr 20, 2026
6f77e7a
refactor: extract wallet RPC client with jitter, breadcrumbs, and add…
joelorzet Apr 20, 2026
cc0f88e
feat: fail over wallet balance fetches to secondary RPC when primary …
joelorzet Apr 20, 2026
f35ac72
chore: move wallet rpc test to tests/unit to match project convention
joelorzet Apr 20, 2026
a99e9a4
fix: KEEP-287 prevent duplicate edges between same nodes
suisuss Apr 21, 2026
3a17441
fix: KEEP-289 gate pane context menu to workflow routes
suisuss Apr 21, 2026
feb05cc
fix: KEEP-287 dedupe AI-generated edges before apply/persist
suisuss Apr 21, 2026
76b55cf
feat(nav): reorganize left nav and user menu
eskp Apr 21, 2026
fa65323
feat(billing): landing-style redesign, billing details card, status p…
eskp Apr 21, 2026
f6e1bdf
Merge pull request #900 from KeeperHub/feat/KEEP-287-duplicate-node-c…
suisuss Apr 21, 2026
c26960b
docs: KEEP-289 explain why pane context menu is gated on route
suisuss Apr 21, 2026
23e7c2c
feat(banner): add skinny dismissible announcement banner for paid plans
eskp Apr 21, 2026
650449f
fix: KEEP-290 hide delete option on trigger nodes
suisuss Apr 21, 2026
158c272
Merge pull request #901 from KeeperHub/fix/KEEP-289-disable-landing-n…
suisuss Apr 21, 2026
0807fef
Merge pull request #903 from KeeperHub/feat/KEEP-290-remove-trigger-d…
suisuss Apr 21, 2026
78f05cc
fix: KEEP-291 remount config panel subtree on node selection change
suisuss Apr 21, 2026
3191e33
docs: KEEP-291 explain why config panel subtree is keyed on selected …
suisuss Apr 21, 2026
3ca1e25
fix: KEEP-288 guard onConnectEnd node creation with connectionState.i…
suisuss Apr 21, 2026
aa1cb9e
Merge pull request #905 from KeeperHub/fix/KEEP-288-node-created-on-c…
suisuss Apr 21, 2026
d61add4
Merge pull request #904 from KeeperHub/fix/KEEP-291-refresh-config-pa…
suisuss Apr 21, 2026
0886233
fix: KEEP-293 make PR CI build tolerant of missing ECR credentials fo…
suisuss Apr 21, 2026
e3ea437
Merge pull request #906 from KeeperHub/fix/KEEP-293-dependabot-pr-bui…
suisuss Apr 21, 2026
f064dc7
chore(deps): bump dompurify
dependabot[bot] Apr 21, 2026
2c2550b
fix(bazaar): emit discoverable/category/tags + fix resource URL
eskp Apr 21, 2026
2bd9b1f
Merge pull request #889 from KeeperHub/dependabot/npm_and_yarn/docs-s…
suisuss Apr 21, 2026
4e22073
chore(chain-id): migrate legacy Tempo chain ID 42420 to 4217
eskp Apr 21, 2026
173801b
docs: add paid-workflows and agentcash-install guides for dual-chain …
eskp Apr 21, 2026
20622b4
feat: add Aave V4 protocol definition (Lido Spoke)
suisuss Apr 14, 2026
044f5eb
feat(earnings): per-chain breakdown + docs tooltip on KPI cards
eskp Apr 21, 2026
f5378ab
refactor: rename Aave V3 slug from "aave" to "aave-v3"
suisuss Apr 15, 2026
25db005
test: add Aave V4 on-chain integration tests (mainnet)
suisuss Apr 16, 2026
752a38d
feat: add getUserAccountData to Aave V4 protocol definition
suisuss Apr 16, 2026
e1ddb44
fix: restrict protocol chain selector to deployed chains only
suisuss Apr 16, 2026
8895862
feat: link Aave V4 field tooltips to official docs
suisuss Apr 16, 2026
fa690bc
test(x402-call-route): update db mock chain for leftJoin on tags
eskp Apr 21, 2026
6c6fe54
fix: remove write-action outputs that resolve to undefined at runtime
suisuss Apr 21, 2026
c425e14
docs(agent-wallets): reframe as wallet-neutral + call out agentcash c…
eskp Apr 21, 2026
baa08ef
docs(billing): explain 2s billing details refresh heuristic
suisuss Apr 21, 2026
5b83078
fix: gate write-action outputs in UI layer, not protocol overrides
suisuss Apr 21, 2026
7f9baba
test(mcp-meta-tools): update db mock chain for leftJoin on tags
eskp Apr 21, 2026
aaffc10
fix(billing): prefer active subscription in getBillingDetails cascade
suisuss Apr 21, 2026
2778600
fix: broaden aave slug migration to cover event triggers and integrat…
suisuss Apr 21, 2026
b00f33b
test(billing): cover getBillingDetails cascade tiers
suisuss Apr 21, 2026
095e866
test: strengthen aave v4 integration test assertions
suisuss Apr 21, 2026
cfc6dcf
test: accept clean-success as valid outcome for write integration tests
suisuss Apr 21, 2026
84212d5
Merge pull request #846 from KeeperHub/feat/aave-v4-integration
suisuss Apr 21, 2026
a0f1c19
Merge pull request #899 from KeeperHub/fix/wallet-balance-rpc-retry
eskp Apr 21, 2026
422b903
Merge branch 'staging' into fix/workflow-editor-translate-crash
eskp Apr 21, 2026
3a9bdd0
fix(mcp): make call_workflow wait for read completion (KEEP-265)
eskp Apr 21, 2026
f2f8d72
Merge pull request #864 from KeeperHub/fix/workflow-editor-translate-…
eskp Apr 21, 2026
322c420
Merge pull request #902 from KeeperHub/feature/billing-landing-redesi…
eskp Apr 21, 2026
a5aaa68
Merge pull request #907 from KeeperHub/simon/keep-294-bazaar-discovery
eskp Apr 21, 2026
a172eca
Merge pull request #908 from KeeperHub/simon/keep-261-tempo-chain-id-…
eskp Apr 21, 2026
fb387e0
Merge pull request #909 from KeeperHub/simon/keep-259-dual-chain-docs
eskp Apr 21, 2026
b7bf7b2
Merge pull request #910 from KeeperHub/simon/keep-259-dual-chain-earn…
eskp Apr 21, 2026
17f9562
Merge pull request #912 from KeeperHub/simon/keep-265-call_workflow-s…
eskp Apr 21, 2026
23edead
fix(analytics): account for app-banner height in page padding
eskp Apr 21, 2026
e780212
Merge pull request #914 from KeeperHub/fix/analytics-banner-padding
eskp Apr 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/agents/protocol-domain.md
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,7 @@ EVM chains with Etherscan-compatible explorer configs (safe for seed workflows):
- "11155111" -- Sepolia Testnet (Etherscan Sepolia)

Non-EVM / Blockscout chains with explorer configs (NOT safe for protocol seed workflows -- different API format):
- "42420" -- Tempo (Blockscout)
- "4217" -- Tempo (Blockscout)
- "42429" -- Tempo Testnet (Blockscout)
- "101" -- Solana (Solscan)
- "103" -- Solana Devnet (Solscan)
Expand Down
4 changes: 2 additions & 2 deletions .claude/commands/test-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Read the protocol definition file:
protocols/$ARGUMENTS.ts
```

If it does not exist, check alternate names (e.g., `aave-v3.ts` for `aave`, `compound-v3.ts` for `compound`, `uniswap-v3.ts` for `uniswap`, `yearn-v3.ts` for `yearn`).
If it does not exist, check alternate names (e.g., `compound-v3.ts` for `compound`, `uniswap-v3.ts` for `uniswap`, `yearn-v3.ts` for `yearn`). Aave versions use explicit slugs: `aave-v3.ts` has slug `aave-v3`, `aave-v4.ts` has slug `aave-v4`.

Extract:
- Protocol name, slug, and description
Expand Down Expand Up @@ -132,7 +132,7 @@ Save tested workflow configurations to `scripts/seed/workflows/$ARGUMENTS/` for

After write tests, create and execute withdrawal workflows to recover deposited funds:
- `vault-withdraw` / `vault-redeem` for ERC-4626 vaults
- Protocol-specific withdraw actions (e.g., `aave/withdraw`, `compound/withdraw`)
- Protocol-specific withdraw actions (e.g., `aave-v3/withdraw`, `compound/withdraw`)
- Run withdrawals **sequentially** (same nonce contention concern)

Verify final balances match expectations.
Expand Down
21 changes: 19 additions & 2 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,23 @@ jobs:
- name: Run check
run: pnpm check

- name: Forbid legacy Tempo chain ID 42420
# Canonical Tempo mainnet chain ID is 4217 (see lib/rpc/types.ts).
# 42420 is a retired legacy ID; re-introducing it silently routes to
# the wrong RPC entry or mismatches the chains table. See KEEP-261.
# Uses git grep on the checked-out tree -- no user input is interpolated.
run: |
MATCHES=$(git grep -nE '\b42420\b' -- \
':!*.md' \
':!drizzle/meta/' \
':!.github/workflows/pr-checks.yml' \
|| true)
if [ -n "$MATCHES" ]; then
echo "::error::Legacy Tempo chain ID 42420 found. Use 4217 instead (KEEP-261)."
echo "$MATCHES"
exit 1
fi

typecheck:
runs-on: ubuntu-latest
steps:
Expand Down Expand Up @@ -143,15 +160,15 @@ jobs:
uses: docker/setup-buildx-action@v4

- name: Configure AWS credentials
if: steps.changes.outputs.relevant == 'true'
if: steps.changes.outputs.relevant == 'true' && github.actor != 'dependabot[bot]'
uses: aws-actions/configure-aws-credentials@v6
with:
aws-access-key-id: ${{ secrets.TO_AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.TO_AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.TO_REGION }}

- name: Login to AWS ECR
if: steps.changes.outputs.relevant == 'true'
if: steps.changes.outputs.relevant == 'true' && github.actor != 'dependabot[bot]'
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2

Expand Down
44 changes: 44 additions & 0 deletions app/api/billing/billing-details/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { NextResponse } from "next/server";
import { isBillingEnabled } from "@/lib/billing/feature-flag";
import { getOrgSubscription } from "@/lib/billing/plans-server";
import { getBillingProvider } from "@/lib/billing/providers";
import { requireOrgOwner } from "@/lib/billing/require-org-owner";
import { ErrorCategory, logSystemError } from "@/lib/logging";

export async function GET(): Promise<NextResponse> {
if (!isBillingEnabled()) {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}

try {
const authResult = await requireOrgOwner();
if ("error" in authResult) {
return authResult.error;
}
const { orgId: activeOrgId } = authResult;

const sub = await getOrgSubscription(activeOrgId);
if (!sub?.providerCustomerId) {
return NextResponse.json({
paymentMethod: null,
billingEmail: null,
});
}

const provider = getBillingProvider();
const details = await provider.getBillingDetails(sub.providerCustomerId);

return NextResponse.json(details);
} catch (error) {
logSystemError(
ErrorCategory.EXTERNAL_SERVICE,
"[Billing] Billing details error",
error,
{ endpoint: "/api/billing/billing-details", operation: "get" }
);
return NextResponse.json(
{ error: "Failed to load billing details" },
{ status: 500 }
);
}
}
25 changes: 15 additions & 10 deletions app/api/mcp/workflows/[slug]/call/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { start } from "workflow/api";
import { checkConcurrencyLimit } from "@/app/api/execute/_lib/concurrency-limit";
import { enforceExecutionLimit } from "@/lib/billing/execution-guard";
import { db } from "@/lib/db";
import { workflowExecutions, workflows } from "@/lib/db/schema";
import { tags, workflowExecutions, workflows } from "@/lib/db/schema";
import { ErrorCategory, logSystemError } from "@/lib/logging";
import { checkIpRateLimit, getClientIp } from "@/lib/mcp/rate-limit";
import { hashMppCredential } from "@/lib/mpp/server";
Expand All @@ -15,6 +15,7 @@ import {
} from "@/lib/payments/router";
import { executeWorkflow } from "@/lib/workflow-executor.workflow";
import type { WorkflowEdge, WorkflowNode } from "@/lib/workflow-store";
import { buildCallCompletionResponse } from "@/lib/x402/execution-wait";
import {
hashPaymentSignature,
recordPayment,
Expand Down Expand Up @@ -146,8 +147,9 @@ function startExecutionInBackground(
}

/**
* Free-path helper: prepares the execution and starts it. Used by the
* non-paid call path where there is no payment to record between the two.
* Free-path helper: prepares the execution, starts it, and awaits completion
* up to the read-wait timeout. Returns the mapped output inline on success or
* falls back to `{executionId, status: "running"}` on timeout.
*/
async function createAndStartExecution(
workflow: CallRouteWorkflow,
Expand All @@ -158,16 +160,18 @@ async function createAndStartExecution(
return prepared.error;
}
startExecutionInBackground(workflow, body, prepared.executionId);
return NextResponse.json(
{ executionId: prepared.executionId, status: "running" },
{ headers: corsHeaders }
const responseBody = await buildCallCompletionResponse(
prepared.executionId,
workflow.outputMapping
);
return NextResponse.json(responseBody, { headers: corsHeaders });
}

async function lookupWorkflow(slug: string): Promise<CallRouteWorkflow | null> {
const rows = await db
.select(CALL_ROUTE_COLUMNS)
.select({ ...CALL_ROUTE_COLUMNS, tagName: tags.name })
.from(workflows)
.leftJoin(tags, eq(workflows.tagId, tags.id))
.where(and(eq(workflows.listedSlug, slug), eq(workflows.isListed, true)))
.limit(1);
return rows[0] ?? null;
Expand Down Expand Up @@ -333,10 +337,11 @@ async function handlePaidWorkflow(

startExecutionInBackground(workflow, body, executionId);

return NextResponse.json(
{ executionId, status: "running" },
{ headers: corsHeaders }
const responseBody = await buildCallCompletionResponse(
executionId,
workflow.outputMapping
);
return NextResponse.json(responseBody, { headers: corsHeaders });
};
}
);
Expand Down
2 changes: 2 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Analytics } from "@vercel/analytics/react";
import { SpeedInsights } from "@vercel/speed-insights/next";
import { Provider } from "jotai";
import { type ReactNode, Suspense } from "react";
import { AppBanner } from "@/components/app-banner";
import { AuthProvider } from "@/components/auth/provider";
import { KeeperHubExtensionLoader } from "@/components/extension-loader";
import { GitHubStarsLoader } from "@/components/github-stars-loader";
Expand Down Expand Up @@ -76,6 +77,7 @@ const RootLayout = ({ children }: RootLayoutProps) => (
<Suspense fallback={<GitHubStarsProvider stars={null} />}>
<GitHubStarsLoader />
</Suspense>
<AppBanner />
<LayoutContent>{children}</LayoutContent>
<Toaster />
<GlobalModals />
Expand Down
10 changes: 9 additions & 1 deletion app/workflows/[workflowId]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,13 @@ export async function generateMetadata({
}

export default function WorkflowLayout({ children }: WorkflowLayoutProps) {
return children;
// Opt out of in-browser auto-translation (Google Translate, Edge, etc.) on
// the editor. Translators replace text nodes with <font> wrappers, which
// breaks React's reconciler and throws NotFoundError on insertBefore /
// removeChild. Marketing / hub pages remain translatable.
return (
<div className="contents" translate="no">
{children}
</div>
);
}
2 changes: 1 addition & 1 deletion app/workflows/[workflowId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -880,7 +880,7 @@ const WorkflowEditor = ({ params }: WorkflowPageProps) => {
{/* Right panel overlay (desktop only) - only show if trigger exists */}
{!isMobile && hasTriggerNode && (
<div
className="pointer-events-auto absolute top-24 right-0 bottom-0 z-20 border-l bg-background transition-transform duration-300 ease-out lg:top-[60px]"
className="pointer-events-auto absolute top-[calc(6rem+var(--app-banner-height,0px))] right-0 bottom-0 z-20 border-l bg-background transition-transform duration-300 ease-out lg:top-[calc(60px+var(--app-banner-height,0px))]"
style={{
width: `${panelWidth}%`,
transform:
Expand Down
19 changes: 12 additions & 7 deletions components/ai-elements/prompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { toast } from "sonner";
import { Shimmer } from "@/components/ai-elements/shimmer";
import { Button } from "@/components/ui/button";
import { api } from "@/lib/api-client";
import { dedupeEdges } from "@/lib/workflow/edge-helpers";
import {
currentWorkflowIdAtom,
currentWorkflowNameAtom,
Expand Down Expand Up @@ -135,9 +136,10 @@ export function AIPrompt({ workflowId, onWorkflowCreated }: AIPromptProps) {
);
}

// Update the canvas incrementally
// Update the canvas incrementally. Dedupe in case the AI emitted
// duplicate edges (same source/sourceHandle -> target/targetHandle).
setNodes(partialData.nodes || []);
setEdges(validEdges);
setEdges(dedupeEdges(validEdges));
if (partialData.name) {
setCurrentWorkflowName(partialData.name);
}
Expand All @@ -153,11 +155,14 @@ export function AIPrompt({ workflowId, onWorkflowCreated }: AIPromptProps) {
console.log("[AI Prompt] Nodes:", workflowData.nodes?.length || 0);
console.log("[AI Prompt] Edges:", workflowData.edges?.length || 0);

// Use edges from workflow data with animated type
const finalEdges = (workflowData.edges || []).map((edge) => ({
...edge,
type: "animated",
}));
// Use edges from workflow data with animated type; dedupe before
// persisting so AI hallucinations don't leak duplicates into the DB.
const finalEdges = dedupeEdges(
(workflowData.edges || []).map((edge) => ({
...edge,
type: "animated",
}))
);

// Validate: check for blank/incomplete nodes
console.log("[AI Prompt] Validating nodes:", workflowData.nodes);
Expand Down
4 changes: 2 additions & 2 deletions components/analytics/analytics-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export function AnalyticsPage(): ReactNode {
return (
<div className="pointer-events-auto fixed inset-0 overflow-y-auto bg-sidebar">
<div className="transition-[margin-left] duration-200 ease-out md:ml-[var(--nav-sidebar-width,60px)]">
<div className="flex flex-col gap-6 p-6 pt-20">
<div className="flex flex-col gap-6 p-6 pt-[calc(5rem+var(--app-banner-height,0px))]">
<AnalyticsHeader onRefetch={refetch} />
<EmptyState />
</div>
Expand All @@ -118,7 +118,7 @@ export function AnalyticsPage(): ReactNode {
return (
<div className="pointer-events-auto fixed inset-0 overflow-y-auto bg-sidebar">
<div className="transition-[margin-left] duration-200 ease-out md:ml-[var(--nav-sidebar-width,60px)]">
<div className="flex flex-col gap-6 p-6 pt-20">
<div className="flex flex-col gap-6 p-6 pt-[calc(5rem+var(--app-banner-height,0px))]">
<AnalyticsHeader onRefetch={refetch} />

<KpiCards />
Expand Down
81 changes: 81 additions & 0 deletions components/app-banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"use client";

import { Info, X } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";

const STORAGE_KEY = "kh-billing-announce-v1";

export function AppBanner(): React.ReactElement | null {
const [mounted, setMounted] = useState(false);
const [dismissed, setDismissed] = useState(true);

useEffect(() => {
setMounted(true);
try {
const stored = window.localStorage.getItem(STORAGE_KEY);
setDismissed(stored === "1");
} catch {
setDismissed(false);
}
}, []);

useEffect(() => {
if (!mounted) {
return;
}
if (dismissed) {
document.documentElement.style.removeProperty("--app-banner-height");
} else {
document.documentElement.style.setProperty("--app-banner-height", "36px");
}
return (): void => {
document.documentElement.style.removeProperty("--app-banner-height");
};
}, [mounted, dismissed]);

function handleDismiss(): void {
try {
window.localStorage.setItem(STORAGE_KEY, "1");
} catch {
// localStorage unavailable; dismissal only lasts this session
}
setDismissed(true);
}

if (!mounted || dismissed) {
return null;
}

return (
<div
className="pointer-events-auto fixed top-0 right-0 left-0 z-[55] flex h-9 items-center justify-center border-b border-keeperhub-green/30 bg-keeperhub-green/10 px-12 text-sm backdrop-blur-sm"
data-testid="app-banner"
>
<p className="flex items-center gap-2 truncate text-foreground">
<Info
aria-hidden="true"
className="size-4 shrink-0 text-keeperhub-green-dark"
/>
<span className="truncate">
New Pro and Business plans unlock higher execution limits and gas
credits. Free stays free forever.{" "}
<Link
className="font-medium text-keeperhub-green-dark underline-offset-4 hover:underline"
href="/billing#plans-section"
>
See plans
</Link>
</span>
</p>
<button
aria-label="Dismiss announcement"
className="absolute right-3 shrink-0 rounded-md p-1 text-muted-foreground transition-colors hover:bg-keeperhub-green/10 hover:text-foreground"
onClick={handleDismiss}
type="button"
>
<X className="size-3.5" />
</button>
</div>
);
}
Loading
Loading