Token导航 LogoToken导航TokenDH.com
研究检索操作浏览器github未标认证来源可访问许可证需确认审计提醒

gloveglove 搜索

Agent Skill

glove 用于查找、检索和筛选相关信息,适合在 Codex、Claude、Cursor、Gemini CLI 中需要根据关键词、任务场景或来源线索快速定位候选结果时使用。可结合来源仓库、安装命令和原始 README 继续核验具体用法。安装前建议确认权限范围、维护状态,以及是否会触发联网、命令执行或文件读写。

总安装

685

周安装

28

GitHub Stars

7

下载量

220
CodexClaudeCursorGemini CLI

安装说明

本站只整理中文说明和来源信息,不托管安装包,也不代用户安装。

GitHub

来源数

2

许可证

unknown

最后核验

2026-05-01

来源状态

来源可访问

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:glove(glove 搜索)
来源仓库:https://github.com/porkytheblack/glove
仓库路径:skills/glove
安装命令:
npx skills add https://github.com/porkytheblack/glove --skill glove
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。该命令会通过 npx skills 从第三方来源获取 Skill;本站只展示命令,不托管安装包,也不自动执行。

skills.shnpx skills
npx skills add https://github.com/porkytheblack/glove --skill glove

简介

glove 用于查找、检索和筛选相关信息,支持关键词和任务场景匹配。

  • 适用于 Codex、Claude、Cursor、Gemini CLI 中的快速信息定位。
  • 结合来源仓库和原始 README 可进一步验证具体用法。
  • 安装命令:npx skills add https://github.com/porkytheblack/glove --skill glove。
  • 建议确认权限范围和维护状态,注意是否触发联网或文件读写。

SKILL.md

Glove Framework — Development Guide

You are an expert on the Glove framework. Use this knowledge when writing, debugging, or reviewing Glove code.

What Glove Is

Glove is an open-source TypeScript framework for building AI-powered applications. Users describe what they want in conversation, and an AI decides which capabilities (tools) to invoke. Developers define tools and renderers; Glove handles the agent loop.

Repository: https://github.com/porkytheblack/glove Docs site: https://glove.dterminal.net License: MIT (dterminal)

Package Overview

PackagePurposeInstall
glove-coreRuntime engine: agent loop, tool execution, display manager, model adapters (browser-safe — no native deps)pnpm add glove-core
glove-sqliteSqliteStore — persistent SQLite-backed store (server-side only, depends on better-sqlite3)pnpm add glove-sqlite
glove-reactReact hooks (useGlove), GloveClient, GloveProvider, defineTool, <Render>, MemoryStore, ToolConfig with colocated rendererspnpm add glove-react
glove-nextOne-line Next.js API route handler (createChatHandler) for streaming SSEpnpm add glove-next
glove-mcpBridge MCP servers into a Glove agent: mountMcp, connectMcp, bridgeMcpTool, McpAdapter, find_capability discovery subagent. Opt-in OAuth helpers at glove-mcp/oauth.pnpm add glove-mcp

Most projects need just glove-react + glove-next. glove-core is included as a dependency of glove-react. For server-side or non-React agents, use glove-core directly — see Server-Side Agents below. For agents that need third-party tools via the Model Context Protocol, see MCP Integration.

What's in the framework

  • glove-core — agent loop, tools, display stack, store/model/subscriber adapters, context compaction, inbox.
  • glove-react — colocated renderers via defineTool, <Render>, useGlove, MemoryStore, createRemoteStore, createEndpointModel, createRemoteModel.
  • glove-nextcreateChatHandler (one-line SSE route), voice token handler.
  • glove-sqliteSqliteStore for persistence (server-side only).
  • glove-voice — full-duplex voice pipeline: STT/TTS/VAD adapters, GloveVoice, useGloveVoice, useGlovePTT, <VoicePTTButton>.
  • glove-mcp — MCP servers as first-class tools: mountMcp, connectMcp, bridgeMcpTool, McpAdapter (consumer-supplied per-conversation seam). find_capability discovery subagent. Opt-in OAuth helpers at glove-mcp/oauth (runMcpOAuth, FsOAuthStore, MemoryOAuthStore, McpOAuthProvider).

Architecture at a Glance

User message → Agent Loop → Model decides tool calls → Execute tools → Feed results back → Loop until done
                                                          ↓
                                                   Display Stack (pushAndWait / pushAndForget)
                                                          ↓
                                                   React renders UI slots

Core Concepts

  • Agent — AI coordinator that replaces router/navigation logic. Reads tools, decides which to call.
  • Tool — A capability: name, description, inputSchema (Zod), do function, optional render + renderResult.
  • Display Stack — Stack of UI slots tools push onto. pushAndWait blocks tool; pushAndForget doesn't.
  • Display Strategy — Controls slot visibility lifecycle: "stay", "hide-on-complete", "hide-on-new".
  • renderData — Client-only data returned from do() that is NOT sent to the AI model. Used by renderResult for history rendering.
  • Adapter — Pluggable interfaces for Model, Store, DisplayManager, and Subscriber. Swap providers without changing app code.
  • Context Compaction — Auto-summarizes long conversations to stay within context window limits. The store preserves full message history (so frontends can display the entire chat), while Context.getMessages() splits at the last compaction summary so the model only sees post-compaction context. Summary messages are marked with is_compaction: true.
  • Inbox — Persistent async mailbox for cross-instance communication. An agent posts a request (text) that can't be resolved now; an external service resolves it later (text response). Resolved items are automatically injected into the agent's context on the next ask() call. Items can be blocking (agent should wait) or non-blocking. Built-in glove_post_to_inbox tool auto-registered when store supports inbox methods.
  • MCP catalogue + adapterglove-mcp introduces two pieces: a static McpCatalogueEntry[] describing servers the app supports, and a per-conversation McpAdapter holding active ids and resolving access tokens. mountMcp reloads previously active servers and folds in a find_capability discovery subagent — model finds and activates servers it needs mid-conversation.

Quick Start (Next.js)

1. Install

pnpm add glove-core glove-react glove-next zod

2. Server route

// app/api/chat/route.ts
import { createChatHandler } from "glove-next";

export const POST = createChatHandler({
  provider: "anthropic",     // or "openai", "openrouter", "gemini", etc.
  model: "claude-sonnet-4-20250514",
});

