Token导航 LogoToken导航TokenDH.com
开发操作浏览器github未标认证来源可访问许可证需确认审计通过

workspace-apiworkspace API 文档

Agent Skill

用于辅助 API 设计、接口文档、请求响应结构和服务集成说明。它适合让 Agent 梳理 endpoint、生成 OpenAPI 草稿、检查字段命名、整理错误码或辅助前后端联调。使用时需要确认真实业务语义、鉴权方式、分页和错误处理规则;涉及生成接口文档时,应避免凭空补字段,最好从现有代码、schema 或接口样例中提取事实。

总安装

576

周安装

24

GitHub Stars

4,539

下载量

192
CodexClaudeCursorGemini CLI

安装说明

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

GitHub

来源数

2

许可证

unknown

最后核验

2026-05-01

来源状态

来源可访问

安装方式

通过对话安装

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

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

命令行安装

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

skills.shnpx skills
npx skills add https://github.com/epicenterhq/epicenter --skill workspace-api

简介

用于辅助 API 设计、接口文档和请求响应结构梳理。

  • 适合生成 OpenAPI 草稿、检查字段命名或整理错误码。
  • 使用时需确认业务语义、鉴权方式和分页规则。适用宿主包括 Codex、Claude、Cursor、Gemini CLI,接入前应确认版本、权限和运行环境要求。
  • 涉及文档生成时应避免凭空补字段,优先提取现有代码事实。
  • workspace-api 属于开发类 Skill,可作为该场景下的辅助能力补充。

SKILL.md

Workspace API

Reference Repositories

  • Yjs — CRDT framework (foundation of workspace data layer)

Type-safe schema definitions for tables and KV stores.

Related Skills: See yjs for Yjs CRDT patterns and shared types. See svelte for reactive wrappers (fromTable, fromKv).

When to Apply This Skill

  • Defining a new table or KV store with defineTable() or defineKv()
  • Adding a new version to an existing table definition
  • Writing table migration functions
  • Reading, writing, or observing table/KV data
  • Attaching actions to a workspace client via .withActions()
  • Chaining extensions with .withExtension() or .withWorkspaceExtension()
  • Writing server-side Bun scripts with connectWorkspace()

Tables

Shorthand (Single Version)

Use when a table has only one version:

import { defineTable } from '@epicenter/workspace';
import { type } from 'arktype';

const usersTable = defineTable(type({ id: UserId, email: 'string', _v: '1' }));
export type User = InferTableRow<typeof usersTable>;

Every table schema must include _v with a number literal. The type system enforces this — passing a schema without _v to defineTable() is a compile error.

Variadic (Multiple Versions)

Use when you need to evolve a schema over time:

const posts = defineTable(
	type({ id: 'string', title: 'string', _v: '1' }),
	type({ id: 'string', title: 'string', views: 'number', _v: '2' }),
).migrate((row) => {
	switch (row._v) {
		case 1:
			return { ...row, views: 0, _v: 2 };
		case 2:
			return row;
	}
});

KV Stores

KV stores use defineKv(schema, defaultValue). No versioning, no migration—invalid stored data falls back to the default.

import { defineKv } from '@epicenter/workspace';
import { type } from 'arktype';

const sidebar = defineKv(type({ collapsed: 'boolean', width: 'number' }), { collapsed: false, width: 300 });
const fontSize = defineKv(type('number'), 14);
const enabled = defineKv(type('boolean'), true);

KV Design Convention: One Scalar Per Key

Use dot-namespaced keys for logical groupings of scalar values:

// ✅ Correct — each preference is an independent scalar
'theme.mode': defineKv(type("'light' | 'dark' | 'system'"), 'light'),
'theme.fontSize': defineKv(type('number'), 14),

// ❌ Wrong — structured object invites migration needs
'theme': defineKv(type({ mode: "'light' | 'dark'", fontSize: 'number' }), { mode: 'light', fontSize: 14 }),

With scalar values, schema changes either don't break validation (widening 'light' | 'dark' to 'light' | 'dark' | 'system' still validates old data) or the default fallback is acceptable (resetting a toggle takes one click).

