npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@stevysmith/agentk

v0.7.1

Published

A command palette for the agentic web — forked from cmdk with WebMCP primitives

Readme

agentk

A command palette for the agentic web. Extends cmdk with tool execution, auto-generated forms, WebMCP registration, and human-in-the-loop agent mode.

Define your tools once as JSON Schema. Users browse and execute them from a command palette. AI agents discover them via WebMCP. Both share the same interface — human-in-the-loop by design, with an opt-in approval gate.

On-page agent experiences usually mean high fidelity at the cost of a bespoke integration to maintain. agentk collapses that trade: one tool catalog gives humans a palette and agents a WebMCP surface, so the on-page fidelity comes free with the UI you were building anyway.

Live demo · npm — running in production on the Stacktree dashboard, where the same catalog serves the ⌘K palette and in-browser agents.

Install

npm install @stevysmith/agentk

Use

import { Command } from '@stevysmith/agentk'

const tools = [
  {
    name: 'search',
    label: 'Search Products',
    description: 'Find products by keyword',
    inputSchema: {
      type: 'object',
      properties: {
        query: { type: 'string', description: 'Search query' },
      },
      required: ['query'],
    },
  },
]

async function executeTool(name: string, params: Record<string, any>) {
  // your app logic
}

const App = () => {
  const [open, setOpen] = React.useState(false)

  return (
    <Command.Dialog
      open={open}
      onOpenChange={setOpen}
      tools={tools}
      onToolExecute={executeTool}
      label="Command Menu"
    >
      <Command.Input placeholder="Type a command or search..." />
      <Command.List>
        <Command.Group heading="Tools">
          {tools.map((tool) => (
            <Command.Tool key={tool.name} tool={tool} />
          ))}
        </Command.Group>
        <Command.Empty>No results found.</Command.Empty>
      </Command.List>
      <Command.ToolForm />
      <Command.ToolResult />
    </Command.Dialog>
  )
}

When a user selects a tool, agentk transitions through a built-in lifecycle: browse → form → executing → result. Forms are generated automatically from inputSchemaenum fields render as dropdowns, number fields with minimum/maximum render as sliders, and string fields render as text inputs.

With agent mode

Add the agent prop and three more primitives to enable natural language intent → tool execution:

<Command.Dialog
  open={open}
  onOpenChange={setOpen}
  tools={tools}
  onToolExecute={executeTool}
  agent={{
    provider: 'anthropic',
    endpoint: '/api/agent',
    requireApproval: true,
  }}
  label="Command Menu"
>
  <Command.Input placeholder="Type a command or search..." />
  <Command.List>
    <Command.Group heading="Tools">
      {tools.map((tool) => (
        <Command.Tool key={tool.name} tool={tool} />
      ))}
    </Command.Group>
    <Command.Empty>No results found.</Command.Empty>
  </Command.List>
  <Command.AgentHint />
  <Command.Approval />
  <Command.ToolForm />
  <Command.ToolResult />
  <Command.ActivityFeed />
</Command.Dialog>

When the user types a query that doesn't match any tool, AgentHint appears prompting them to press Enter. The query is sent to the LLM, which returns a plan of tool calls. The user reviews and approves the plan in Approval before any tools execute.

Multi-step runs

By default the agent is single-shot: it plans once, those calls run, and the run ends — the model never sees the results. Set maxSteps to let it work in steps:

agent={{
  provider: 'anthropic',
  endpoint: '/api/agent',
  requireApproval: true,
  autoApproveReadOnly: true,  // reads run freely; changes still stop for a human
  maxSteps: 6,
}}

After each plan's calls finish, the results — and any errors — go back to the model, which either calls more tools or replies with text to finish. That is what makes "plan me a tour, then walk me through it" work instead of stopping after the first lookup, and it lets the model recover from a tool error rather than halting on it: a message like No ticket for the Van Gogh Room ($0.02). buy_ticket("van-gogh") first. is exactly the hint it needs.

The follow-up prompt is plain text (the original request plus a transcript of calls and results), so custom providerFn agents get multi-step for free — no message-format work. maxSteps defaults to 1 precisely because scripted providers that return the same plan for the same prompt would otherwise loop.

What stops for a human

Read vs. write is a blunt line — drafting an email is a write, and stopping for it is noise; sending it is not the same thing at all. agentk reads three tiers off your WebMCP annotations:

