This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This is the AlphaHuman Skills repository — a plugin/extension system for the AlphaHuman AI agent. Skills extend the agent with domain-specific capabilities for the crypto community platform. This repo is a git submodule of the main AlphaHuman Tauri app.
Skills are written in TypeScript and compiled to JavaScript for execution in a sandboxed QuickJS runtime embedded in the Rust host application.
skills/ # Repo root
├── src/ # TypeScript source files
│ ├── example-skill/ # Comprehensive example (kitchen sink)
│ ├── server-ping/ # Server health monitoring skill
│ ├── simple-skill/ # Minimal skill template
│ ├── gmail/ # Gmail integration
│ ├── notion/ # Notion API integration
│ └── telegram/ # Telegram integration
├── skills/ # Compiled JavaScript output (git-ignored)
├── types/
│ └── globals.d.ts # Ambient type declarations for bridge APIs
├── dev/
│ └── test-harness/ # Node.js test harness (tsx)
│ ├── runner-node.ts # Test runner
│ ├── bootstrap-node.ts # Mock bridge APIs
│ ├── live-runner-node.ts # Live test runner
│ ├── mock-state.ts # Mock state management
│ └── mock-db.ts # Mock SQLite database
├── scripts/
│ ├── build-bundle.mjs # esbuild bundler
│ ├── strip-exports.mjs # Post-build processing
│ ├── validate.mjs # Skill validation checks
│ ├── scan-secrets.mjs # Secret scanner
│ ├── install-skill-deps.mjs # Per-skill dependency installer
│ └── test-harness.mjs # Test orchestrator
├── package.json # Build scripts
├── tsconfig.json # Base TypeScript config
├── tsconfig.build.json # Production build config
└── tsconfig.test.json # Test build config
Each skill is a directory under src/ with a modular file layout. Keep concerns separated into dedicated files and folders:
src/my-skill/
├── manifest.json # Metadata (id, name, version, runtime, platforms, setup, events, entity schema)
├── index.ts # Entry point — lifecycle hooks, imports all modules, wires everything together
├── types.ts # All TypeScript type/interface definitions for the skill
├── state.ts # Shared mutable state via globalThis pattern
├── setup.ts # Multi-step setup wizard (onSetupStart, onSetupSubmit logic)
├── sync.ts # Initial data sync and periodic refresh logic
├── update-handlers.ts # Event/update dispatch handlers (if skill has real-time updates)
├── db/
│ ├── schema.ts # SQLite CREATE TABLE statements, indexes, migrations
│ └── helpers.ts # Upsert, query, and data extraction utilities
├── api/
│ ├── index.ts # Barrel re-export of all API functions
│ ├── auth.ts # Authentication API calls
│ ├── messages.ts # Messaging API calls
│ ├── chats.ts # Chat/channel API calls
│ └── ... # One file per API domain
├── tools/
│ ├── index.ts # Barrel re-export of all tool definitions
│ ├── send-message.ts # Individual tool definition + execute function
│ ├── get-chats.ts
│ └── ... # One file per tool (or per logical group)
├── package.json # (optional) Per-skill npm dependencies
└── __tests__/
└── test-my-skill.ts # Unit tests
Key principles:
index.tsis the orchestrator — it imports all modules, implements lifecycle hooks (init,start,stop, etc.), assembles thetoolsarray, and exposes helper functions onglobalThisstate.tsowns the state — defines the state interface, initializes defaults, and registersglobalThis.getSkillState()setup.tsowns the setup wizard — allonSetupStart/onSetupSubmitlogic lives here, imported byindex.tssync.tsowns data synchronization — initial sync, periodic refresh, progress trackingtypes.tsowns all types — shared interfaces, API response types, database row typesdb/schema.ts— allCREATE TABLE/CREATE INDEXstatementsdb/helpers.ts— upsert functions, query helpers, data extraction/parsing utilitiesapi/— one file per API domain, each exporting pure functions that make API calls; barrel-exported fromapi/index.tstools/— one file per tool (or per logical group), each exporting aToolDefinition; barrel-exported fromtools/index.tsupdate-handlers.ts— dispatches incoming events/updates to the right handlers (optional, for real-time integrations)
For simple skills that don't need all of these, you can start with just manifest.json, index.ts, and state.ts, then split into more files as complexity grows.
{
"id": "my-skill",
"name": "My Skill",
"runtime": "quickjs",
"entry": "index.js",
"version": "1.0.0",
"description": "What this skill does",
"auto_start": false,
"platforms": ["windows", "macos", "linux"],
"setup": { "required": true, "label": "Configure My Skill" }
}# Install dependencies
yarn install
# Full build: clean, install skill deps, compile TypeScript, bundle, post-process
yarn build
# Type checking only (no emit)
yarn typecheck
# Watch mode for development
yarn build:watch
# Validate skills (manifest, secrets, code quality)
yarn validate
# Secret scanning only
yarn validate:secrets
# Run all tests
yarn test
# Run specific test
yarn test src/server-ping/__tests__/test-server-ping.ts
# Lint and format
yarn lint
yarn format:check
# Download local model for inference testing
yarn model:download
# Run test script with real local model
yarn test:model <skill-id> <script-file>Skills have access to these global namespaces (defined in types/globals.d.ts):
| Namespace | Purpose |
|---|---|
db |
SQLite database scoped to skill |
net |
HTTP networking (synchronous) |
cron |
Cron scheduling (6-field syntax) |
skills |
Inter-skill communication |
platform |
OS info, env vars, notifications |
state |
Persistent key-value store + real-time frontend pub |
data |
File I/O in skill's data directory |
model |
Local LLM inference (generate, summarize) |
db.exec('CREATE TABLE IF NOT EXISTS logs (...)', []);
db.exec('INSERT INTO logs (msg) VALUES (?)', ['hello']);
const row = db.get('SELECT * FROM logs WHERE id = ?', [1]);
const rows = db.all('SELECT * FROM logs LIMIT 10', []);
db.kvSet('key', { any: 'value' });
const value = db.kvGet('key');const response = net.fetch('https://api.example.com/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: 'test' }),
timeout: 10000,
});
// response: { status: number, headers: Record<string, string>, body: string }// 6-field syntax: seconds minutes hours day month dow
cron.register('every-10s', '*/10 * * * * *');
cron.unregister('every-10s');
const schedules = cron.list();Unified persistent key-value store that also publishes values to the frontend in real time.
state.set('config', { apiKey: 'xxx' }); // Persists AND publishes to frontend
const config = state.get('config'); // Read from persistent store
state.setPartial({ lastPing: Date.now() }); // Bulk set (persists + publishes each key)
state.delete('config'); // Remove from persistent store
const keys = state.keys(); // List all persisted keysdata.write('config.json', JSON.stringify(config, null, 2));
const content = data.read('config.json'); // null if not found// Check if a local model is available
const available = model.isAvailable();
const status = model.getStatus(); // { available, loaded, loading, downloaded, error? }
// Generate text from a prompt
const response = model.generate('What is Bitcoin?', {
maxTokens: 200, // default: 2048
temperature: 0.7, // default: 0.7
topP: 0.9, // default: 0.9
});
// Summarize a block of text
const summary = model.summarize(longText, { maxTokens: 100 });const os = platform.os(); // "windows", "macos", "linux", "android", "ios"
const apiKey = platform.env('MY_API_KEY');
platform.notify('Title', 'Body');const allSkills = skills.list();
const result = skills.callTool('other-skill', 'tool-name', { arg: 'value' });Skills implement these functions (all synchronous):
function init(): void; // Load config, create DB tables
function start(): void; // Register cron schedules, begin work
function stop(): void; // Cleanup, persist state
function onCronTrigger(scheduleId: string): void; // Handle cron triggers
function onSessionStart(args: { sessionId: string }): void; // User started conversation
function onSessionEnd(args: { sessionId: string }): void; // Conversation ended
function onSetupStart(): SetupStartResult; // Return first setup step
function onSetupSubmit(args): SetupSubmitResult; // Process setup step
function onSetupCancel(): void; // Cleanup on cancel
function onDisconnect(): void; // User disconnected skill
function onListOptions(): { options: SkillOption[] }; // Runtime options
function onSetOption(args: { name: string; value: unknown }): void;Skill Load ── init()
│
┌── start()
│ │
│ onCronTrigger(scheduleId) ← fires on schedule
│ │
│ onSessionStart/End
│ │
└── stop()
Tools are exposed to the AI via the global tools array:
tools = [
{
name: 'get-status',
description: 'Get current skill status',
input_schema: {
type: 'object',
properties: {
format: { type: 'string', enum: ['json', 'text'], description: 'Output format' },
},
required: [],
},
execute(args): string {
// Must return JSON string
return JSON.stringify({ status: 'ok', uptime: 99.9 });
},
},
];Important: Tool execute functions must return JSON strings, not objects.
Multi-step configuration wizard:
function onSetupStart(): SetupStartResult {
return {
step: {
id: "credentials",
title: "API Credentials",
description: "Enter your credentials",
fields: [
{ name: "apiKey", type: "password", label: "API Key", required: true },
{ name: "region", type: "select", label: "Region", options: [...] },
],
},
};
}
function onSetupSubmit(args: { stepId: string; values: Record<string, unknown> }): SetupSubmitResult {
if (args.stepId === "credentials") {
if (!args.values.apiKey) {
return { status: "error", errors: [{ field: "apiKey", message: "Required" }] };
}
// Multi-step: return next step
return { status: "next", nextStep: { id: "step2", ... } };
// Or complete:
return { status: "complete" };
}
}Field types: text, password, number, select, boolean.
Runtime-configurable settings:
function onListOptions(): { options: SkillOption[] } {
return {
options: [
{
name: 'interval',
type: 'select',
label: 'Check Interval',
value: String(CONFIG.interval),
options: [
{ label: 'Every 10s', value: '10' },
{ label: 'Every 30s', value: '30' },
],
},
],
};
}
function onSetOption(args: { name: string; value: unknown }): void {
if (args.name === 'interval') {
CONFIG.interval = parseInt(args.value as string);
// Update cron schedule
cron.unregister('work');
cron.register('work', `*/${CONFIG.interval} * * * * *`);
}
}Tests use a Node.js harness (tsx) with mocked bridge APIs.
// src/my-skill/__tests__/test-my-skill.ts
function freshInit(overrides?: Partial<Config>): void {
setupSkillTest({
stateData: { config: { ...defaultConfig, ...overrides } },
fetchResponses: { 'https://api.example.com': { status: 200, body: '{"ok":true}' } },
});
init();
}
_describe('My Skill', () => {
_it('should initialize', () => {
freshInit();
_assertNotNull(state.get('config'));
});
_it('should call API', () => {
freshInit({ apiKey: 'test' });
start();
const result = callTool('get-status', {});
_assertEqual(result.status, 'ok');
});
});setupSkillTest(options?: {
stateData?: Record<string, unknown>;
fetchResponses?: Record<string, { status: number; body: string }>;
env?: Record<string, string>;
platformOs?: string;
});
callTool(name: string, args?: Record<string, unknown>): unknown;
getMockState(): { state, fetchCalls, notifications, cronSchedules, ... };
mockFetchResponse(url: string, status: number, body: string): void;
mockFetchError(url: string, message?: string): void;# Run all tests
yarn test
# Run specific test
yarn test src/server-ping/__tests__/test-server-ping.ts
# Compile only (for debugging)
npx tsc -p tsconfig.test.json- Create directory structure:
mkdir -p src/my-skill/{api,tools,db,__tests__}- Create
manifest.json:
{
"id": "my-skill",
"name": "My Skill",
"runtime": "quickjs",
"entry": "index.js",
"version": "1.0.0",
"description": "What this skill does",
"platforms": ["windows", "macos", "linux"],
"setup": { "required": true, "label": "Configure My Skill" }
}-
Create the core files in this order:
types.ts— all type definitionsstate.ts— state interface + globalThis registrationdb/schema.ts— CREATE TABLE statements + globalThis registrationdb/helpers.ts— upsert/query functions + globalThis registrationapi/*.ts— API functions per domain +api/index.tsbarrel exporttools/*.ts— one tool per file +tools/index.tsbarrel exportsetup.ts— setup wizard stepssync.ts— data sync logic + globalThis registrationindex.ts— lifecycle hooks, imports all modules, assemblestoolsarray
-
(Optional) Add per-skill dependencies by creating a
package.jsonin your skill directory:
{
"name": "@alphahuman/skill-my-skill",
"private": true,
"dependencies": { "some-library": "^1.0.0" }
}Only dependencies are bundled — esbuild inlines them into the single output file.
- Build, validate, and test:
yarn build
yarn typecheck
yarn validate
yarn test src/my-skill/__tests__/test-my-skill.tsSee src/telegram/ for the reference implementation demonstrating the full modular pattern with API layer, 50+ tools, database schema/helpers, setup wizard, sync, and state management.
- TypeScript only — Skills are TypeScript compiled to JavaScript
- QuickJS runtime — Sandboxed JS environment with bridge APIs
- Synchronous execution — No async/await;
net.fetch()is sync with timeout - JSON string results — Tool execute functions must return JSON strings
- 6-field cron — Cron includes seconds:
sec min hour day month dow - SQL params required — Always use
?placeholders, never interpolation - No underscores in skill names — Use lowercase-hyphens (e.g.,
my-skill) - Isolated data — Skills cannot access other skills' databases or files
- Globals via globalThis — Tools must access shared state via
globalThis.getSkillState(), not bare variable names (see Skill State Management pattern)
-
Install skill dependencies:
node scripts/install-skill-deps.mjs- Runs
yarn installin eachsrc/<skill>/that has apackage.json
- Runs
-
TypeScript Compilation:
tsc -p tsconfig.build.json- Input:
src/*/index.ts - Output:
skills/*/index.js
- Input:
-
esbuild Bundling:
node scripts/build-bundle.mjs- Bundles each skill into a single IIFE file with all dependencies inlined
-
Post-Processing (
strip-exports.mjs):- Removes
export {};module boundaries - Normalizes indentation (4-space → 2-space)
- Copies
manifest.jsonto output
- Removes
-
Output: Ready-to-run JavaScript in
skills/
All skills must use the globalThis state pattern for cross-module state access. This ensures state works in both the bundled esbuild IIFE (production) and the test harness.
// state.ts
import type { MyConfig } from './types';
export interface MySkillState {
config: MyConfig;
isRunning: boolean;
cache: { items: Map<string, unknown> };
}
declare global {
function getMySkillState(): MySkillState;
var __mySkillState: MySkillState;
}
const skillState: MySkillState = {
config: { apiKey: '', region: 'us' },
isRunning: false,
cache: { items: new Map() },
};
globalThis.__mySkillState = skillState;
globalThis.getMySkillState = function (): MySkillState {
return globalThis.__mySkillState;
};Why this pattern: Bundled skills use esbuild IIFE format (module-local scopes) and the test harness uses new Function(). Accessing state via globalThis.getMySkillState() works in both environments.
All type definitions for the skill live in a single types.ts file:
// types.ts
// Config stored in state.set('config', ...)
export interface MyConfig {
apiKey: string;
region: string;
syncEnabled: boolean;
}
// API response types
export interface ApiItem {
id: string;
title: string;
updatedAt: string;
}
// Database row types
export interface ItemRow {
id: string;
title: string;
content: string;
synced_at: string;
}All CREATE TABLE and CREATE INDEX statements in one place:
// db/schema.ts
export function initializeSchema(): void {
db.exec(
`CREATE TABLE IF NOT EXISTS items (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
content TEXT,
content_type TEXT DEFAULT 'text',
synced_at TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
)`,
[]
);
db.exec(`CREATE INDEX IF NOT EXISTS idx_items_synced_at ON items(synced_at)`, []);
db.exec(
`CREATE TABLE IF NOT EXISTS sync_state (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)`,
[]
);
}
// Register on globalThis for access from index.ts
declare global {
var initializeMySkillSchema: () => void;
}
globalThis.initializeMySkillSchema = initializeSchema;Upsert, query, and data extraction utilities:
// db/helpers.ts
export function upsertItem(item: ApiItem): void {
db.exec(
`INSERT INTO items (id, title, content, synced_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET title=?, content=?, synced_at=?`,
[item.id, item.title, item.content, item.updatedAt, item.title, item.content, item.updatedAt]
);
}
export function getItemById(id: string): ItemRow | null {
return db.get('SELECT * FROM items WHERE id = ?', [id]) as ItemRow | null;
}
export function searchItems(query: string, limit: number = 20): ItemRow[] {
return db.all('SELECT * FROM items WHERE title LIKE ? OR content LIKE ? LIMIT ?', [
`%${query}%`,
`%${query}%`,
limit,
]) as ItemRow[];
}
// Register on globalThis
declare global {
var mySkillDb: {
upsertItem: typeof upsertItem;
getItemById: typeof getItemById;
searchItems: typeof searchItems;
};
}
globalThis.mySkillDb = { upsertItem, getItemById, searchItems };One file per API domain with pure functions. Barrel-export from api/index.ts:
// api/items.ts
export function fetchItems(
apiKey: string,
cursor?: string
): { items: ApiItem[]; nextCursor?: string } {
const url = `https://api.example.com/items${cursor ? `?cursor=${cursor}` : ''}`;
const response = net.fetch(url, {
headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
timeout: 15000,
});
if (response.status >= 400) throw new Error(`API error: ${response.status}`);
return JSON.parse(response.body);
}
// api/index.ts — barrel export
export { fetchItems, createItem, updateItem } from './items';
export { authenticate, refreshToken } from './auth';One file per tool (or per logical group). Each exports a ToolDefinition:
// tools/search-items.ts
import type { ToolDefinition } from '../../types/globals';
export const searchItemsTool: ToolDefinition = {
name: 'search-items',
description: 'Search items by keyword',
input_schema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search query' },
limit: { type: 'number', description: 'Max results (default 20)' },
},
required: ['query'],
},
execute(args: Record<string, unknown>): string {
const query = args.query as string;
const limit = (args.limit as number) || 20;
const results = globalThis.mySkillDb.searchItems(query, limit);
return JSON.stringify({ success: true, results, count: results.length });
},
};
// tools/index.ts — barrel export
export { searchItemsTool } from './search-items';
export { getItemTool } from './get-item';
export { createItemTool } from './create-item';Multi-step configuration wizard, imported by index.ts:
// setup.ts
export function onSetupStart(): SetupStartResult {
const s = globalThis.getMySkillState();
// If already have a key, show it masked
return {
step: {
id: 'credentials',
title: 'API Credentials',
description: 'Enter your API key',
fields: [
{ name: 'apiKey', type: 'password', label: 'API Key', required: true },
{
name: 'region',
type: 'select',
label: 'Region',
options: [
{ label: 'US', value: 'us' },
{ label: 'EU', value: 'eu' },
],
},
],
},
};
}
export function onSetupSubmit(args: {
stepId: string;
values: Record<string, unknown>;
}): SetupSubmitResult {
if (args.stepId === 'credentials') {
const apiKey = args.values.apiKey as string;
if (!apiKey) return { status: 'error', errors: [{ field: 'apiKey', message: 'Required' }] };
// Validate key works
try {
const result = api.authenticate(apiKey);
const s = globalThis.getMySkillState();
s.config.apiKey = apiKey;
s.config.region = (args.values.region as string) || 'us';
state.set('config', s.config);
return { status: 'complete' };
} catch (e) {
return { status: 'error', errors: [{ field: 'apiKey', message: 'Invalid API key' }] };
}
}
return { status: 'error', errors: [{ field: '', message: 'Unknown step' }] };
}Initial data sync and periodic refresh:
// sync.ts
export function performInitialSync(onProgress?: (msg: string) => void): void {
const s = globalThis.getMySkillState();
onProgress?.('Fetching items...');
let cursor: string | undefined;
let totalSynced = 0;
do {
const result = api.fetchItems(s.config.apiKey, cursor);
for (const item of result.items) {
globalThis.mySkillDb.upsertItem(item);
totalSynced++;
}
cursor = result.nextCursor;
onProgress?.(`Synced ${totalSynced} items...`);
} while (cursor);
db.exec(`INSERT OR REPLACE INTO sync_state (key, value) VALUES ('last_sync', ?)`, [
new Date().toISOString(),
]);
}
// Register on globalThis
declare global {
var mySkillSync: { performInitialSync: typeof performInitialSync };
}
globalThis.mySkillSync = { performInitialSync };The orchestrator that wires everything together:
// index.ts — import order matters
// 4. Sync registration
import * as api from './api';
// 2. DB schema registration
import './db/helpers';
// 1. State first
import './db/schema';
// 5. API layer
import { onSetupStart, onSetupSubmit } from './setup';
import './skill-state';
// 3. DB helpers registration
import './sync';
import { createItemTool, getItemTool, searchItemsTool } from './tools';
function init(): void {
globalThis.initializeMySkillSchema();
const s = globalThis.getMySkillState();
const saved = state.get('config');
if (saved) s.config = { ...s.config, ...(saved as Partial<MyConfig>) };
}
function start(): void {
const s = globalThis.getMySkillState();
if (s.config.apiKey) {
globalThis.mySkillSync.performInitialSync();
cron.register('refresh', '0 */5 * * * *'); // every 5 min
}
s.isRunning = true;
publishState();
}
function stop(): void {
const s = globalThis.getMySkillState();
s.isRunning = false;
cron.unregister('refresh');
state.set('config', s.config);
}
function onCronTrigger(scheduleId: string): void {
if (scheduleId === 'refresh') {
globalThis.mySkillSync.performInitialSync();
publishState();
}
}
function publishState(): void {
const s = globalThis.getMySkillState();
state.setPartial({
connection_status: s.isRunning ? 'connected' : 'disconnected',
is_initialized: true,
});
}
// Expose for tools
const _g = globalThis as Record<string, unknown>;
_g.publishState = publishState;
tools = [searchItemsTool, getItemTool, createItemTool];Every module that needs cross-module access registers on globalThis:
| Module | Registers | Purpose |
|---|---|---|
state.ts |
globalThis.getMySkillState() |
State access |
db/schema.ts |
globalThis.initializeMySkillSchema() |
Schema creation |
db/helpers.ts |
globalThis.mySkillDb.* |
DB operations |
sync.ts |
globalThis.mySkillSync.* |
Sync operations |
index.ts |
globalThis.publishState() etc. |
Lifecycle helpers for tools |
Always publish state to the frontend via state.setPartial():
function publishState(): void {
const s = globalThis.getMySkillState();
state.setPartial({
connection_status: s.isRunning ? 'connected' : 'disconnected',
is_initialized: true,
lastSync: db.get("SELECT value FROM sync_state WHERE key = 'last_sync'", [])?.value ?? null,
itemCount: (db.get('SELECT COUNT(*) as count FROM items', []) as { count: number })?.count ?? 0,
});
}function onCronTrigger(scheduleId: string): void {
if (scheduleId === 'refresh') {
try {
globalThis.mySkillSync.performInitialSync();
publishState();
} catch (e) {
console.error(`Sync error: ${e}`);
platform.notify('Sync Failed', String(e));
}
}
}All bridge API types are in types/globals.d.ts. Key interfaces:
interface ToolDefinition {
name: string;
description: string;
input_schema: ToolInputSchema;
execute: (args: Record<string, unknown>) => string;
}
interface SetupStep {
id: string;
title: string;
description: string;
fields: SetupField[];
}
interface SetupField {
name: string;
type: 'text' | 'select' | 'boolean' | 'number' | 'password';
label: string;
description?: string;
required?: boolean;
default?: unknown;
options?: SetupFieldOption[];
}
interface SetupStartResult {
step: SetupStep;
}
interface SetupSubmitResult {
status: 'next' | 'complete' | 'error';
nextStep?: SetupStep;
errors?: SetupFieldError[];
}
interface SkillOption {
name: string;
type: 'boolean' | 'text' | 'number' | 'select';
label: string;
value: unknown;
options?: SetupFieldOption[];
}The skills-py/ directory contains legacy Python skills that are being migrated to TypeScript. Do not create new Python skills — all new skills should be TypeScript.