mcpField
C
create-agent-tui

create-agent-tui

github.com/openrouterteamAgent 工作流309296 次安装1 天前更新
代码仓库

关于 create-agent-tui

create-agent-tui 是一项用于生成 TypeScript Agent 终端界面的技能,面向从头搭建 Agent 的开发者。它通过可选工具、模块和显示样式来定制项目,并以 OpenRouter 作为目标模型服务。

主要功能

  1. 01生成 TypeScript Agent TUI 项目
  2. 02按清单选择工具与架构模块
  3. 03配置会话、权限和日志功能
  4. 04定制输入、工具显示与加载动画
  5. 05提供 OpenRouter 模型切换命令

适用场景

  1. 01从零创建终端 Agent 项目
  2. 02为现有 Agent 添加工具
  3. 03为 Agent 增加会话保存或审批
  4. 04定制 Agent 的终端交互方式

MCP 相关

README 提及可扩展的 MCP 接入点,但未说明具体的 MCP 配置或能力。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/openrouterteam/skills --skill create-agent-tui

安装后在 Claude Code 中输入/create-agent-tui确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

metadata.json
README.md
references/
input-styles.md
loader.md
modules.md
server-entry-points.md
slash-commands.md
system-prompt.md
tool-display.md
tools.md
tui.md
sample/
.gitignore
package-lock.json
package.json
sample/src/
agent.ts
banner.ts
cli.ts
commands.ts
config.ts
loader.ts
renderer.ts
screenshot-demos.ts
session.ts
terminal-bg.ts
sample/src/tools/
custom.ts
file-edit.ts
file-read.ts
file-write.ts
glob.ts
grep.ts

详细介绍

创建 Agent TUI

搭建一个使用 TypeScript 编写、面向 OpenRouter 的完整 Agent TUI。生成的项目使用 @openrouter/agent 处理内层循环(模型调用、工具执行、停止条件),并提供外层框架:可自定义的终端界面、配置、会话管理、工具定义和入口点。

架构借鉴了三个生产级 Agent 系统:

  • pi-mono/coding-agent — 三层分离、JSONL 会话、可插拔的工具操作
  • Claude Code — 工具元数据(只读、破坏性操作、审批)、系统提示词组合
  • Codex CLI — 分层配置、带会话缓存的审批流程、结构化日志记录

先决条件

  • Node.js 18+
  • 从 openrouter.ai/settings/keys 获取的 OPENROUTER_API_KEY
  • 如需完整的 SDK 参考,请参阅 openrouter-typescript-sdk 技能

决策树

用户想要……操作
从头构建新的 Agent展示下方清单 → 按照生成工作流操作
为现有框架添加工具阅读 references/tools.md,仅展示工具清单
添加框架模块阅读 references/modules.md,生成该模块
添加 API 服务入口点阅读 references/server-entry-points.md

交互式工具清单

将此清单作为多选列表展示。标记为 ON 的项目为预选默认项。

OpenRouter 服务端工具(服务端执行,无需实现)
工具类型字符串默认值配置
网页搜索openrouter:web_searchONengine、max_results、域名过滤
日期时间openrouter:datetimeONtimezone
图像生成openrouter:image_generationOFFmodel、quality、size、format

服务端工具与用户定义的工具一起放入 tools 数组。无需客户端代码 — OpenRouter 会执行这些工具。

用户定义的工具(客户端执行,生成到 src/tools/)
工具默认值描述
文件读取ON读取文件,支持 offset/limit,检测图像
文件写入ON写入/创建文件,自动创建目录
文件编辑ON搜索并替换,带差异验证
Glob/查找ON按 glob 模式查找文件
Grep/搜索ON使用正则表达式搜索内容
目录列表ON列出目录内容
Shell/BashON执行命令,支持超时和输出捕获
JS REPLOFF持久化 Node.js 环境
子 Agent 生成OFF将任务委派给子 Agent
计划/待办OFF跟踪多步骤任务进度
请求用户输入OFF结构化多选问题
网页抓取OFF获取网页并提取文本
查看图像OFF将本地图像读取为 base64
自定义工具模板ON用于特定领域工具的空白骨架
框架模块(架构组件)
模块默认值描述
会话持久化ON仅追加的 JSONL 对话日志
ASCII Logo 横幅OFF启动时显示自定义 ASCII 艺术横幅 — 询问项目名称
上下文压缩OFF上下文较长时总结较早的消息
系统提示词组合OFF从静态和动态上下文组装指令
工具权限/审批OFF对危险工具的使用要求用户确认
结构化事件日志记录OFF为工具调用、API 请求和错误生成事件
@-文件引用OFF使用 @filename 将文件内容附加到下一条消息
! Shell 快捷方式OFF使用 !command 运行 Shell,并将输出注入上下文
多行输入OFF使用 Shift+Enter 输入多行内容(需要原始终端模式)
斜杠命令(面向用户的 REPL 命令)
命令默认值描述
/modelON通过 OpenRouter API 切换模型
/newON开始新的对话
/helpON列出可用命令
/compactOFF手动压缩上下文
/sessionOFF显示会话元数据和令牌用量
/exportOFF将对话保存为 Markdown