Set ANTHROPIC_API_KEY (or OPENAI_API_KEY, etc.) in .env.local.

3. Define tools with defineTool

// lib/glove.tsx
import { GloveClient, defineTool } from "glove-react";
import type { ToolConfig } from "glove-react";
import { z } from "zod";

const inputSchema = z.object({
  question: z.string().describe("The question to display"),
  options: z.array(z.object({
    label: z.string().describe("Display text"),
    value: z.string().describe("Value returned when selected"),
  })),
});

const askPreferenceTool = defineTool({
  name: "ask_preference",
  description: "Present options for the user to choose from.",
  inputSchema,
  displayPropsSchema: inputSchema,       // Zod schema for display props
  resolveSchema: z.string(),             // Zod schema for resolve value
  displayStrategy: "hide-on-complete",   // Hide slot after user responds
  async do(input, display) {
    const selected = await display.pushAndWait(input);  // typed!
    return {
      status: "success" as const,
      data: `User selected: ${selected}`,         // sent to AI
      renderData: { question: input.question, selected },  // client-only
    };
  },
  render({ props, resolve }) {           // typed props, typed resolve
    return (
      <div>
        <p>{props.question}</p>
        {props.options.map(opt => (
          <button key={opt.value} onClick={() => resolve(opt.value)}>
            {opt.label}
          </button>
        ))}
      </div>
    );
  },
  renderResult({ data }) {               // renders from history
    const { question, selected } = data as { question: string; selected: string };
    return <div><p>{question}</p><span>Selected: {selected}</span></div>;
  },
});

// Tools without display stay as raw ToolConfig
const getDateTool: ToolConfig = {
  name: "get_date",
  description: "Get today's date",
  inputSchema: z.object({}),
  async do() { return { status: "success", data: new Date().toLocaleDateString() }; },
};

export const gloveClient = new GloveClient({
  endpoint: "/api/chat",
  systemPrompt: "You are a helpful assistant.",
  tools: [askPreferenceTool, getDateTool],
  // getSessionId: () => fetch("/api/session").then(r => r.json()).then(d => d.id),
});

4. Provider + Render

// app/providers.tsx
"use client";
import { GloveProvider } from "glove-react";
import { gloveClient } from "@/lib/glove";

export function Providers({ children }: { children: React.ReactNode }) {
  return <GloveProvider client={gloveClient}>{children}</GloveProvider>;
}
// app/page.tsx — using <Render> component
"use client";
import { useGlove, Render } from "glove-react";

export default function Chat() {
  const glove = useGlove();

  return (
    <Render
      glove={glove}
      strategy="interleaved"
      renderMessage={({ entry }) => (
        <div><strong>{entry.kind === "user" ? "You" : "AI"}:</strong> {entry.text}</div>
      )}
      renderStreaming={({ text }) => <div style={{ opacity: 0.7 }}>{text}</div>}
    />
  );
}

Or use useGlove() directly for full manual control:

// app/page.tsx — manual rendering
"use client";
import { useState } from "react";
import { useGlove } from "glove-react";

export default function Chat() {
  const { timeline, streamingText, busy, slots, sendMessage, renderSlot, renderToolResult } = useGlove();
  const [input, setInput] = useState("");

  return (
    <div>
      {timeline.map((entry, i) => (
        <div key={i}>
          {entry.kind === "user" && <p><strong>You:</strong> {entry.text}</p>}
          {entry.kind === "agent_text" && <p><strong>AI:</strong> {entry.text}</p>}
          {entry.kind === "tool" && (
            <>
              <p>Tool: {entry.name} — {entry.status}</p>
              {entry.renderData !== undefined && renderToolResult(entry)}
            </>
          )}
        </div>
      ))}
      {streamingText && <p style={{ opacity: 0.7 }}>{streamingText}</p>}
      {slots.map(renderSlot)}
      <form onSubmit={(e) => { e.preventDefault(); sendMessage(input.trim()); setInput(""); }}>
        <input value={input} onChange={(e) => setInput(e.target.value)} disabled={busy} />
        <button type="submit" disabled={busy}>Send</button>
      </form>
    </div>
  );
}

Server-Side Agents

For CLI tools, backend services, WebSocket servers, or any non-browser environment, use glove-core directly. No React, Next.js, or browser required.

Minimal Setup

import { Glove, Displaymanager, createAdapter } from "glove-core";
import z from "zod";

// In-memory store (see MemoryStore below) or SqliteStore from glove-sqlite for persistence
const store = new MemoryStore("my-session");

const agent = new Glove({
  store,
  model: createAdapter({ provider: "anthropic", stream: true }),
  displayManager: new Displaymanager(),  // required but can be empty
  systemPrompt: "You are a helpful assistant.",
  serverMode: true,  // canonical "I am headless" flag — drives default permission gating + MCP discovery policy
  compaction_config: {
    compaction_instructions: "Summarize the conversation.",
  },
})
  .fold({
    name: "search",
    description: "Search the database.",
    inputSchema: z.object({ query: z.string() }),
    async do(input) {
      const results = await db.search(input.query);
      return { status: "success", data: results };
    },
  })
  .build();

const result = await agent.processRequest("Find recent orders");
console.log(result.messages[0]?.text);

Minimal MemoryStore

import type { StoreAdapter, Message } from "glove-core";

class MemoryStore implements StoreAdapter {
  identifier: string;
  private messages: Message[] = [];
  private tokenCount = 0;
  private turnCount = 0;

  constructor(id: string) { this.identifier = id; }

  async getMessages() { return this.messages; }
  async appendMessages(msgs: Message[]) { this.messages.push(...msgs); }
  async getTokenCount() { return this.tokenCount; }
  async addTokens(count: number) { this.tokenCount += count; }
  async getTurnCount() { return this.turnCount; }
  async incrementTurn() { this.turnCount++; }
  async resetCounters() { this.tokenCount = 0; this.turnCount = 0; }
}

For persistent storage: import {SqliteStore} from "glove-sqlite" then new SqliteStore({dbPath: "./agent.db", sessionId: "abc"}).

Key Differences from React

React (glove-react)Server-side (glove-core)
defineTool with render/renderResult.fold() with just do — no renderers needed
useGlove() hook manages stateCall agent.processRequest() directly
GloveClient + GloveProvidernew Glove({...}).build()
createEndpointModel (SSE client)createAdapter() or direct adapter (e.g. new AnthropicAdapter())
MemoryStore from glove-reactImplement StoreAdapter yourself or use SqliteStore from glove-sqlite