Exception: discriminated unions and Record<string, T> | null are acceptable when they represent a single atomic value.

Branded Table IDs (Required)

Every table's id field and every string foreign key field MUST use a branded type instead of plain 'string'. This prevents accidental mixing of IDs from different tables at compile time.

Pattern

Define a branded type + arktype validator + generator in the same file as the workspace definition:

import type { Brand } from 'wellcrafted/brand';
import { type } from 'arktype';
import { generateId, type Id } from '@epicenter/workspace';

// 1. Branded type + arktype validator (co-located with workspace definition)
export type ConversationId = Id & Brand<'ConversationId'>;
export const ConversationId = type('string').as<ConversationId>();

// 2. Generator function — the ONLY place with the cast
export const generateConversationId = (): ConversationId =>
	generateId() as ConversationId;

// 3. Use in defineTable + co-locate type export
const conversationsTable = defineTable(
	type({
		id: ConversationId,              // Primary key — branded
		title: 'string',
		'parentId?': ConversationId.or('undefined'),  // Self-referencing FK
		_v: '1',
	}),
);
export type Conversation = InferTableRow<typeof conversationsTable>;

// 4. At call sites — use the generator, never cast directly
const newId = generateConversationId();  // Good
// const newId = generateId() as string as ConversationId;  // Bad

Actions (.withActions())

Actions wrap workspace operations as defineMutation (writes) or defineQuery (reads). Attach them via .withActions() on a workspace builder—the call is non-terminal, so you can chain .withExtension() after it.

import { createWorkspace, defineMutation, defineQuery, defineWorkspace } from '@epicenter/workspace';

export function createBlogWorkspace() {
	return createWorkspace(blogDefinition).withActions(({ tables }) => ({
		/**
		 * Mark a post as published and record the publication timestamp.
		 *
		 * Separated from a raw `tables.posts.update()` call because publish
		 * involves setting multiple fields atomically and may trigger side
		 * effects (notifications, RSS rebuild) in future versions.
		 */
		publish: defineMutation({
			description: 'Publish a draft post',
			input: type({ id: PostId }),
			handler: ({ id }) => {
				tables.posts.update({ id, published: true, publishedAt: Date.now() });
			},
		}),
	}));
}

JSDoc on Action Methods

Every action method inside .withActions() should have a JSDoc comment. The JSDoc and the description field serve different audiences:

  • description — consumed by MCP servers, CLI help text, and OpenAPI specs. Keep it short and declarative ("Import skills from disk").
  • JSDoc — consumed by developers hovering in an IDE. Explain *why* the action exists as a separate operation, what non-obvious behavior it has, or what assumptions it makes.
// ❌ Parrots the description
/** Import skills from an agentskills.io-compliant directory. */
importFromDisk: defineMutation({ description: 'Import skills from an agentskills.io-compliant directory', ... })

// ✅ Adds distinct value
/**
 * Scan a directory of SKILL.md files and upsert them into the workspace.
 *
 * Skills without a `metadata.id` in their frontmatter get one generated
 * and written back to the file, so future imports produce stable IDs
 * across machines.
 */
importFromDisk: defineMutation({ description: 'Import skills from an agentskills.io-compliant directory', ... })

Workspace File Structure

Each app splits workspace code into an isomorphic workspace/ folder and a runtime-specific client.ts:

src/lib/
│
├── workspace/                          ← 100% isomorphic (safe for Node, Bun, browser)
│   ├── definition.ts                   ← Schema: defineWorkspace, defineTable, branded IDs
│   ├── workspace.ts                    ← Factory: createWorkspace(definition) + isomorphic actions
│   └── index.ts                        ← Barrel: re-exports definition + workspace only
│
└── client.ts                           ← Runtime singleton: extensions, encryption, sync,
                                           runtime-specific actions (browser APIs, Node fs, etc.)
                    ┌─────────────────────────┐
                    │     definition.ts        │
                    │  tables, KV, branded IDs │
                    └────────────┬────────────┘
                                 │ imports
                    ┌────────────▼────────────┐
                    │     workspace.ts         │
                    │  createX() factory       │
                    │  + isomorphic actions    │
                    └────────────┬────────────┘
                                 │ imports
   ┌─────────────────────────────┼─────────────────────────────┐
   │                             │                             │
   ▼                             ▼                             ▼