启用斜杠命令后,生成带有命令注册表的 src/commands.ts。规范请参阅 references/slash-commands.md。

视觉自定义(每项均为单选)

输入样式 — 提示词的外观。请参阅 references/input-styles.md:

样式默认值描述
blockON全宽背景框,带有 › 提示符,并适配终端主题
bordered输入区域上下带有水平 ─ 线
plain简单的 > readline 提示符,不使用转义序列
Other用户描述所需样式 — 实现自定义输入样式

工具显示 — 执行期间工具调用的显示方式。请参阅 references/tool-display.md:

样式默认值描述
groupedON粗体操作标签与树枝状输出
emoji每次调用使用 ⚡/✓ 标记,并显示参数和耗时
minimal汇总为单行摘要
hidden不显示工具输出
Other用户描述所需样式 — 实现自定义显示方式

加载动画 — 等待模型响应时显示。请参阅 references/loader.md:

样式默认值描述
spinnerON文本左侧显示 Braille 点状旋转动画(⠋⠙⠹…)
gradient加载文本上滚动显示彩色渐变光泽
minimal末尾显示点号(Working···)
Other用户描述所需样式 — 实现自定义动画

还要询问加载文本(默认值:"Working")。


生成工作流

获取清单选项后,按以下工作流操作:

- [ ] Generate package.json with dependencies
- [ ] Generate src/config.ts (add showBanner field if ASCII Logo Banner is ON)
- [ ] Generate src/tools/index.ts wiring selected tools + server tools
- [ ] Generate selected tool files in src/tools/ (see Tool Pattern below, specs in references/tools.md)
- [ ] Generate src/agent.ts (core runner)
- [ ] Generate selected harness modules (specs in references/modules.md)
- [ ] Generate src/terminal-bg.ts (adaptive input background — see references/tui.md)
- [ ] Generate input style functions in src/cli.ts (block/bordered/plain — see references/input-styles.md)
- [ ] Generate src/renderer.ts (tool display — see references/tool-display.md)
- [ ] Generate src/loader.ts (loader animation — see references/loader.md)
- [ ] If slash commands selected: generate src/commands.ts (see references/slash-commands.md)
- [ ] If ASCII Logo Banner is ON: generate src/banner.ts (see ASCII Logo Banner section below)
- [ ] Generate src/cli.ts entry point (or src/server.ts — see references/server-entry-points.md)
- [ ] Generate .env.example with OPENROUTER_API_KEY=
- [ ] Verify: run npx tsc --noEmit to check types

工具模式

所有用户自定义工具都遵循此模式,并使用 @openrouter/agent/tool。以下是一个完整示例 — references/tools.md 中的所有其他工具也遵循相同结构:

import { tool } from '@openrouter/agent/tool';
import { z } from 'zod';
import { readFile, stat } from 'fs/promises';

const DEFAULT_LINE_LIMIT = 2000;
const MAX_LINE_CHARS = 2000;