| annotation on the tool | what it means | runs without asking when | |---|---|---| | readOnlyHint: true | doesn't change anything | autoApproveReadOnly | | neither | a write you wouldn't think twice about | autoApproveReversible | | consequentialHint: true | significant, real-world or non-reversible — paying, sending, booking | never, under either flag |

A plan is judged as a whole: one consequential call and the whole plan waits. Only requireApproval: false — the app saying it wants no gate at all — skips a consequential call, because an explicit off switch is the developer's to keep.

consequentialHint is off by default, per the WebMCP spec: most tools just do what they say, and the hint only stays meaningful while it marks the exception.

On a phone

WebMCP is committed to all six Blink platforms — Android and Android WebView included — so the palette has to work under a thumb, not just a cursor. Three things follow, two of them handled for you.

A finger scrolling a list no longer re-selects it. onPointerMove still moves the selection for a mouse, and ignores pointerType === 'touch', where a pointer travelling down the list is a scroll and the row under it is incidental.

autoFocus is ignored on a touch device, because focusing the input opens the on-screen keyboard over the list the palette just opened to show. Pass autoFocusOnTouch when the keyboard is the point — a search-first palette where nobody browses:

<Command.Input autoFocus autoFocusOnTouch placeholder="Search…" />

The root carries data-touch when the primary pointer is coarse, so the touch layout keys off the pointer rather than a viewport width that says nothing about how the page is being operated. A tablet with a keyboard is not a phone, and a narrow desktop window is not a thumb.

The library ships no CSS, so the layout is yours. The one we'd start from is a bottom sheet — the input sits above the keyboard and results grow upward, so the row being read is never the row about to be covered:

[cmdk-root][data-touch] {
  position: fixed;
  inset: auto 0 0;
  max-height: 60dvh;                    /* dvh, so the keyboard shrinks it */
  border-radius: 16px 16px 0 0;
  padding-bottom: env(safe-area-inset-bottom);
  display: flex;
  flex-direction: column-reverse;       /* input at the bottom, list above */
}

[cmdk-root][data-touch] [cmdk-item] {
  min-height: 44px;                     /* a tap target, not a hover target */
}

There is no ⌘K on a phone and no convention to lean on, so a touch palette needs a visible way in — a button, or a persistent bar. That is the app's call: it costs screen real estate the library has no right to spend.

Parts and styling

All parts forward props and refs to an appropriate element. Each part has a specific data-attribute that can be used for styling.

Command [cmdk-root]

Root component. Inherits the full cmdk API and adds the following props:

| Prop | Type | Description | |------|------|-------------| | tools | AgentKToolDef[] | Tool definitions to register in the palette | | onToolExecute | (name, params) => Promise<string \| Record> | Called when a tool is executed. Return a string for text display, an object for JSON. | | onToolResult | (name, result) => void | Called after successful execution | | onToolError | (name, error) => void | Called on execution failure | | onModeChange | (mode) => void | Called when mode changes | | agent | AgentKAgentConfig | LLM agent configuration (omit to disable) | | onAgentPlan | (plan) => void | Called when the LLM returns a plan | | onAgentApprove | (plan) => void | Called when user approves a plan | | onAgentReject | (plan) => void | Called when user rejects a plan |

All standard cmdk props (value, onValueChange, filter, shouldFilter, loop, label) are also supported.

The root element exposes data-agentk-mode reflecting the current state machine mode. Use it for mode-aware styling:

/* Hide tool list when a form, result, or execution is active */
[data-agentk-mode="form"] [cmdk-list]      { display: none; }
[data-agentk-mode="executing"] [cmdk-list]  { display: none; }
[data-agentk-mode="result"] [cmdk-list]     { display: none; }
[data-agentk-mode="planning"] [cmdk-list]   { display: none; }
[data-agentk-mode="approval"] [cmdk-list]   { display: none; }

Possible values: browse, form, executing, result, planning, approval. Works alongside data-agentk-entering and data-agentk-exiting for transition animations.

Dialog [cmdk-dialog] [cmdk-overlay]

Composes Radix UI's Dialog. Props are forwarded to Command.

<Command.Dialog open={open} onOpenChange={setOpen}>
  ...
</Command.Dialog>

Input [cmdk-input]

Search input. All props forwarded to the underlying input element.

<Command.Input placeholder="Search tools..." />

List [cmdk-list]

Contains Tool items and groups. Animate height using the --cmdk-list-height CSS variable.

[cmdk-list] {
  height: var(--cmdk-list-height);
  transition: height 100ms ease;
}

Tool [cmdk-item] [data-agentk-tool]

Renders a selectable tool item. Selecting it transitions to the form view.