Tools Without Display

Most server-side tools ignore the display manager — just return a result:

gloveBuilder.fold({
  name: "get_weather",
  description: "Get weather for a city.",
  inputSchema: z.object({ city: z.string() }),
  async do(input) {
    const res = await fetch(`https://wttr.in/${input.city}?format=j1`);
    return { status: "success", data: await res.json() };
  },
});

Returning a plain string also works — auto-wrapped to {status: "success", data: yourString}.

Interactive Tools (pushAndWait)

When a tool calls display.pushAndWait(), the agent loop blocks until dm.resolve(slotId, value) is called. Wire this to your UI layer (WebSocket, terminal, Slack, etc.):

// Tool side
async do(input, display) {
  const confirmed = await display.pushAndWait({
    renderer: "confirm",
    input: { message: `Delete ${input.file}?` },
  });
  if (!confirmed) return { status: "error", data: null, message: "Cancelled" };
  // proceed...
}

// Server side — resolve when user responds
dm.resolve(slotId, true);

Subscribers (Logging, Forwarding)

import type { SubscriberAdapter } from "glove-core";

const logger: SubscriberAdapter = {
  async record(event_type, data) {
    if (event_type === "text_delta") process.stdout.write((data as any).text);
    if (event_type === "tool_use") console.log(`\n[tool] ${(data as any).name}`);
  },
};

gloveBuilder.addSubscriber(logger);

Common Patterns

  • CLI script: Build agent, call processRequest(), print result
  • Multi-turn REPL: Loop with readline, each processRequest() accumulates in the store
  • WebSocket server: Per-connection session with isolated store/dm/subscriber, forward events via record()
  • Background worker: Build agent per job, process from a queue, no display needed
  • Hot-swap model: Call agent.setModel(newAdapter) at runtime
  • MCP-backed agent: Set serverMode: true, call mountMcp(glove, {adapter, entries}) before build(). See MCP Integration.

Optional Store Features

  • Tasks (getTasks, addTasks, updateTask): Auto-registers glove_update_tasks tool
  • Permissions (getPermission, setPermission): Tools with requiresPermission: true check consent
  • Inbox (getInboxItems, addInboxItem, updateInboxItem, getResolvedInboxItems): Auto-registers glove_post_to_inbox tool. Enables async cross-instance communication.

If your store doesn't implement these, they're silently disabled.

Inbox (Async Mailbox)

The inbox enables agents to post requests that will be resolved later by external services — surviving across sessions and instances.

How It Works

  1. Agent calls glove_post_to_inbox with a tag, request text, and blocking flag
  2. Item persists in the store with status pending
  3. External service resolves the item (via SqliteStore.resolveInboxItem() from glove-sqlite, or store API)
  4. Next time agent.ask() runs, resolved items are injected as text messages and marked consumed
  5. Pending blocking items are surfaced as transient reminders (not persisted)
  6. Compaction preserves pending inbox items in the summary block

Built-in Tool: glove_post_to_inbox

Auto-registered when store implements inbox methods. Input schema:

{
  tag: string,       // Category label, e.g. "restock_watch"
  request: string,   // Natural language description of what needs to happen
  blocking: boolean, // Default false. If true, agent should wait for resolution
}

External Resolution

// From a background job, webhook handler, or cron:
import { SqliteStore } from "glove-sqlite";

SqliteStore.resolveInboxItem(
  "path/to/db.db",
  "inbox_item_id",
  "The item you requested is now available."  // text response
);

Or via REST if you've set up inbox API routes (see coffee example).

InboxItem Type

interface InboxItem {
  id: string;
  tag: string;
  request: string;
  response: string | null;
  status: "pending" | "resolved" | "consumed";
  blocking: boolean;
  created_at: string;
  resolved_at: string | null;
}

Store Methods (Optional)

// Add to StoreAdapter to enable inbox:
getInboxItems?(): Promise<InboxItem[]>
addInboxItem?(item: InboxItem): Promise<void>
updateInboxItem?(itemId: string, updates: Partial<Pick<InboxItem, "status" | "response" | "resolved_at">>): Promise<void>
getResolvedInboxItems?(): Promise<InboxItem[]>

All store implementations (SqliteStore from glove-sqlite, MemoryStore, createRemoteStore) support inbox.

React Integration

useGlove() returns inbox: InboxItem[] alongside tasks:

const { inbox, tasks, timeline, sendMessage } = useGlove({ tools, sessionId });

// Show pending watches in UI
{inbox.filter(i => i.status === "pending").map(item => (
  <div key={item.id}>{item.tag}: {item.request}</div>
))}

Remote Store Actions

When using createRemoteStore, add inbox actions to persist to your backend:

const storeActions: RemoteStoreActions = {
  // ...existing getMessages, appendMessages...
  getInboxItems: (sid) => fetch(`/api/sessions/${sid}/inbox`).then(r => r.json()),
  addInboxItem: (sid, item) => fetch(`/api/sessions/${sid}/inbox`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ item }) }),
  updateInboxItem: (sid, itemId, updates) => fetch(`/api/sessions/${sid}/inbox/update`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ itemId, updates }) }),
  getResolvedInboxItems: (sid) => fetch(`/api/sessions/${sid}/inbox/resolved`).then(r => r.json()),
};

MCP Integration (glove-mcp)

glove-mcp bridges Model Context Protocol servers (Notion, Gmail, Linear, Slack, an internal MCP wrapper around your own APIs, …) into a Glove agent so their tools appear in the model's tool list as ordinary Glove tools. Streamable HTTP transport only in v1.

When to use it

  • You need third-party capabilities a vendor already exposes via MCP — Notion, Gmail, Linear, Slack, Zapier-MCP, etc.
  • You have multiple internal services and want a single integration shape across them.
  • You want the agent to discover and activate capabilities mid-conversation rather than wiring all tools at startup.

If you control both ends and just need a few first-party tools, hand-rolled glove.fold(...) is still simpler. MCP earns its keep when the catalogue is large or the servers are not yours.

Mental model: catalogue + adapter