export const fileReadTool = tool({
  name: 'file_read',
  description:
    'Read the contents of a file. Output is capped at 2000 lines by default (use offset/limit to paginate) and any line longer than 2000 characters is truncated. When the response is truncated, the hint field tells you how to continue.',
  inputSchema: z.object({
    path: z.string().describe('Absolute path to the file'),
    offset: z.number().optional().describe('Start reading from this line (1-indexed)'),
    limit: z.number().optional().describe(`Maximum lines to return (default ${DEFAULT_LINE_LIMIT})`),
  }),
  execute: async ({ path, offset, limit }) => {
    try {
      const content = await readFile(path, 'utf-8');
      const lines = content.split('\n');

      const start = offset ? offset - 1 : 0;
      const end = Math.min(start + (limit ?? DEFAULT_LINE_LIMIT), lines.length);
      let longLines = 0;
      const slice = lines.slice(start, end).map((line) => {
        if (line.length <= MAX_LINE_CHARS) return line;
        longLines++;
        return line.slice(0, MAX_LINE_CHARS) + `… [line truncated, ${line.length - MAX_LINE_CHARS} chars dropped]`;
      });
      const tailTruncated = end < lines.length;
      const truncated = tailTruncated || longLines > 0;
      const hintParts: string[] = [`Showing lines ${start + 1}-${end} of ${lines.length}.`];
      if (tailTruncated) hintParts.push(`Use offset=${end + 1} to continue.`);
      if (longLines > 0) hintParts.push(`${longLines} line(s) exceeded ${MAX_LINE_CHARS} chars and were per-line truncated; use grep to fetch content from those lines.`);

      return {
        content: slice.join('\n'),
        totalLines: lines.length,
        ...(truncated && {
          truncated: true,
          ...(tailTruncated && { nextOffset: end + 1 }),
          hint: hintParts.join(' '),
        }),
      };
    } catch (err: any) {
      if (err.code === 'ENOENT') return { error: `File not found: ${path}` };
      if (err.code === 'EACCES') return { error: `Permission denied: ${path}` };
      return { error: err.message };
    }
  },
});

有关所有其他工具的规范,请参阅 references/tools.md。


核心文件

以下文件始终会生成。Agent 会根据清单选项对其进行调整。

package.json

初始化项目并安装最新版本的依赖项:

npm init -y
npm pkg set type=module
npm pkg set scripts.start="tsx src/cli.ts"
npm pkg set scripts.dev="tsx watch src/cli.ts"
npm install @openrouter/agent glob zod
npm install -D tsx typescript @types/node
tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}
src/config.ts
import { readFileSync, existsSync } from 'fs';
import { resolve } from 'path';

export interface DisplayConfig {
  toolDisplay: 'emoji' | 'grouped' | 'minimal' | 'hidden';
  reasoning: boolean;
  inputStyle: 'block' | 'bordered' | 'plain';
}

export interface AgentConfig {
  apiKey: string;
  model: string;
  systemPrompt: string;
  maxSteps: number;
  maxCost: number;
  sessionDir: string;
  showBanner: boolean;
  display: DisplayConfig;
  slashCommands: boolean;
}

const DEFAULTS: AgentConfig = {
  apiKey: '',
  model: 'anthropic/claude-opus-4.7',
  systemPrompt: [
    'You are a coding assistant with access to tools for reading, writing, editing, and searching files, and running shell commands.',
    '',
    'Current working directory: {cwd}',
    '',
    'Guidelines:',
    '- Use your tools proactively. Explore the codebase to find answers instead of asking the user.',
    '- Keep working until the task is fully resolved before responding.',
    '- Do not guess or make up information — use your tools to verify.',
    '- Be concise and direct.',
    '- Show file paths clearly when working with files.',
    '- Prefer grep and glob tools over shell commands for file search.',
    '- When editing code, make minimal targeted changes consistent with the existing style.',
  ].join('\n'),
  maxSteps: 20,
  maxCost: 1.0,
  sessionDir: '.sessions',
  showBanner: false,
  display: { toolDisplay: 'grouped', reasoning: false, inputStyle: 'block' },
  slashCommands: true,
};

export function loadConfig(overrides: Partial<AgentConfig> = {}): AgentConfig {
  let config = { ...DEFAULTS };

  const configPath = resolve('agent.config.json');
  if (existsSync(configPath)) {
    const file = JSON.parse(readFileSync(configPath, 'utf-8'));
    if (file.display) {
      config.display = { ...config.display, ...file.display };
    }
    config = { ...config, ...file, display: config.display };
  }

  if (process.env.OPENROUTER_API_KEY) config.apiKey = process.env.OPENROUTER_API_KEY;
  if (process.env.AGENT_MODEL) config.model = process.env.AGENT_MODEL;
  if (process.env.AGENT_MAX_STEPS) config.maxSteps = Number(process.env.AGENT_MAX_STEPS);
  if (process.env.AGENT_MAX_COST) config.maxCost = Number(process.env.AGENT_MAX_COST);

  if (overrides.display) {
    config.display = { ...config.display, ...overrides.display };
  }
  config = { ...config, ...overrides, display: config.display };
  if (!config.apiKey) throw new Error('OPENROUTER_API_KEY is required.');
  return config;
}
src/tools/index.ts