┌──────────────┐   ┌──────────────────┐   ┌──────────────────┐
│ client.ts    │   │ server-client.ts │   │ cli-client.ts    │
│ (browser)    │   │ (Node/Bun)       │   │ (CLI)            │
│ IndexedDB    │   │ SQLite           │   │ filesystem       │
│ WebSocket    │   │ TCP sync         │   │ persistence      │
│ Chrome APIs  │   │ Node fs APIs     │   │                  │
└──────────────┘   └──────────────────┘   └──────────────────┘

Layering Rules

  1. definition.ts — Pure schema. defineWorkspace(), defineTable(), defineKv(), branded ID types and generators. Isomorphic.
  2. workspace.ts — Factory function that calls createWorkspace(definition). May chain .withActions() for isomorphic actions (table reads/writes only). Isomorphic.
  3. index.ts — Barrel that re-exports from definition.ts and workspace.ts only. Never re-exports from client.ts. This is the import path for $lib/workspace and the package.json subpath export.
  4. client.ts — Lives outside the workspace/ folder at src/lib/client.ts. Calls the factory, chains .withEncryption(), .withExtension(), and runtime-specific .withActions(). Exports the singleton as a named export (export const workspace =...).

Import Convention

// Components/state that need the live workspace instance:
import { workspace, auth } from '$lib/client';

// Components that only need types or the definition:
import { type Note, NoteId } from '$lib/workspace';

// Other packages in the monorepo:
import { createHoneycrisp } from '@epicenter/honeycrisp/workspace';
import { honeycrisp } from '@epicenter/honeycrisp/definition';

Package.json Subpath Exports

Each app exports a single ./workspace subpath pointing to the barrel:

{
  "exports": {
    "./workspace": "./src/lib/workspace/index.ts"
  }
}

The barrel is 100% isomorphic, so this single subpath is safe for any consumer (server, CLI, other apps). The separate ./definition subpath is no longer needed since the barrel already re-exports everything from definition.ts.

Isomorphic vs Runtime-Specific Actions

Isomorphic actions (table reads/writes, portable logic) belong in the exported workspace.ts factory. Runtime-specific actions—whether browser APIs, Chrome extension APIs, Node/Bun filesystem calls, or Tauri commands—are chained via .withActions() in the client file closest to that runtime.

// workspace.ts — isomorphic actions (exported via barrel)
export function createMyApp() {
  return createWorkspace(definition).withActions(({ tables }) => ({
    devices: {
      list: defineQuery({
        title: 'List Devices',
        description: 'List all synced devices.',
        input: Type.Object({}),
        handler: () => ({ devices: tables.devices.getAllValid() }),
      }),
    },
  }));
}

// src/lib/client.ts — browser-specific actions chained at the runtime boundary
export const workspace = createMyApp()
  .withExtension('persistence', indexeddbPersistence)
  .withExtension('sync', createSyncExtension({ ... }))
  .withActions(({ tables }) => ({
    tabs: {
      close: defineMutation({
        title: 'Close Tabs',
        description: 'Close browser tabs by ID.',
        input: Type.Object({ tabIds: Type.Array(Type.Number()) }),
        handler: async ({ tabIds }) => {
          await browser.tabs.remove(tabIds);  // Chrome API
          return { closedCount: tabIds.length };
        },
      }),
    },
  }));

// OR: src/lib/server-client.ts — Node/Bun-specific at the server boundary
export const workspace = createMyApp()
  .withExtension('persistence', sqlitePersistence)
  .withActions(({ tables }) => ({
    files: {
      importFromDisk: defineMutation({
        title: 'Import Files',
        description: 'Import files from a local directory.',
        input: Type.Object({ dirPath: Type.String() }),
        handler: async ({ dirPath }) => {
          const entries = await readdir(dirPath);  // Node fs API
          // ...
        },
      }),
    },
  }));

Extension Ordering