// Default rendering: icon + name + description
<Command.Tool tool={tool} />

// Custom rendering: children fully replace the default layout
<Command.Tool tool={tool}>
  <MyCustomIcon />
  <div>
    <strong>{tool.label}</strong>
    <p>{tool.description}</p>
  </div>
</Command.Tool>

When children are omitted, the default layout renders data-agentk-tool-icon, data-agentk-tool-name, and data-agentk-tool-description elements for styling. When children are provided, only your children render.

The tool prop is an AgentKToolDef:

type AgentKToolDef = {
  name: string
  label?: string           // Falls back to humanized name
  description?: string
  inputSchema?: {          // JSON Schema for parameters
    type: 'object'
    properties: Record<string, {
      type: string
      description?: string
      enum?: string[]      // → renders dropdown
      minimum?: number     // → renders slider (with maximum)
      maximum?: number
      default?: any
    }>
    required?: string[]
  }
  icon?: React.ReactNode
  keywords?: string[]      // Aliases for fuzzy matching
  annotations?: {          // WebMCP ToolAnnotations, forwarded to registerTool
    readOnlyHint?: boolean
    untrustedContentHint?: boolean
  }
}

ToolForm [data-agentk-form]

Auto-generates a parameter form from the active tool's inputSchema. Renders when mode is form.

// Default: auto-generated fields
<Command.ToolForm />

// Custom field renderer
<Command.ToolForm
  renderField={(name, schema, value, onChange) => (
    <MyCustomInput value={value} onChange={onChange} />
  )}
/>

// Custom action buttons (Cancel + Execute)
<Command.ToolForm
  renderActions={({ cancel, submit, canSubmit }) => (
    <div>
      <button onClick={cancel}>Cancel</button>
      <button onClick={submit} disabled={!canSubmit}>Run it</button>
    </div>
  )}
/>

Schema type mapping:

| Schema | Rendered as | |--------|-------------| | type: 'string' | Text input | | type: 'string', enum: [...] | Select dropdown | | type: 'number', minimum, maximum | Range slider | | type: 'number' | Number input | | type: 'boolean' | Checkbox |

ToolResult [data-agentk-result]

Displays the result after tool execution. Renders when mode is result.

The return value of onToolExecute controls what is displayed:

onToolExecute={async (name, params) => {
  const data = await myApi(name, params);
  // String → rendered as text in <span data-agentk-result-data="">
  return `Found ${data.length} results`;
  // Object → rendered as formatted JSON in <pre data-agentk-result-data="">
  // return { count: data.length, items: data };
}}
// Default display
<Command.ToolResult />

// Custom result renderer
<Command.ToolResult
  renderResult={(execution) => (
    <div>
      {execution.error
        ? <span>Error: {execution.error}</span>
        : <span>Done in {((Date.now() - execution.startedAt) / 1000).toFixed(1)}s</span>
      }
    </div>
  )}
/>

// Custom dismiss button
<Command.ToolResult
  renderDismiss={({ dismiss }) => (
    <button onClick={dismiss}>Got it</button>
  )}
/>

// Auto-dismiss successful results after 6s
<Command.ToolResult autoDismissAfterMs={6000} />

autoDismissAfterMs only fires for successful results — errors stay visible until the user dismisses them manually. Combine with onModeChange if you need side effects (e.g. navigation) when the panel closes.

The execution object:

type ToolExecution = {
  toolName: string
  parameters: Record<string, any>
  result?: any
  error?: string
  startedAt: number
}

AgentHint [data-agentk-agent-hint]

Appears when the search query doesn't match any tool but an agent is configured. Interactive — clicking, pressing Enter, or pressing Space triggers sendIntent with the current search query. Renders with role="button" and tabIndex={0} for accessibility.

// Default: "Ask the agent" with sparkle icon
<Command.AgentHint />

// Custom content
<Command.AgentHint>
  <span>Let AI handle this</span>
</Command.AgentHint>

Set data-agentk-hint on [cmdk-root] is toggled automatically when the hint is visible, useful for styling the input border:

[cmdk-root][data-agentk-hint] [cmdk-input] {
  border-bottom-color: var(--accent);
}

Approval [data-agentk-approval]

Renders the agent's plan for user review before execution. Shows each proposed tool call with parameters. The user can approve or reject.

// Default display
<Command.Approval />