根据清单选项调整导入内容。此示例包含所有默认启用的工具:

import { serverTool } from '@openrouter/agent';
import { fileReadTool } from './file-read.js';
import { fileWriteTool } from './file-write.js';
import { fileEditTool } from './file-edit.js';
import { globTool } from './glob.js';
import { grepTool } from './grep.js';
import { listDirTool } from './list-dir.js';
import { shellTool } from './shell.js';

export const tools = [
  // User-defined tools — executed client-side
  fileReadTool,
  fileWriteTool,
  fileEditTool,
  globTool,
  grepTool,
  listDirTool,
  shellTool,

  // Server tools — executed by OpenRouter, no client implementation needed
  serverTool({ type: 'openrouter:web_search' }),
  serverTool({ type: 'openrouter:datetime', parameters: { timezone: 'UTC' } }),
];
src/agent.ts
import { OpenRouter } from '@openrouter/agent';
import type { Item } from '@openrouter/agent';
import { stepCountIs, maxCost } from '@openrouter/agent/stop-conditions';
import type { AgentConfig } from './config.js';
import { tools } from './tools/index.js';

export type ChatMessage = { role: 'user' | 'assistant' | 'system'; content: string };

export type AgentEvent =
  | { type: 'text'; delta: string }
  | { type: 'tool_call'; name: string; callId: string; args: Record<string, unknown> }
  | { type: 'tool_result'; name: string; callId: string; output: string }
  | { type: 'reasoning'; delta: string };

export async function runAgent(
  config: AgentConfig,
  input: string | ChatMessage[],
  options?: { onEvent?: (event: AgentEvent) => void; signal?: AbortSignal },
) {
  const client = new OpenRouter({ apiKey: config.apiKey });

  const result = client.callModel({
    model: config.model,
    instructions: config.systemPrompt.replace('{cwd}', process.cwd()),
    input: input as string | Item[],
    tools,
    stopWhen: [stepCountIs(config.maxSteps), maxCost(config.maxCost)],
  });

  if (options?.onEvent) {
    // Track text length PER message item by id. A multi-step agent emits
    // multiple OutputMessage items over the course of a single run (one per
    // assistant turn between tool calls), and each one grows from 0 to its
    // final length. A single global cursor breaks on the second message:
    // when its length is smaller than the cursor from the first, the slice
    // cuts mid-string and drops the start of the new message's text.
    const textByItem = new Map<string, number>();
    const callNames = new Map<string, string>();

    for await (const item of result.getItemsStream()) {
      if (options?.signal?.aborted) break;
      if (item.type === 'message') {
        const text = item.content
          ?.filter((c): c is { type: 'output_text'; text: string } => 'text' in c)
          .map((c) => c.text)
          .join('') ?? '';
        const prev = textByItem.get(item.id) ?? 0;
        if (text.length > prev) {
          options.onEvent({ type: 'text', delta: text.slice(prev) });
          textByItem.set(item.id, text.length);
        }
      } else if (item.type === 'function_call') {
        callNames.set(item.callId, item.name);
        if (item.status === 'completed') {
          const args = (() => { try { return item.arguments ? JSON.parse(item.arguments) : {}; } catch { return {}; } })();
          options.onEvent({ type: 'tool_call', name: item.name, callId: item.callId, args });
        }
      } else if (item.type === 'function_call_output') {
        const out = typeof item.output === 'string' ? item.output : JSON.stringify(item.output);
        options.onEvent({
          type: 'tool_result',
          name: callNames.get(item.callId) ?? 'unknown',
          callId: item.callId,
          output: out.length > 200 ? out.slice(0, 200) + '…' : out,
        });
      } else if (item.type === 'reasoning') {
        const text = item.summary?.map((s: { text: string }) => s.text).join('') ?? '';
        if (text) options.onEvent({ type: 'reasoning', delta: text });
      }
    }
  }

  const response = await result.getResponse();
  return { text: response.outputText ?? '', usage: response.usage, output: response.output };
}