Two pieces, deliberately split:

  • McpCatalogueEntry[] — a static list authored at the application level. One entry per MCP server the app supports: id, name, description, url, tags?, metadata?. Identical across users. The id doubles as the tool namespace prefix and the activation key.
  • McpAdapter — a per-conversation interface the consumer implements (analogous to StoreAdapter). Holds the conversation's active server ids and resolves access tokens. interface McpAdapter {identifier: string; // for log correlation getActive(): Promise<string[]>; // ids active in this conversation activate(id: string): Promise<void>; // called by the discovery subagent deactivate(id: string): Promise<void>; // for the consumer's UI; v1 limitation: doesn't unload tools getAccessToken(id: string): Promise<string>; // SOLE auth seam — return a bearer string}

getAccessToken is the only auth seam. The framework wraps the returned string in Authorization: Bearer.... Token acquisition, refresh, and persistence are entirely the consumer's responsibility — env vars, vault, your own OAuth flow, the opt-in runMcpOAuth helper, all valid.

mountMcp — the canonical entry point

After new Glove(...) and before glove.build():

import { mountMcp } from "glove-mcp";

const glove = new Glove({ /* ... */ , serverMode: true });

await mountMcp(glove, {
  adapter,                                  // McpAdapter
  entries,                                  // McpCatalogueEntry[]
  ambiguityPolicy: { type: "auto-pick-best" },  // optional
  subagentModel: undefined,                 // optional — defaults to glove.model
  subagentSystemPrompt: undefined,          // optional — defaults to per-policy prompt
  clientInfo: { name: "My App", version: "1.0.0" },  // optional
});

glove.build();

What it does, in order:

  1. Reads adapter.getActive(), opens an MCP connection per active id (using getAccessToken), lists tools, and folds each one onto the main agent via bridgeMcpTool. Per-server reload failures are logged and skipped — a transient outage doesn't kill the agent.
  2. Folds in the find_capability discovery subagent so the model can activate more servers mid-conversation.

mountMcp returns when reload + discovery fold are complete. Call it before build() for the cleanest init order, but fold() after build() works too.

Bridged tool shape

bridgeMcpTool(connection, tool, serverMode) produces a GloveFoldArgs with these conventions:

  • Name: ${entry.id}__${tool.name} (e.g. notion__search). The __ separator (exported as MCP_NAMESPACE_SEP) is regex-safe across all model providers.
  • Schema: raw JSON Schema from the MCP server, passed via jsonSchema (no Zod). The MCP server is the source of truth.
  • requiresPermission: in serverMode always false; otherwise true unless the MCP tool annotates readOnlyHint: true.
  • Result: server content[] text is joined into data (what the model sees); the full content[] is also passed through as renderData so React renderers can use it.
  • Auth-expired contract: any 401-shaped error during callTool is mapped to {status: "error", message: "auth_expired", data: null}. Detect this from the conversation log, refresh your token, and the next call picks up the new value via getAccessToken.

Discovery (find_capability) and ambiguity policies