// Custom renderers
<Command.Approval
  renderSummary={(plan) => <p>{plan.summary}</p>}
  renderCall={(call, index) => (
    <div>{call.toolName}({JSON.stringify(call.parameters)})</div>
  )}
  renderActions={({ approve, reject }) => (
    <div>
      <button onClick={reject}>Cancel</button>
      <button onClick={approve}>Run plan</button>
    </div>
  )}
/>

ActivityFeed [data-agentk-activity]

Shows a timeline of agent activity: intent detection, planning, tool execution, results.

<Command.ActivityFeed maxEntries={20} />

IntentTrigger [data-agentk-intent-trigger]

A Command.Item that triggers sendIntent when selected, instead of the default tool-selection behaviour. Renders identically to other items — same styling, same keyboard navigation.

<Command.IntentTrigger query="summer programs in europe">
  Search Europe
</Command.IntentTrigger>

Must be rendered inside a Command.List. Style with [data-agentk-intent-trigger].

Note: Since IntentTrigger is a Command.Item, it counts as a matching item for filtering. If you want AgentHint to appear when the user types a custom query, place IntentTrigger items alongside other items so they get filtered out by cmdk's fuzzy matching. For agent-only search (no items), use AgentHint directly without IntentTrigger.

Empty [cmdk-empty]

Renders when there are no results. Automatically hidden when AgentHint is visible.

Group [cmdk-group]

Groups items with a heading. Same as cmdk.

<Command.Group heading="Actions">
  <Command.Tool tool={tool} />
</Command.Group>

Separator, Loading

Same as cmdk. See cmdk documentation.

Hooks

useAgentK()

Access agentk state from within the Command tree.

function MyComponent() {
  const ak = useAgentK()

  // Read state
  ak.state.mode        // 'browse' | 'form' | 'executing' | 'result' | 'planning' | 'approval'
  ak.state.activeTool  // current tool or null
  ak.state.parameters  // current form values
  ak.state.execution   // current execution or null
  ak.agentHintVisible  // true when agent hint is showing

  // Actions
  ak.selectTool(tool)
  ak.setParameter('key', value)
  ak.execute()
  ak.reset()
  ak.sendIntent('natural language query')
  ak.approvePlan()
  ak.rejectPlan()
}

useWebMCPRegistration(tools, onToolExecute, options?)

Registers the same catalog with the browser's WebMCP surface (document.modelContext, falling back to navigator.modelContext) so in-browser agents can call your tools. Polls briefly if the API arrives late (origin trials, inspector extensions), unregisters on unmount, and returns { active }.

const { active } = useWebMCPRegistration(tools, executeTool, { prefix: 'shop_' })

Each tool is registered with its name (prefixed), label as title, description, inputSchema and annotations. Mark tools that don't change state with annotations: { readOnlyHint: true } — agents use it to decide when to ask the user before calling. execute results are returned as text content; a thrown error comes back with isError: true so the agent can tell a failure from a success that mentions the word "error".

The hook re-registers when the tools array changes, so the catalog can follow page state (a walk tool only inside a room, buy_ticket only for rooms not yet held). Changes are deferred while a call is in flight: before Chrome 153, unregistering a tool aborts an execution still running on it, so a tool whose own result changes the catalog would fail its own call. The hook waits for the last in-flight execution to return, lets the browser deliver the result, then applies the change. Keep the array referentially stable between real changes (useMemo keyed on the state that matters).

Host differences it papers over. ChatGPT's in-app browser exposes only document.modelContext (no navigator.modelContext) and enforces the spec's object argument for executeTool; shipping Chrome exposes both and also accepts a JSON string. agentk registers on whichever exists and sends an object with a string fallback, so the same page works in both. Verified against ChatGPT desktop and Chrome 151.

useWebMCPTools()

Discover tools registered by other apps on the page via WebMCP.

const { tools, available, refresh, executeTool } = useWebMCPTools()
// tools: AgentKToolDef[] — discovered tools
// available: boolean — whether WebMCP API is present
// refresh: () => void — re-scan for tools
// executeTool: (name, params) => Promise<any>

useCommandState(state => state.field)

Same as cmdk. Access the underlying combobox state.

Agent configuration

The agent prop accepts:

type AgentKAgentConfig = {
  provider: 'anthropic' | 'openai' | 'google' | 'custom'
  apiKey?: string         // For development only — warns in browser
  endpoint?: string       // Proxy URL for production
  model?: string          // Defaults: claude-sonnet-5, gpt-4o, gemini-3.5-flash
  systemPrompt?: string   // Override the built-in system prompt
  requireApproval?: boolean  // Show Approval before executing (default: false)
  maxCalls?: number       // Max tool calls per plan
  providerFn?: AgentKProvider  // Custom provider function
}