export async function runAgentWithRetry(
  config: AgentConfig,
  input: string | ChatMessage[],
  options?: { onEvent?: (event: AgentEvent) => void; signal?: AbortSignal; maxRetries?: number },
) {
  for (let attempt = 0, max = options?.maxRetries ?? 3; attempt <= max; attempt++) {
    try { return await runAgent(config, input, options); }
    catch (err: any) {
      const s = err?.status ?? err?.statusCode;
      if (!(s === 429 || (s >= 500 && s < 600)) || attempt === max) throw err;
      await new Promise((r) => setTimeout(r, Math.min(1000 * 2 ** attempt, 30000)));
    }
  }
  throw new Error('Unreachable');
}
src/cli.ts

支持三种输入样式:block(背景框)、bordered(水平线)和 plain(简单插入符)。完整实现请参阅 references/input-styles.md 中的 styledReadLine()、borderedReadLine() 和 getInput() 分派器。

import { createInterface } from 'readline';
import { loadConfig } from './config.js';
import { runAgentWithRetry, type AgentEvent } from './agent.js';
import { detectBg } from './terminal-bg.js';
// import { styledReadLine, borderedReadLine } from ... — see references/input-styles.md

const DIM = '\x1b[2m';
const RESET = '\x1b[0m';
const BOLD = '\x1b[1m';
const CYAN = '\x1b[36m';
const GREEN = '\x1b[32m';
const YELLOW = '\x1b[33m';
const GRAY = '\x1b[90m';

function formatTokens(n: number): string {
  return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
}

function summarizeArgs(name: string, args: Record<string, unknown>): string {
  const key = { shell: 'command', file_read: 'path', file_write: 'path',
    file_edit: 'path', glob: 'pattern', grep: 'pattern', web_search: 'query',
  }[name] ?? Object.keys(args)[0];
  if (!key || !(key in args)) return '';
  const val = String(args[key]);
  return `${key}=${val.length > 40 ? val.slice(0, 40) + '…' : val}`;
}

async function main() {
  const config = loadConfig();
  const BG_INPUT = config.display.inputStyle === 'block' ? await detectBg() : '';

  // Banner
  const width = Math.min(process.stdout.columns || 60, 60);
  const line = GRAY + '─'.repeat(width) + RESET;
  console.log(`\n${line}`);
  console.log(`  ${BOLD}My Agent${RESET}  ${DIM}v0.1.0${RESET}`);
  console.log(`  ${DIM}model${RESET}  ${CYAN}${config.model}${RESET}`);
  if (config.slashCommands) console.log(`  ${DIM}/model to change${RESET}`);
  console.log(`${line}\n`);

  const rl = createInterface({ input: process.stdin, output: process.stdout, prompt: `${GREEN}>${RESET} ` });

  async function getInput(): Promise<string> {
    switch (config.display.inputStyle) {
      case 'block': return styledReadLine(BG_INPUT);
      case 'bordered': return borderedReadLine();
      case 'plain':
      default:
        return new Promise((r) => { rl.prompt(); rl.once('line', r); });
    }
  }

  while (true) {
    const input = await getInput();
    const trimmed = input.trim();
    if (!trimmed) continue;

    if (config.display.inputStyle !== 'plain') {
      const cwd = process.cwd().replace(process.env.HOME ?? '', '~');
      process.stdout.write(`\x1b[K  ${DIM}${cwd}${RESET}\n`);
    }

    if (trimmed.toLowerCase() === 'exit') { process.exit(0); }

    console.log();
    let streaming = false, started = false;
    const toolStart = new Map<string, number>();
    const dots = ['·', '··', '···'];
    let di = 0;
    const spin = setInterval(() => {
      if (!started) process.stdout.write(`\r${DIM}${dots[di++ % 3]}${RESET}`);
    }, 300);

    const handleEvent = (event: AgentEvent) => {
      if (!started) { started = true; process.stdout.write('\r\x1b[K'); }
      if (event.type === 'text') { streaming = true; process.stdout.write(event.delta); }
      else if (event.type === 'tool_call') {
        if (streaming) { process.stdout.write('\n'); streaming = false; }
        toolStart.set(event.callId, Date.now());
        const args = summarizeArgs(event.name, event.args);
        console.log(`  ${YELLOW}⚡${RESET} ${DIM}${event.name}${args ? ' ' + args : ''}${RESET}`);
      } else if (event.type === 'tool_result') {
        const ms = Date.now() - (toolStart.get(event.callId) ?? Date.now());
        console.log(`  ${GREEN}✓${RESET} ${DIM}${event.name} (${(ms / 1000).toFixed(1)}s)${RESET}`);
        started = false;
      }
    };

    try {
      const result = await runAgentWithRetry(config, trimmed, { onEvent: handleEvent });
      clearInterval(spin);
      if (streaming) process.stdout.write(RESET);
      const inT = result.usage?.inputTokens ?? 0;
      const outT = result.usage?.outputTokens ?? 0;
      console.log(`\n${GRAY}  ${formatTokens(inT)} in · ${formatTokens(outT)} out${RESET}\n`);
    } catch (err: any) {
      clearInterval(spin);
      if (streaming) process.stdout.write(RESET);
      console.log(`\n${YELLOW}  Error: ${err.message}${RESET}\n`);
    }
  }
}