Extensions initialize in registration order. Each extension's factory receives a whenReady promise that resolves when all previously registered extensions have finished initializing. Whether this creates a waterfall depends on whether each extension awaits it:

ExtensionAwaits prior whenReady?Behavior
filesystemPersistenceNoStarts loading SQLite immediately
indexeddbPersistenceNoStarts loading IndexedDB immediately
createCliUnlockYesWaits for persistence, then applies encryption keys
createSyncExtensionYesWaits for everything before it, then opens WebSocket
createMarkdownMaterializerYesWaits for persistence + sync, then materializes

The standard chain is persistence → unlock → sync:

persistence starts loading ────────────────────→ done
                                                   ↓
                        unlock waits... ──────────→ applies keys → done
                                                                    ↓
                        sync waits... ─────────────────────────────→ connects

This ordering matters because sync only exchanges the delta between local state and the server. Without persistence loading first, every cold start downloads the full document.

// ✅ Correct — persistence loads first, sync exchanges delta only
createWorkspace(definition)
  .withExtension('persistence', filesystemPersistence({ filePath: '...' }))
  .withWorkspaceExtension('unlock', createCliUnlock(sessions, SERVER_URL))
  .withExtension('sync', createSyncExtension({ url: ..., getToken: ... }))

// ❌ Wrong — sync starts before local state is loaded, downloads full document
createWorkspace(definition)
  .withExtension('sync', createSyncExtension({ url: ..., getToken: ... }))
  .withExtension('persistence', filesystemPersistence({ filePath: '...' }))

connectWorkspace (CLI/Script Shortcut)

For server-side Bun scripts, connectWorkspace from @epicenter/cli handles the unlock → sync chain automatically. It is ephemeral by design — no local persistence, so a script can coexist with a long-running epicenter start daemon without fighting over the same SQLite file:

import { connectWorkspace } from '@epicenter/cli';
import { createFujiWorkspace } from '@epicenter/fuji/workspace';

const workspace = await connectWorkspace(createFujiWorkspace);
// Ready. Authenticated. Syncing. Full doc downloaded from server.

const entries = workspace.tables.entries.getAllValid();
await workspace.dispose();

Writes propagate through sync to the daemon, which owns the materializer (markdown, SQLite mirror, etc.).

Use connectWorkspace for one-off scripts and agent-written automation. Use epicenter.config.ts for long-running daemons and materializers that need persistence and custom workspace-specific extensions.

The _v Convention

  • _v is a number discriminant field ('1' in arktype = the literal number 1)
  • Required for tables — enforced at the type level via CombinedStandardSchema<{id: string; _v: number}>
  • Not used by KV stores — KV has no versioning; defineKv(schema, defaultValue) is the only pattern
  • In arktype schemas: _v: '1', _v: '2', _v: '3' (number literals)
  • In migration returns: _v: 2 (TypeScript narrows automatically, as const is unnecessary)
  • Convention: _v goes last in the object ({id,...fields, _v: '1'})

References

Load these on demand based on what you're working on:

Code references:

  • packages/workspace/src/workspace/define-table.ts
  • packages/workspace/src/workspace/define-kv.ts
  • packages/workspace/src/workspace/index.ts
  • packages/workspace/src/workspace/create-tables.ts
  • packages/workspace/src/workspace/create-kv.ts
  • packages/workspace/src/workspace/create-workspace.ts

适合场景

01

用户想查找某类 Agent Skill 时

02

需要根据任务场景推荐可安装能力包时

03

需要对比不同来源的安装命令和来源信息时

能力概览

能力 1

按任务关键词查找相关 Skills

能力 2

展示可复制的安装命令

能力 3

保留来源站点、仓库和原始说明,方便继续核验

能力 4

展示第三方安全扫描或审计结果

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

平台分布

Codex

37.66%
按下载量换算72

Claude

27.97%
按下载量换算54

Cursor

18.76%
按下载量换算36

Gemini CLI

8.39%
按下载量换算16

安全审计

Gen Agent Trust Hub

通过

Socket

通过

Snyk

通过

权限和风险

操作浏览器

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

安装前确认

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

来源信息

继续浏览同类 Skills