For production, proxy through your own server using the endpoint prop instead of exposing API keys client-side:

agent={{
  provider: 'anthropic',
  endpoint: '/api/agent',
  requireApproval: true,
}}

Custom provider

agent={{
  provider: 'custom',
  providerFn: async (prompt, tools, config) => {
    const res = await fetch('/my-api', {
      method: 'POST',
      body: JSON.stringify({ prompt, tools }),
    })
    return res.json() // { calls: AgentKToolCall[], summary: string }
  },
}}

WebMCP registration

agentk tools use the same JSON Schema format as WebMCP. Register them so AI agents can discover your app. Note that Chrome 150 moved the API from navigator.modelContext to document.modelContext; feature-detect both:

useEffect(() => {
  const mc = document.modelContext ?? navigator.modelContext
  if (!mc) return

  for (const tool of tools) {
    mc.registerTool({
      name: tool.name,
      description: tool.description,
      inputSchema: tool.inputSchema,
      execute: async (params) => {
        const result = await executeTool(tool.name, params)
        return { content: [{ type: 'text', text: JSON.stringify(result) }] }
      },
    })
  }

  return () => {
    for (const tool of tools) {
      mc.unregisterTool(tool.name)
    }
  }
}, [])

Define once, use everywhere — the same tool definitions power the palette UI, the agent, and WebMCP discovery.

Recipes

Consumer search (agent-only, no tool list)

For customer-facing search where users type natural language and get results — no tool browsing needed:

<Command.Dialog
  open={open}
  onOpenChange={setOpen}
  tools={tools}
  onToolExecute={handleExecute}
  agent={{ provider: 'anthropic', endpoint: '/api/agent' }}
>
  <Command.Input placeholder="What are you looking for?" />
  <Command.List>
    {/* No Command.Tool items — agent-only */}
    <Command.AgentHint />
    <Command.Empty>Type to search with AI</Command.Empty>
  </Command.List>
  <Command.ToolResult />
</Command.Dialog>

Use data-agentk-mode to hide the list during execution:

[data-agentk-mode="executing"] [cmdk-list] { display: none; }
[data-agentk-mode="result"] [cmdk-list]    { display: none; }

Add suggested queries with IntentTrigger:

<Command.List>
  <Command.Group heading="Suggestions">
    <Command.IntentTrigger query="popular items">Popular items</Command.IntentTrigger>
    <Command.IntentTrigger query="deals under $50">Deals under $50</Command.IntentTrigger>
  </Command.Group>
  <Command.AgentHint />
</Command.List>

Data-attribute reference

Every part exposes data-attributes you can target from CSS without bundling styles. The cmdk-prefixed attrs come from cmdk; everything data-agentk-* is introduced by agentk.

Root and dialog

| Selector | Where | |---|---| | [cmdk-root] | Top-level element (Command / Command.Dialog) | | [cmdk-root][data-agentk-mode="<mode>"] | Reflects the current state machine mode (browse, form, executing, result, planning, approval) | | [cmdk-root][data-agentk-hint] | Present when AgentHint is showing | | [cmdk-root][data-touch] | Present when the primary pointer is coarse — style the touch layout off this, not a viewport width | | [cmdk-overlay] | Dialog backdrop | | [cmdk-dialog] | Dialog surface | | [cmdk-input] | Search input | | [cmdk-list] | List wrapper (animatable via --cmdk-list-height) | | [cmdk-group], [cmdk-group-heading] | Group + its heading | | [cmdk-empty] | Empty state |

Tool item

| Selector | Where | |---|---| | [cmdk-item][data-agentk-tool="<name>"] | The tool list item | | [data-agentk-tool-icon] | Default icon span (only when tool.icon is set) | | [data-agentk-tool-name] | Default label span | | [data-agentk-tool-description] | Default description span | | [data-agentk-intent-trigger] | A Command.IntentTrigger item |

ToolForm

| Selector | Where | |---|---| | [data-agentk-form] | Form container | | [data-agentk-form-invalid] | Set on the form when validation has errors | | [data-agentk-form-heading] | Header row (icon + title + description) | | [data-agentk-form-title] | Form title | | [data-agentk-form-description] | Form description | | [data-agentk-form-fields] | Wrapper around the field list | | [data-agentk-form-field] | Wrapper around a single field | | [data-agentk-form-field][data-agentk-field-error] | Field whose value is invalid | | [data-agentk-form-label] | Default field label | | [data-agentk-required] | Required-field marker (default *) | | [data-agentk-form-hint] | Field description / hint | | [data-agentk-field-error-message] | Inline validation message | | [data-agentk-form-actions] | Cancel + submit row | | [data-agentk-form-cancel] | Cancel button | | [data-agentk-form-submit] | Submit button |