mountMcp folds in a single tool the model can call: find_capability. It takes a brief need description, spins up a tiny subagent (with its own DiscoveryMemoryStore, inheriting the main agent's model and displayManager), and gives the subagent four tools:

  • list_capabilities(query?, tags?) — substring search the catalogue.
  • activate(id) — connect, bridge tools onto the *main* agent, persist active state. Tools become available to the main model on its next turn.
  • deactivate(id) — flip persisted state. v1 limitation: tools stay loaded until session refresh.
  • ask_user(question, options) — only registered under the interactive policy. Renders via the mcp_picker renderer on the main displayManager.

The ambiguity policy controls what happens when the subagent finds multiple plausible matches:

PolicyBehaviorWhen to use
{type: "interactive"}Subagent calls ask_user via pushAndWait. Requires an mcp_picker renderer on your displayManager.Browser UIs / chat apps with a renderer wired up. Default when serverMode: false.
{type: "auto-pick-best"}Subagent always picks the highest-ranked match. No human in the loop.Headless / server-side / CLI. Default when serverMode: true.
{type: "defer-to-main"}Subagent returns the candidate list as text and lets the main agent decide what to activate.Multi-MCP discovery flows where the main model has more conversation context than the subagent.

serverMode: true on the Glove config is the canonical "I am headless" flag — drives both the default ambiguity policy and the default requiresPermission on bridged tools (never gate).

Auth model — bearer-only

The framework only knows about static bearer tokens. connectMcp ships an auth: bearer(token | () => token) helper; pass either a string or a thunk that resolves a fresh token per connection. mountMcp and the discovery activate tool both use the thunk form so every connection re-reads getAccessToken.

import { bearer, connectMcp } from "glove-mcp";

const conn = await connectMcp({
  namespace: "notion",
  url: "https://mcp.notion.com/mcp",
  auth: bearer(() => adapter.getAccessToken("notion")),
  clientInfo: { name: "My App", version: "1.0.0" },
});

auth_expired contract

Mid-call, an expired token surfaces as {status: "error", message: "auth_expired"} on the bridged tool result. The framework does not refresh tokens. Your app must:

  1. Detect auth_expired on the conversation log (subscriber tool_use_result event, or post-hoc).
  2. Refresh / re-auth via whatever mechanism owns the credential.
  3. Update your store; the next bridged call pulls a fresh token from getAccessToken.

For UI consumers this is usually a "Reconnect Notion" toast. For CLIs, instructing the user to re-run the auth command is normal.

glove-mcp/oauth — opt-in OAuth tooling

If you don't already have an OAuth flow, the glove-mcp/oauth subpath ships a small reference implementation built on the MCP authorization spec:

  • runMcpOAuth(opts) — one call, end-to-end flow. Spins up a local listener on http://localhost:53683/callback (configurable), drives the SDK through DCR (or skips it via preRegisteredClient), opens the user's browser, exchanges the code for tokens, and verifies via listTools (or a callTool of your choice). Used by the examples/mcp-cli/*-mcp-auth.ts scripts.
  • FsOAuthStore / MemoryOAuthStoreOAuthStore implementations. FsOAuthStore writes a single JSON file with mode 0600 and atomic temp+rename. Replace with your DB for production.
  • McpOAuthProvider — lower-level OAuthClientProvider for advanced consumers driving auth() from the SDK directly.
  • buildClientMetadata, MCP_DEFAULT_CLIENT_INFO, emptyOAuthState — small helpers.

Consumers who already have tokens (env vars, internal integrations, vault, an existing OAuth setup) can ignore this subpath entirely — getAccessToken returns the bearer, full stop.

See api-reference.md — glove-mcp/oauth for full type signatures, and examples.md — Pattern: MCP OAuth flow for a worked example.

Production lift-and-shift

The reference examples/mcp-cli setup is a single-user Node CLI; production typically wants:

  • Multi-user store — replace FsOAuthStore with a per-user OAuthStore backed by your DB. The interface is three methods (get, set, delete).
  • OAuth flow in route handlersGET /oauth/<id>/start calls runMcpOAuth (or the lower-level SDK auth() directly), GET /oauth/<id>/callback finishes it. Same machinery, different invocation. The local-listener flavour of runMcpOAuth is convenient for CLIs but not what you want behind a load balancer.
  • Background refresh — refresh expired tokens however your stack does it; getAccessToken just reads the latest bearer string.
  • Persistent active state — the McpAdapter shown in examples uses an in-memory Set for active ids. In production, persist active ids per conversation (alongside messages) so reload after restart actually does something.

The agent code itself doesn't change — McpAdapter.getAccessToken is the only seam.

Quick reference — where things live

NeedSymbol
Mount MCP onto an agentmountMcp(glove, {adapter, entries,...})
Implement consumer adapterMcpAdapter interface
Author catalogue entriesMcpCatalogueEntry
One-off connect (preflight, custom flow)connectMcp({namespace, url, auth})
Bridge a tool by handbridgeMcpTool(connection, tool, serverMode)
Bearer header helper`bearer(token
Discovery subagent factorydiscoveryTool({adapter, entries, ambiguityPolicy})
Tool namespace separatorMCP_NAMESPACE_SEP ("__")
401 detection on raw connectUnauthorizedError
Run the OAuth flowrunMcpOAuth(opts) from glove-mcp/oauth
Persist OAuth stateFsOAuthStore, MemoryOAuthStore from glove-mcp/oauth
Build client metadatabuildClientMetadata(opts) from glove-mcp/oauth

Display Stack Patterns

pushAndForget — Show results (non-blocking)

async do(input, display) {
  const data = await fetchData(input);
  await display.pushAndForget({ input: data }); // Shows UI, tool continues
  return { status: "success", data: "Displayed results", renderData: data };
},
render({ data }) {
  return <Card>{data.title}</Card>;
},
renderResult({ data }) {
  return <Card>{(data as any).title}</Card>;  // Same card from history
},

pushAndWait — Collect user input (blocking)

async do(input, display) {
  const confirmed = await display.pushAndWait({ input }); // Pauses until user responds
  return {
    status: "success",
    data: confirmed ? "Confirmed" : "Cancelled",
    renderData: { confirmed },
  };
},
render({ data, resolve }) {
  return (
    <div>
      <p>{data.message}</p>
      <button onClick={() => resolve(true)}>Yes</button>
      <button onClick={() => resolve(false)}>No</button>
    </div>
  );
},
renderResult({ data }) {
  const { confirmed } = data as { confirmed: boolean };
  return <div>{confirmed ? "Confirmed" : "Cancelled"}</div>;
},

Display Strategies

StrategyBehaviorUse for
"stay" (default)Slot always visibleInfo cards, results
"hide-on-complete"Hidden when slot is resolvedForms, confirmations, pickers
"hide-on-new"Hidden when newer slot from same tool appearsCart summaries, status panels

SlotRenderProps

PropTypeDescription
dataTInput passed to pushAndWait/pushAndForget
resolve(value: unknown) => voidResolves the slot. For pushAndWait, the value returns to do. For pushAndForget, use resolve() or removeSlot(id) to dismiss.
reject(reason?: string) => voidRejects the slot. For pushAndWait, this causes the promise to reject. Use for cancellation flows.

Tool Definition

defineTool (recommended for tools with UI)

import { defineTool } from "glove-react";

const tool = defineTool({
  name: string,
  description: string,
  inputSchema: z.ZodType,              // Zod schema for tool input
  displayPropsSchema?: z.ZodType,      // Zod schema for display props (recommended for tools with UI)
  resolveSchema?: z.ZodType,           // Zod schema for resolve value (omit for pushAndForget-only)
  displayStrategy?: SlotDisplayStrategy,
  requiresPermission?: boolean,
  unAbortable?: boolean,                 // Tool runs to completion even if abort signal fires (e.g. voice barge-in)
  do(input, display): Promise<ToolResultData>,  // display is TypedDisplay<D, R>
  render?({ props, resolve, reject }): ReactNode,
  renderResult?({ data, output, status }): ReactNode,
});

Key points:

  • do() should return {status, data, renderData}data goes to model, renderData stays client-only
  • render() gets typed props (matching displayPropsSchema) and typed resolve (matching resolveSchema)
  • renderResult() receives renderData for showing read-only views from history
  • displayPropsSchema is optional but recommended — tools without display should use raw ToolConfig

ToolConfig (for tools without UI or manual control)

interface ToolConfig<I = any> {
  name: string;
  description: string;
  inputSchema?: z.ZodType<I>;          // Optional now — tools may use jsonSchema instead
  jsonSchema?: Record<string, unknown>; // Raw JSON Schema alternative (used by MCP-bridged tools)
  do: (input: I, display: ToolDisplay) => Promise<ToolResultData>;
  render?: (props: SlotRenderProps) => ReactNode;
  renderResult?: (props: ToolResultRenderProps) => ReactNode;
  displayStrategy?: SlotDisplayStrategy;
  requiresPermission?: boolean;
  unAbortable?: boolean;
}

jsonSchema vs inputSchema: Pass exactly one. inputSchema (Zod) gets local validation before do() runs. jsonSchema (raw JSON Schema) is forwarded verbatim to the model and the executor skips Zod validation — the source of truth lives elsewhere. Used by bridgeMcpTool where the MCP server defines the schema, but you can use it directly when wrapping any external tool catalogue.

glove.fold after build()

fold() is legal at any time on an IGloveRunnable, including after build(). The discovery subagent's activate tool relies on this — it folds in newly bridged MCP tools mid-conversation so they're available on the next turn. Useful for any "register tools dynamically" pattern.

const agent = new Glove({...}).build();
// ...later, mid-conversation:
agent.fold({ name: "new_tool", description: "...", inputSchema: z.object({}), async do() { ... } });

do(input, display, glove) — third argument

A tool's do function now receives the running IGloveRunnable as a third argument. This is how find_capability's discovery subagent reaches back to fold tools onto the main agent and to inherit its model/displayManager. Most tools ignore this.

ToolResultData

interface ToolResultData {
  status: "success" | "error";
  data: unknown;          // Sent to the AI model
  message?: string;       // Error message (for status: "error")
  renderData?: unknown;   // Client-only — NOT sent to model, used by renderResult
}

Important: Model adapters explicitly strip renderData before sending to the AI. This makes it safe to store sensitive client-only data (e.g., email addresses, UI state) in renderData.

<Render> Component

Headless render component that replaces manual timeline rendering:

import { Render } from "glove-react";

<Render
  glove={gloveHandle}           // return value of useGlove()
  strategy="interleaved"        // "interleaved" | "slots-before" | "slots-after" | "slots-only"
  renderMessage={({ entry, index, isLast }) => ...}
  renderToolStatus={({ entry, index, hasSlot }) => ...}
  renderStreaming={({ text }) => ...}
  renderInput={({ send, busy, abort }) => ...}
  renderSlotContainer={({ slots, renderSlot }) => ...}
  as="div"                      // wrapper element
  className="chat"
/>

Features:

  • Automatic slot visibility based on displayStrategy
  • Automatic renderResult rendering for completed tools with renderData
  • Interleaving: slots appear inline next to their tool call
  • Sensible defaults for all render props

GloveHandle Interface

The interface consumed by <Render>, returned by useGlove():

interface GloveHandle {
  timeline: TimelineEntry[];
  streamingText: string;
  busy: boolean;
  sessionReady: boolean;
  sessionId: string;
  slots: EnhancedSlot[];
  sendMessage: (text: string, images?: { data: string; media_type: string }[]) => void;
  abort: () => void;
  renderSlot: (slot: EnhancedSlot) => ReactNode;
  renderToolResult: (entry: ToolEntry) => ReactNode;
  resolveSlot: (slotId: string, value: unknown) => void;
  rejectSlot: (slotId: string, reason?: string) => void;
}

useGlove Hook Return

PropertyTypeDescription
timelineTimelineEntry[]Messages + tool calls
streamingTextstringCurrent streaming buffer
busybooleanAgent is processing
sessionReadybooleanfalse while async getSessionId resolves; always true if not configured
sessionIdstringThe resolved session ID
isCompactingbooleanContext compaction in progress (driven by compaction_start/compaction_end events)
slotsEnhancedSlot[]Active display stack with metadata
tasksTask[]Agent task list
inboxInboxItem[]Inbox items (pending, resolved, consumed)
statsGloveStats{turns, tokens_in, tokens_out}
sendMessage(text, images?)voidSend user message
abort()voidCancel current request
renderSlot(slot)ReactNodeRender a display slot
renderToolResult(entry)ReactNodeRender a tool result from history
resolveSlot(id, value)voidResolve a pushAndWait slot
rejectSlot(id, reason?)voidReject a pushAndWait slot

TimelineEntry

type TimelineEntry =
  | { kind: "user"; text: string; images?: string[] }
  | { kind: "agent_text"; text: string }
  | { kind: "tool"; id: string; name: string; input: unknown; status: "running" | "success" | "error"; output?: string; renderData?: unknown };

type ToolEntry = Extract<TimelineEntry, { kind: "tool" }>;

Supported Providers

ProviderEnv VariableDefault ModelSDK Format
openaiOPENAI_API_KEYgpt-4.1openai
anthropicANTHROPIC_API_KEYclaude-sonnet-4-20250514anthropic
openrouterOPENROUTER_API_KEYanthropic/claude-sonnet-4openai
geminiGEMINI_API_KEYgemini-2.5-flashopenai
minimaxMINIMAX_API_KEYMiniMax-M2.5openai
kimiMOONSHOT_API_KEYkimi-k2.5openai
glmZHIPUAI_API_KEYglm-4-plusopenai

Pre-built Tool Registry

Available at https://glove.dterminal.net/tools — copy-paste into your project:

  • confirm_action — Yes/No confirmation dialog
  • collect_form — Multi-field form
  • ask_preference — Single-select preference picker
  • text_input — Free-text input
  • show_info_card — Info/success/warning card (pushAndForget)
  • suggest_options — Multiple-choice suggestions
  • approve_plan — Step-by-step plan approval

Voice Integration (glove-voice)

Package Overview

PackagePurposeInstall
glove-voiceVoice pipeline: GloveVoice, adapters (STT/TTS/VAD), AudioCapture, AudioPlayerpnpm add glove-voice
glove-react/voiceReact hooks: useGloveVoice, useGlovePTT, VoicePTTButtonIncluded in glove-react
glove-nextToken handlers: createVoiceTokenHandler (already in glove-next, no separate import)Included in glove-next

Architecture

Mic → VAD → STTAdapter → glove.processRequest() → TTSAdapter → Speaker

GloveVoice wraps a Glove instance with a full-duplex voice pipeline. Glove remains the intelligence layer — all tools, display stack, and context management work normally. STT and TTS are swappable adapters. Text tokens stream through a SentenceBuffer into TTS in real-time.

Quick Start (Next.js + ElevenLabs)

Step 1: Token routes — server-side handlers that exchange your API key for short-lived tokens

// app/api/voice/stt-token/route.ts
import { createVoiceTokenHandler } from "glove-next";
export const GET = createVoiceTokenHandler({ provider: "elevenlabs", type: "stt" });
// app/api/voice/tts-token/route.ts
import { createVoiceTokenHandler } from "glove-next";
export const GET = createVoiceTokenHandler({ provider: "elevenlabs", type: "tts" });

Set ELEVENLABS_API_KEY in .env.local.

Step 2: Client voice config

// app/lib/voice.ts
import { createElevenLabsAdapters } from "glove-voice";

async function fetchToken(path: string): Promise<string> {
  const res = await fetch(path);
  const data = await res.json();
  return data.token;
}

export const { stt, createTTS } = createElevenLabsAdapters({
  getSTTToken: () => fetchToken("/api/voice/stt-token"),
  getTTSToken: () => fetchToken("/api/voice/tts-token"),
  voiceId: "JBFqnCBsd6RMkjVDRZzb",
});

Step 3: SileroVAD — dynamic import for SSR safety

export async function createSileroVAD() {
  const { SileroVADAdapter } = await import("glove-voice/silero-vad");
  const vad = new SileroVADAdapter({
    positiveSpeechThreshold: 0.5,
    negativeSpeechThreshold: 0.35,
    wasm: { type: "cdn" },
  });
  await vad.init();
  return vad;
}

Step 4: React hook

const { runnable } = useGlove({ tools, sessionId });
const voice = useGloveVoice({ runnable, voice: { stt, createTTS, vad } });
// voice.mode, voice.isActive, voice.isMuted, voice.error, voice.transcript
// voice.start(), voice.stop(), voice.interrupt(), voice.commitTurn()
// voice.mute(), voice.unmute()              — gate mic audio to STT/VAD
// voice.narrate("text")                     — speak text via TTS without model (returns Promise)

startMuted Config Option

In manual (push-to-talk) mode, the pipeline now starts muted by default — no need to call mute() immediately after start(). This eliminates the race condition where audio leaks in the gap.

// Manual mode auto-mutes — just works
await voice.start(); // already muted in manual mode

// Explicit override
const voice = useGloveVoice({
  runnable,
  voice: { stt, createTTS, turnMode: "manual", startMuted: false }, // opt out
});

enabled State on useGloveVoice

The hook now exposes voice.enabled — tracks user intent (true after start(), false after stop() or pipeline death). Replaces the manual useState + sync useEffect pattern:

// Before — consumer tracks + syncs
const [voiceEnabled, setVoiceEnabled] = useState(false);
useEffect(() => {
  if (voiceEnabled && !voice.isActive) setVoiceEnabled(false);
}, [voiceEnabled, voice.isActive]);

// After — hook tracks it
voice.enabled  // auto-resets on pipeline death

useGlovePTT Hook (Push-to-Talk)

High-level hook that encapsulates the entire PTT lifecycle. Reduces ~80 lines of boilerplate to ~5 lines:

import { useGlovePTT } from "glove-react/voice";

const glove = useGlove({ endpoint: "/api/chat", tools });
const ptt = useGlovePTT({
  runnable: glove.runnable,
  voice: { stt, createTTS },    // turnMode forced to "manual"
  hotkey: "Space",               // default, auto-guards INPUT/TEXTAREA
  holdThreshold: 300,            // click-vs-hold discrimination (ms)
  minRecordingMs: 350,           // min audio before committing
});

// ptt.enabled      — is the pipeline active
// ptt.recording    — is the user currently holding
// ptt.processing   — is STT finalizing
// ptt.mode         — voice mode (idle/listening/thinking/speaking)
// ptt.transcript   — partial transcript while recording
// ptt.error        — last error
// ptt.toggle()     — enable/disable the pipeline
// ptt.interrupt()  — barge-in
// ptt.bind         — { onPointerDown, onPointerUp, onPointerLeave }

<button {...ptt.bind}><MicIcon /></button>

<VoicePTTButton> Component

Headless (unstyled) component with render prop for the mic button:

import { VoicePTTButton } from "glove-react/voice";

<VoicePTTButton ptt={ptt}>
  {({ enabled, recording, mode }) => (
    <button className={recording ? "active" : ""}>
      <MicIcon />
      {enabled && <StatusDot />}
    </button>
  )}
</VoicePTTButton>

Includes click-vs-hold discrimination, pointer leave safety, and aria attributes.

<Render> Voice Support

<Render> accepts an optional voice prop to auto-render transcript and voice status:

<Render
  glove={glove}
  voice={ptt}                    // or useGloveVoice() return
  renderTranscript={...}         // optional custom renderer
  renderVoiceStatus={...}        // optional custom renderer
  renderInput={() => null}
/>

Turn Modes

ModeBehaviorUse for
"vad" (default)Auto speech detection + barge-inHands-free, voice-first apps
"manual"Push-to-talk, explicit commitTurn()Noisy environments, precise control

Narration + Mic Control

  • voice.narrate(text) — Speak arbitrary text through TTS without the model. Resolves when audio finishes. Auto-mutes mic during narration. Abortable via interrupt(). Safe to call from pushAndWait tool handlers.
  • voice.mute() / voice.unmute() — Gate mic audio forwarding to STT/VAD. audio_chunk events still fire when muted (for visualization).
  • audio_chunk event — Raw Int16Array PCM from the mic, emitted even when muted. Use for waveform/level visualization.
  • Compaction silence — Voice automatically ignores text_delta during context compaction so the summary is never narrated.

Voice-First Tool Design

  • Use pushAndForget instead of pushAndWait — blocking tools that wait for clicks are unusable in voice mode
  • Return descriptive text in data — the LLM reads it to formulate spoken responses
  • Add a voice-specific system prompt — instruct the agent to narrate results concisely
  • Use narrate() for slot narration — read display content aloud from within tool handlers

Supported Voice Providers

ProviderToken Handler ConfigEnv Variable
ElevenLabs`{provider: "elevenlabs", type: "stt" \"tts"}`ELEVENLABS_API_KEY
Deepgram{provider: "deepgram"}DEEPGRAM_API_KEY
Cartesia{provider: "cartesia"}CARTESIA_API_KEY

Supporting Files

For detailed API reference, see api-reference.md. For example patterns from real implementations, see examples.md.

Common Gotchas

  1. model_response_complete vs model_response: Streaming adapters emit model_response_complete, not model_response. Subscribers must handle both.
  2. Closure capture in React hooks: When re-keying sessions, use mutable let currentKey = key to avoid stale closures.
  3. React useEffect timing: State updates don't take effect in the same render cycle — guard with early returns.
  4. Browser-safe imports: glove-core is now browser-safe (no native deps). SqliteStore (with its native better-sqlite3 dependency) lives in the separate glove-sqlite package for server-side use only. Subpath imports (glove-core/core, glove-core/glove, etc.) still work but are no longer required for browser safety.
  5. Displaymanager casing: The concrete class is Displaymanager (lowercase 'm'), not DisplayManager. Import it as: import {Displaymanager} from "glove-core/display-manager".
  6. createAdapter stream default: stream defaults to true, not false. Pass stream: false explicitly if you want synchronous responses.
  7. Tool return values: The do function should return ToolResultData with {status, data, renderData?}. data goes to the AI; renderData stays client-only.
  8. Zod.describe(): Always add .describe() to schema fields — the AI reads these descriptions to understand what to provide.
  9. displayPropsSchema is optional but recommended: defineTool's displayPropsSchema is optional, but recommended for tools with display UI — tools without display should use raw ToolConfig instead.
  10. renderData is stripped by model adapters: Model adapters explicitly exclude renderData when formatting tool results for the AI, so it's safe for client-only data.
  11. SileroVAD must use dynamic import: Never import glove-voice/silero-vad at module level in Next.js/SSR. Use await import("glove-voice/silero-vad") to avoid pulling WASM into the server bundle.
  12. Next.js transpilePackages: Add "glove-voice" to transpilePackages in next.config.ts so Next.js processes the ES module.
  13. createTTS must be a factory: GloveVoice calls it once per turn to get a fresh TTS adapter. Pass () => new ElevenLabsTTSAdapter(...), not a single instance.
  14. Barge-in protection requires unAbortable: A pushAndWait resolver suppresses voice barge-in at the trigger level (GloveVoice skips interrupt() when resolverStore.size > 0). But that alone doesn't protect the tool — if interrupt() is called by other means, only unAbortable: true on the tool guarantees it runs to completion despite the abort signal. Use both together for mutation-critical tools like checkout. Use pushAndForget for voice-first tools.
  15. Empty committed transcripts: ElevenLabs Scribe may return empty committed transcripts for short utterances. The adapter auto-falls back to the last partial transcript.
  16. TTS idle timeout: ElevenLabs TTS WebSocket disconnects after ~20s idle. GloveVoice handles this by closing TTS after each model_response_complete and opening a fresh session on next text_delta.
  17. onnxruntime-web build warnings: Critical dependency: require function is used in a way... warnings from onnxruntime-web are expected and harmless.
  18. Audio sample rate: All adapters must agree on 16kHz mono PCM (the default). Don't change unless your provider explicitly requires something different.
  19. narrate() auto-mutes mic: voice.narrate() automatically mutes the mic during playback to prevent TTS audio from feeding back into STT/VAD. It restores the previous mute state when done.
  20. narrate() needs a started pipeline: Calling narrate() before voice.start() throws. The TTS factory and AudioPlayer must be initialized.
  21. Voice auto-silences during compaction: When context compaction is triggered, the voice pipeline ignores all text_delta events between compaction_start and compaction_end. The compaction summary is never narrated.
  22. isCompacting for React UI feedback: GloveState.isCompacting is true while compaction is in progress. Use it to show a loading indicator or disable input during compaction.
  23. <Render> ships a default input: If you have a custom input form, always pass renderInput={() => null} to suppress the built-in one — otherwise you get duplicate inputs.
  24. Tools execute outside React: Tool do() functions run outside the component tree. To access React context (e.g. useWallet()), use a mutable singleton ref synced from a React component (bridge pattern).
  25. SileroVAD not needed for manual mode: When using turnMode: "manual" (push-to-talk), skip the SileroVAD import and its WASM overhead. VAD is only needed for turnMode: "vad".
  26. System prompt: document tools explicitly: Even though tools have descriptions and schemas, listing every tool with its parameters in the system prompt dramatically improves tool selection accuracy.
  27. Inbox items need remote store wiring: When using createRemoteStore, inbox falls back to in-memory if you don't provide getInboxItems/addInboxItem/updateInboxItem/getResolvedInboxItems actions. Items will vanish on reload.
  28. Inbox resolved items are plain text messages: Resolved inbox items are injected as user text messages, not tool results. This avoids Anthropic API validation errors from unmatched tool_use/tool_result pairs.
  29. Blocking inbox reminders are transient: Pending blocking item reminders are included in the prompt but NOT persisted to the store, preventing context bloat across turns.
  30. MCP tool names use __: Bridged MCP tool names are ${entry.id}__${tool.name} — the __ separator (MCP_NAMESPACE_SEP) is regex-safe across all model providers. A Notion search tool surfaces as notion__search.
  31. auth_expired is a contract, not an exception: 401-shaped errors during MCP callTool become {status: "error", message: "auth_expired"}. The framework never refreshes — your app refreshes the token, writes it back to your store, and the next call picks it up via getAccessToken.
  32. McpAdapter.deactivate doesn't unload tools (v1): It flips persisted state, but bridged tools stay loaded on the running agent until the session is refreshed. Plan accordingly.
  33. mountMcp fails open: If an active server fails to reload (transient outage, expired token), the failure is logged via console.warn and the agent continues with the rest of the catalogue. Don't rely on mountMcp throwing.
  34. serverMode defaults the discovery policy: serverMode: trueauto-pick-best and bridged tools never gate on permission. serverMode: false (default) → interactive policy and read-write MCP tools require permission. Pass ambiguityPolicy explicitly to override.
  35. Interactive discovery needs an mcp_picker renderer: The interactive ambiguity policy renders via the mcp_picker renderer on the displayManager. If you're in a browser and using that policy, register a renderer for it; otherwise the pushAndWait will hang.

适合场景

01

调用多模型

02

代码和文本生成

03

Agent 推理流程

04

OpenRouter 模型接入

能力概览

能力 1

统一调用多种 LLM

能力 2

支持 Claude、Gemini、Kimi 等模型

能力 3

适合聊天、代码和推理任务

能力 4

可作为 Agent 模型调用入口

安装后应在对应宿主中按原始 README 的触发条件使用;具体调用方式请以来源页面和 README 为准。

平台分布

Codex

34.82%
按下载量换算77

Claude

29.05%
按下载量换算64

Cursor

19.14%
按下载量换算42

Gemini CLI

10.57%
按下载量换算23

安全审计

Gen Agent Trust Hub

通过

Socket

通过

Snyk

可疑

权限和风险

操作浏览器

该 Skill 可能涉及浏览器控制能力,使用时可能读取或操作网页内容,需要在受控环境中确认权限边界。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。来源安全扫描存在 warning/failed 结果,不能写成本站确认安全。当前只有一个来源,正式发布前建议补源仓库或其他目录站核验。

来源信息

继续浏览同类 Skills