main();

ASCII Logo 横幅

选择 ASCII Logo Banner 后,询问用户的项目名称,然后生成包含该名称 ASCII 艺术字的 src/banner.ts。使用 █ 字符绘制块状字母样式。横幅应适合 60 列宽的终端。

src/banner.ts

为用户的项目名称生成 ASCII 艺术字。以下是项目名称为“ACME”时的示例:

const RESET = '\x1b[0m';
const BOLD = '\x1b[1m';
const DIM = '\x1b[2m';
const CYAN = '\x1b[36m';

const LOGO = `
   █████╗  ██████╗███╗   ███╗███████╗
  ██╔══██╗██╔════╝████╗ ████║██╔════╝
  ███████║██║     ██╔████╔██║█████╗
  ██╔══██║██║     ██║╚██╔╝██║██╔══╝
  ██║  ██║╚██████╗██║ ╚═╝ ██║███████╗
  ╚═╝  ╚═╝ ╚═════╝╚═╝     ╚═╝╚══════╝`;

export function printBanner(model: string): void {
  console.log(CYAN + BOLD + LOGO + RESET);
  console.log(`  ${DIM}model  ${RESET}${model}\n`);
}

根据用户的实际项目名称调整 ASCII 艺术字。保持名称为一到两个简短单词,并确保不超过 60 列。

接入 src/cli.ts

选择 showBanner 后,在 main() 开头的文本横幅之前添加:

import { printBanner } from './banner.js';

// In main(), replace the text banner with:
if (config.showBanner) {
  printBanner(config.model);
} else {
  // fall back to the text banner from the cli.ts template above
}

向 AgentConfig 添加 showBanner: boolean(默认值为 false)。通过 agent.config.json 或 loadConfig({ showBanner: true }) 启用。


参考文件

如需查阅核心文件之外的内容:

  • references/tools.md — 所有用户自定义工具的规格:file-read、file-write、file-edit、glob、grep、list-dir、shell、js-repl、sub-agent、plan、request-input、web-fetch、view-image、custom template
  • references/modules.md — Harness 模块:会话持久化、上下文压缩、系统提示词组合、工具审批、结构化日志记录
  • references/tui.md — 终端后台检测、自适应输入背景
  • references/tool-display.md — 工具显示样式:emoji、grouped、minimal;TuiRenderer 类、各工具的颜色、格式化器
  • references/input-styles.md — 输入样式:block(背景框)、bordered(水平线)、plain(简单插入符)
  • references/loader.md — 加载动画:gradient(滚动闪光)、spinner(盲文点)、minimal(尾随点)
  • references/slash-commands.md — 斜杠命令注册表:/model、/new、/help、/compact、/session、/export
  • references/system-prompt.md — 默认系统提示词、buildSystemPrompt()、自定义指南
  • references/server-entry-points.md — 支持 SSE 流式传输的 Express/Hono API 服务入口,以及扩展点(MCP、WebSocket、动态模型)

评论

支持 Markdown