ToolResult

| Selector | Where | |---|---| | [data-agentk-result] | Result container | | [data-agentk-result][data-agentk-executing] | Set during the executing phase | | [data-agentk-result-loading] | Spinner row during execution | | [data-agentk-progress] | "Step N of M" indicator during a chained plan | | [data-agentk-result][data-agentk-success] | Set when a successful result is shown | | [data-agentk-result][data-agentk-error] | Set when an error is shown | | [data-agentk-result-heading] | Result title row | | [data-agentk-result-body] | Result content wrapper | | [data-agentk-result-data] | The result value (string <span> or JSON <pre>) | | [data-agentk-result-error] | The error message <pre> | | [data-agentk-result-meta] | Meta row (e.g. duration) | | [data-agentk-result-dismiss] | Default dismiss button |

AgentHint

| Selector | Where | |---|---| | [data-agentk-agent-hint] | Hint container (clickable) | | [data-agentk-agent-hint-icon] | Default sparkle icon | | [data-agentk-agent-hint-content] | Label + query wrapper | | [data-agentk-agent-hint-label] | "Ask the agent" label | | [data-agentk-agent-hint-query] | The current search text in quotes | | [data-agentk-agent-hint-kbd] | Default kbd glyph |

Approval

| Selector | Where | |---|---| | [data-agentk-approval] | Approval container | | [data-agentk-approval-summary] | Plan summary line | | [data-agentk-approval-calls] | List of planned tool calls | | [data-agentk-approval-call] | A single planned tool call | | [data-agentk-approval-call-icon] | Tool icon (only when set) | | [data-agentk-approval-call-name] | Tool name | | [data-agentk-approval-call-params] | Parameter chips wrapper | | [data-agentk-approval-param] | Single parameter chip | | [data-agentk-approval-param-value] | Stringified parameter value | | [data-agentk-approval-actions] | Reject + approve row | | [data-agentk-approval-reject] | Reject button | | [data-agentk-approval-approve] | Approve button |

Planning / spinner

| Selector | Where | |---|---| | [data-agentk-planning] | Planning indicator container | | [data-agentk-planning-text] | "Thinking…" label | | [data-agentk-spinner] | The animated spinner element (also reused inside [data-agentk-result-loading]) |

ActivityFeed

| Selector | Where | |---|---| | [data-agentk-activity] | Feed container | | [data-agentk-activity][data-agentk-activity-expanded] | Set when the feed is expanded | | [data-agentk-activity-toggle] | Expand/collapse button | | [data-agentk-activity-status] | Latest status text | | [data-agentk-activity-chevron] | Chevron icon (with data-expanded when open) | | [data-agentk-activity-entry] | A feed entry | | [data-agentk-activity-entry][data-agentk-activity-type="<type>"] | Entry type (tool_start, tool_complete, tool_error, etc.) | | [data-agentk-activity-icon] | Per-entry icon | | [data-agentk-activity-message] | Per-entry message |

FAQ

cmdk compatible? Yes. agentk is a superset of cmdk. Existing cmdk code works unchanged — add tool props when you're ready.

Unstyled? Yes. All components expose data-attributes for styling. No CSS is bundled.

Which LLM providers? Anthropic, OpenAI, and Google (Gemini) are built in. Use provider: 'custom' with providerFn for anything else.

What is WebMCP? A browser API (document.modelContext, formerly navigator.modelContext) for registering tools that AI agents can discover. Chrome has it in origin trial. agentk makes your app WebMCP-ready.

Do I need WebMCP to use agentk? No. The command palette and tool execution work without it. WebMCP registration is opt-in.

Do I need an LLM to use agentk? No. Without the agent prop, agentk is a command palette with tool forms and execution — no AI required.

Auto-generated forms? Yes. inputSchema defines the form. enum → dropdown, number with min/max → slider, string → text input. Override with renderField for custom fields.

Human-in-the-loop? Set requireApproval: true and render <Command.Approval />. The agent's plan is shown to the user before any tools execute.

React 18+ only? Yes. Uses useId and useSyncExternalStore.

React server component? No, it's a client component.

Acknowledgements

Built on cmdk by Paco Coursey. Uses Radix UI primitives.