Update memory: Camie Tagger v2 upgrade results
This commit is contained in:
721
skills/openclaw-claude-code/src/index.ts
Normal file
721
skills/openclaw-claude-code/src/index.ts
Normal file
@@ -0,0 +1,721 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Claude Code Skill for ClawdBot
|
||||
* Provides CLI access to Claude Code via MCP protocol
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
|
||||
const SASHA_DOCTOR_URL = process.env.SASHA_DOCTOR_URL || 'http://127.0.0.1:18795';
|
||||
const PREFIX = '/sasha-doctor/claude-code';
|
||||
|
||||
interface ApiResponse {
|
||||
ok: boolean;
|
||||
error?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
async function apiCall(endpoint: string, method: string = 'GET', body?: object): Promise<ApiResponse> {
|
||||
const url = `${SASHA_DOCTOR_URL}${PREFIX}${endpoint}`;
|
||||
const options: RequestInit = {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
if (body) {
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
return await response.json() as ApiResponse;
|
||||
} catch (error) {
|
||||
return { ok: false, error: (error as Error).message };
|
||||
}
|
||||
}
|
||||
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name('claude-code-skill')
|
||||
.description('Control Claude Code via MCP protocol')
|
||||
.version('1.0.0');
|
||||
|
||||
// Connect command
|
||||
program
|
||||
.command('connect')
|
||||
.description('Connect to Claude Code MCP server')
|
||||
.action(async () => {
|
||||
console.log('Connecting to Claude Code...');
|
||||
const result = await apiCall('/connect', 'POST');
|
||||
if (result.ok) {
|
||||
console.log(`Connected! Status: ${result.status}`);
|
||||
console.log(`Server: ${JSON.stringify(result.server)}`);
|
||||
console.log(`Available tools: ${result.tools}`);
|
||||
} else {
|
||||
console.error(`Failed: ${result.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Disconnect command
|
||||
program
|
||||
.command('disconnect')
|
||||
.description('Disconnect from Claude Code MCP server')
|
||||
.action(async () => {
|
||||
const result = await apiCall('/disconnect', 'POST');
|
||||
if (result.ok) {
|
||||
console.log('Disconnected from Claude Code');
|
||||
} else {
|
||||
console.error(`Failed: ${result.error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// List tools command
|
||||
program
|
||||
.command('tools')
|
||||
.description('List available Claude Code tools')
|
||||
.action(async () => {
|
||||
const result = await apiCall('/tools');
|
||||
if (result.ok && Array.isArray(result.tools)) {
|
||||
console.log('Available tools:');
|
||||
for (const tool of result.tools) {
|
||||
const t = tool as { name: string; description: string };
|
||||
console.log(` - ${t.name}: ${t.description}`);
|
||||
}
|
||||
} else {
|
||||
console.error(`Failed: ${result.error || 'Not connected'}`);
|
||||
console.log('Tip: Run "claude-code-skill connect" first');
|
||||
}
|
||||
});
|
||||
|
||||
// Bash command
|
||||
program
|
||||
.command('bash <command>')
|
||||
.description('Execute a bash command via Claude Code')
|
||||
.option('-d, --description <desc>', 'Description of what the command does')
|
||||
.action(async (command: string, options: { description?: string }) => {
|
||||
const result = await apiCall('/bash', 'POST', {
|
||||
command,
|
||||
description: options.description || ''
|
||||
});
|
||||
if (result.ok) {
|
||||
const r = result.result as { stdout?: string; stderr?: string };
|
||||
if (r.stdout) console.log(r.stdout);
|
||||
if (r.stderr) console.error(r.stderr);
|
||||
} else {
|
||||
console.error(`Failed: ${result.error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Read file command
|
||||
program
|
||||
.command('read <file>')
|
||||
.description('Read a file via Claude Code')
|
||||
.action(async (filePath: string) => {
|
||||
const result = await apiCall('/read', 'POST', { file_path: filePath });
|
||||
if (result.ok) {
|
||||
const r = result.result as { type?: string; file?: { content?: string } };
|
||||
if (r.file?.content) {
|
||||
console.log(r.file.content);
|
||||
} else {
|
||||
console.log(JSON.stringify(result.result, null, 2));
|
||||
}
|
||||
} else {
|
||||
console.error(`Failed: ${result.error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Call any tool
|
||||
program
|
||||
.command('call <tool>')
|
||||
.description('Call any Claude Code tool with JSON args')
|
||||
.option('-a, --args <json>', 'JSON arguments for the tool', '{}')
|
||||
.action(async (tool: string, options: { args: string }) => {
|
||||
let args: object;
|
||||
try {
|
||||
args = JSON.parse(options.args);
|
||||
} catch {
|
||||
console.error('Invalid JSON args');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const result = await apiCall('/call', 'POST', { tool, args });
|
||||
if (result.ok) {
|
||||
console.log(JSON.stringify(result.result, null, 2));
|
||||
} else {
|
||||
console.error(`Failed: ${result.error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Glob search
|
||||
program
|
||||
.command('glob <pattern>')
|
||||
.description('Search for files by pattern')
|
||||
.option('-p, --path <dir>', 'Directory to search in')
|
||||
.action(async (pattern: string, options: { path?: string }) => {
|
||||
const args: { pattern: string; path?: string } = { pattern };
|
||||
if (options.path) args.path = options.path;
|
||||
|
||||
const result = await apiCall('/call', 'POST', { tool: 'Glob', args });
|
||||
if (result.ok) {
|
||||
const r = result.result as { filenames?: string[] };
|
||||
if (r.filenames) {
|
||||
for (const f of r.filenames) console.log(f);
|
||||
} else {
|
||||
console.log(JSON.stringify(result.result, null, 2));
|
||||
}
|
||||
} else {
|
||||
console.error(`Failed: ${result.error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Grep search
|
||||
program
|
||||
.command('grep <pattern>')
|
||||
.description('Search file contents')
|
||||
.option('-p, --path <dir>', 'Directory to search in')
|
||||
.option('-g, --glob <pattern>', 'File pattern to filter')
|
||||
.option('-c, --content', 'Show matching content')
|
||||
.action(async (pattern: string, options: { path?: string; glob?: string; content?: boolean }) => {
|
||||
const args: Record<string, unknown> = { pattern };
|
||||
if (options.path) args.path = options.path;
|
||||
if (options.glob) args.glob = options.glob;
|
||||
if (options.content) args.output_mode = 'content';
|
||||
|
||||
const result = await apiCall('/call', 'POST', { tool: 'Grep', args });
|
||||
if (result.ok) {
|
||||
const r = result.result as { filenames?: string[]; content?: string };
|
||||
if (r.content) {
|
||||
console.log(r.content);
|
||||
} else if (r.filenames) {
|
||||
for (const f of r.filenames) console.log(f);
|
||||
} else {
|
||||
console.log(JSON.stringify(result.result, null, 2));
|
||||
}
|
||||
} else {
|
||||
console.error(`Failed: ${result.error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Status command
|
||||
program
|
||||
.command('status')
|
||||
.description('Check connection status')
|
||||
.action(async () => {
|
||||
// Try to get tools - if it works, we're connected
|
||||
const result = await apiCall('/tools');
|
||||
if (result.ok) {
|
||||
console.log('Status: Connected');
|
||||
console.log(`Tools available: ${(result.tools as unknown[]).length}`);
|
||||
} else {
|
||||
console.log('Status: Not connected');
|
||||
console.log('Run "claude-code-skill connect" to connect');
|
||||
}
|
||||
});
|
||||
|
||||
// List sessions command
|
||||
program
|
||||
.command('sessions')
|
||||
.description('List all Claude Code sessions')
|
||||
.option('-n, --limit <n>', 'Limit number of sessions', '10')
|
||||
.action(async (options: { limit: string }) => {
|
||||
const result = await apiCall('/sessions');
|
||||
if (result.ok && Array.isArray(result.sessions)) {
|
||||
const sessions = result.sessions.slice(0, parseInt(options.limit));
|
||||
console.log('Recent Claude Code sessions:\n');
|
||||
for (const s of sessions) {
|
||||
const sess = s as {
|
||||
sessionId: string;
|
||||
summary?: string;
|
||||
projectPath?: string;
|
||||
modified?: string;
|
||||
messageCount?: number;
|
||||
};
|
||||
console.log(` ${sess.sessionId}`);
|
||||
console.log(` Summary: ${sess.summary || 'N/A'}`);
|
||||
console.log(` Project: ${sess.projectPath || 'N/A'}`);
|
||||
console.log(` Modified: ${sess.modified || 'N/A'}`);
|
||||
console.log(` Messages: ${sess.messageCount || 0}`);
|
||||
console.log('');
|
||||
}
|
||||
} else {
|
||||
console.error(`Failed: ${result.error || 'Unknown error'}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Resume session command
|
||||
program
|
||||
.command('resume <sessionId> <prompt>')
|
||||
.description('Resume a Claude Code session with a prompt')
|
||||
.option('-d, --cwd <dir>', 'Working directory')
|
||||
.action(async (sessionId: string, prompt: string, options: { cwd?: string }) => {
|
||||
console.log(`Resuming session ${sessionId}...`);
|
||||
const result = await apiCall('/resume', 'POST', {
|
||||
sessionId,
|
||||
prompt,
|
||||
cwd: options.cwd
|
||||
});
|
||||
if (result.ok) {
|
||||
console.log(result.output as string);
|
||||
if (result.stderr) console.error(result.stderr as string);
|
||||
} else {
|
||||
console.error(`Failed: ${result.error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Continue command
|
||||
program
|
||||
.command('continue <prompt>')
|
||||
.description('Continue the most recent session in a directory')
|
||||
.option('-d, --cwd <dir>', 'Working directory', process.cwd())
|
||||
.action(async (prompt: string, options: { cwd: string }) => {
|
||||
console.log(`Continuing session in ${options.cwd}...`);
|
||||
const result = await apiCall('/continue', 'POST', {
|
||||
prompt,
|
||||
cwd: options.cwd
|
||||
});
|
||||
if (result.ok) {
|
||||
console.log(result.output as string);
|
||||
if (result.stderr) console.error(result.stderr as string);
|
||||
} else {
|
||||
console.error(`Failed: ${result.error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// === Persistent Session Commands ===
|
||||
|
||||
// Start a persistent session
|
||||
program
|
||||
.command('session-start [name]')
|
||||
.description('Start a persistent Claude Code session')
|
||||
.option('-d, --cwd <dir>', 'Working directory')
|
||||
.option('-r, --resume <sessionId>', 'Resume an existing Claude session')
|
||||
.option('-m, --model <model>', 'Model to use')
|
||||
.option('-b, --base-url <url>', 'Custom API endpoint (for Gemini/GPT proxy)')
|
||||
.option('--permission-mode <mode>', 'Permission mode: acceptEdits, bypassPermissions, default, delegate, dontAsk, plan', 'acceptEdits')
|
||||
.option('--fork-session', 'Create a new session ID instead of reusing (use with --resume)')
|
||||
.option('--allowed-tools <tools>', 'Comma-separated list of tools to auto-approve (e.g. Bash,Read,Edit)')
|
||||
.option('--disallowed-tools <tools>', 'Comma-separated list of tools to deny')
|
||||
.option('--tools <tools>', 'Limit available tools (use "" to disable all, "default" for all)')
|
||||
.option('--max-turns <n>', 'Maximum agent loop turns')
|
||||
.option('--max-budget <usd>', 'Maximum API spend in USD')
|
||||
.option('--system-prompt <prompt>', 'Replace system prompt completely')
|
||||
.option('--append-system-prompt <prompt>', 'Append to system prompt')
|
||||
.option('--skip-permissions', 'Skip all permission checks (dangerous!)')
|
||||
.option('--agents <json>', 'Custom sub-agents JSON (e.g. \'{"reviewer": {"prompt": "You review code"}}\')')
|
||||
.option('--agent <name>', 'Default agent to use')
|
||||
.option('--session-id <uuid>', 'Use a specific session ID (must be valid UUID)')
|
||||
.option('--add-dir <dirs>', 'Additional directories to allow tool access (comma-separated)')
|
||||
.action(async (name: string | undefined, options: {
|
||||
cwd?: string;
|
||||
resume?: string;
|
||||
model?: string;
|
||||
baseUrl?: string;
|
||||
permissionMode?: string;
|
||||
forkSession?: boolean;
|
||||
allowedTools?: string;
|
||||
disallowedTools?: string;
|
||||
tools?: string;
|
||||
maxTurns?: string;
|
||||
maxBudget?: string;
|
||||
systemPrompt?: string;
|
||||
appendSystemPrompt?: string;
|
||||
skipPermissions?: boolean;
|
||||
agents?: string;
|
||||
agent?: string;
|
||||
sessionId?: string;
|
||||
addDir?: string;
|
||||
}) => {
|
||||
const sessionName = name || `session-${Date.now()}`;
|
||||
console.log(`Starting persistent session: ${sessionName}...`);
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
name: sessionName,
|
||||
cwd: options.cwd,
|
||||
sessionId: options.resume,
|
||||
model: options.model,
|
||||
baseUrl: options.baseUrl,
|
||||
permissionMode: options.permissionMode || 'acceptEdits',
|
||||
forkSession: options.forkSession
|
||||
};
|
||||
|
||||
// New flags
|
||||
if (options.allowedTools) {
|
||||
body.allowedTools = options.allowedTools.split(',').map(t => t.trim());
|
||||
}
|
||||
if (options.disallowedTools) {
|
||||
body.disallowedTools = options.disallowedTools.split(',').map(t => t.trim());
|
||||
}
|
||||
if (options.tools) {
|
||||
body.tools = options.tools === '""' ? [] : options.tools.split(',').map(t => t.trim());
|
||||
}
|
||||
if (options.maxTurns) {
|
||||
body.maxTurns = parseInt(options.maxTurns);
|
||||
}
|
||||
if (options.maxBudget) {
|
||||
body.maxBudgetUsd = parseFloat(options.maxBudget);
|
||||
}
|
||||
if (options.systemPrompt) {
|
||||
body.systemPrompt = options.systemPrompt;
|
||||
}
|
||||
if (options.appendSystemPrompt) {
|
||||
body.appendSystemPrompt = options.appendSystemPrompt;
|
||||
}
|
||||
if (options.skipPermissions) {
|
||||
body.dangerouslySkipPermissions = true;
|
||||
}
|
||||
if (options.agents) {
|
||||
try {
|
||||
body.agents = JSON.parse(options.agents);
|
||||
} catch {
|
||||
console.error('Invalid --agents JSON');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
if (options.agent) {
|
||||
body.agent = options.agent;
|
||||
}
|
||||
if (options.sessionId) {
|
||||
body.customSessionId = options.sessionId;
|
||||
}
|
||||
if (options.addDir) {
|
||||
body.addDir = options.addDir.split(',').map(d => d.trim());
|
||||
}
|
||||
|
||||
const result = await apiCall('/session/start', 'POST', body);
|
||||
|
||||
if (result.ok) {
|
||||
console.log(`Session '${sessionName}' started!`);
|
||||
if (result.claudeSessionId) {
|
||||
console.log(`Claude Session ID: ${result.claudeSessionId}`);
|
||||
}
|
||||
// Show active options
|
||||
if (options.model) console.log(`Model: ${options.model}`);
|
||||
if (options.baseUrl) console.log(`Base URL: ${options.baseUrl}`);
|
||||
console.log(`Permission mode: ${options.permissionMode || 'acceptEdits'}`);
|
||||
if (options.allowedTools) console.log(`Allowed tools: ${options.allowedTools}`);
|
||||
if (options.disallowedTools) console.log(`Disallowed tools: ${options.disallowedTools}`);
|
||||
if (options.tools) console.log(`Available tools: ${options.tools}`);
|
||||
if (options.maxTurns) console.log(`Max turns: ${options.maxTurns}`);
|
||||
if (options.maxBudget) console.log(`Max budget: $${options.maxBudget}`);
|
||||
if (options.forkSession) console.log(`Fork mode: enabled`);
|
||||
} else {
|
||||
console.error(`Failed: ${result.error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Send message to a persistent session
|
||||
program
|
||||
.command('session-send <name> <message>')
|
||||
.description('Send a message to a persistent session')
|
||||
.option('-t, --timeout <ms>', 'Timeout in milliseconds', '120000')
|
||||
.option('-s, --stream', 'Stream output in real-time')
|
||||
.action(async (name: string, message: string, options: { timeout: string; stream?: boolean }) => {
|
||||
console.log(`Sending to session '${name}'...`);
|
||||
|
||||
if (options.stream) {
|
||||
// Use SSE streaming endpoint
|
||||
const url = `${SASHA_DOCTOR_URL}${PREFIX}/session/send-stream`;
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, message, timeout: parseInt(options.timeout) })
|
||||
});
|
||||
|
||||
if (!response.ok || !response.body) {
|
||||
console.error('Failed to connect to stream');
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
try {
|
||||
const data = JSON.parse(line.slice(6)) as { type: string; text?: string; tool?: string; error?: string };
|
||||
if (data.type === 'text') {
|
||||
process.stdout.write(data.text || '');
|
||||
} else if (data.type === 'tool_use') {
|
||||
console.log(`\n🔧 [Tool: ${data.tool}]`);
|
||||
} else if (data.type === 'tool_result') {
|
||||
console.log('✓ Tool completed');
|
||||
} else if (data.type === 'done') {
|
||||
console.log('\n--- Done ---');
|
||||
} else if (data.type === 'error') {
|
||||
console.error(`\nError: ${data.error}`);
|
||||
}
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Stream error: ${(error as Error).message}`);
|
||||
}
|
||||
} else {
|
||||
// Non-streaming mode
|
||||
const result = await apiCall('/session/send', 'POST', {
|
||||
name,
|
||||
message,
|
||||
timeout: parseInt(options.timeout)
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
console.log(result.response as string);
|
||||
} else {
|
||||
console.error(`Failed: ${result.error}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// List active sessions
|
||||
program
|
||||
.command('session-list')
|
||||
.description('List active persistent sessions')
|
||||
.action(async () => {
|
||||
const result = await apiCall('/session/list');
|
||||
if (result.ok && Array.isArray(result.sessions)) {
|
||||
if (result.sessions.length === 0) {
|
||||
console.log('No active persistent sessions.');
|
||||
} else {
|
||||
console.log('Active persistent sessions:\n');
|
||||
for (const s of result.sessions) {
|
||||
const sess = s as {
|
||||
name: string;
|
||||
cwd?: string;
|
||||
created?: string;
|
||||
isReady?: boolean;
|
||||
};
|
||||
console.log(` ${sess.name}`);
|
||||
console.log(` CWD: ${sess.cwd || 'N/A'}`);
|
||||
console.log(` Created: ${sess.created || 'N/A'}`);
|
||||
console.log(` Ready: ${sess.isReady ? 'Yes' : 'No'}`);
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error(`Failed: ${result.error || 'Unknown error'}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Stop a persistent session
|
||||
program
|
||||
.command('session-stop <name>')
|
||||
.description('Stop a persistent session')
|
||||
.action(async (name: string) => {
|
||||
const result = await apiCall('/session/stop', 'POST', { name });
|
||||
if (result.ok) {
|
||||
console.log(`Session '${name}' stopped.`);
|
||||
} else {
|
||||
console.error(`Failed: ${result.error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Get session status
|
||||
program
|
||||
.command('session-status <name>')
|
||||
.description('Get detailed status of a persistent session')
|
||||
.action(async (name: string) => {
|
||||
const result = await apiCall('/session/status', 'POST', { name });
|
||||
if (result.ok) {
|
||||
console.log(`Session: ${name}`);
|
||||
console.log(` Claude ID: ${result.claudeSessionId || 'N/A'}`);
|
||||
console.log(` CWD: ${result.cwd}`);
|
||||
console.log(` Created: ${result.created}`);
|
||||
|
||||
const stats = result.stats as {
|
||||
turns?: number;
|
||||
toolCalls?: number;
|
||||
tokensIn?: number;
|
||||
tokensOut?: number;
|
||||
uptime?: number;
|
||||
lastActivity?: string;
|
||||
isReady?: boolean;
|
||||
};
|
||||
|
||||
console.log('\nStatistics:');
|
||||
console.log(` Ready: ${stats.isReady ? 'Yes' : 'No'}`);
|
||||
console.log(` Turns: ${stats.turns || 0}`);
|
||||
console.log(` Tool Calls: ${stats.toolCalls || 0}`);
|
||||
console.log(` Tokens In: ${stats.tokensIn || 0}`);
|
||||
console.log(` Tokens Out: ${stats.tokensOut || 0}`);
|
||||
console.log(` Uptime: ${stats.uptime || 0}s`);
|
||||
console.log(` Last Activity: ${stats.lastActivity || 'N/A'}`);
|
||||
} else {
|
||||
console.error(`Failed: ${result.error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Get session history
|
||||
program
|
||||
.command('session-history <name>')
|
||||
.description('Get conversation history of a persistent session')
|
||||
.option('-n, --limit <n>', 'Number of events to show', '20')
|
||||
.action(async (name: string, options: { limit: string }) => {
|
||||
const result = await apiCall('/session/history', 'POST', {
|
||||
name,
|
||||
limit: parseInt(options.limit)
|
||||
});
|
||||
if (result.ok) {
|
||||
console.log(`Session '${name}' history (${result.count} events):\n`);
|
||||
|
||||
const history = result.history as Array<{
|
||||
time: string;
|
||||
type: string;
|
||||
event: { message?: { content?: Array<{ type: string; text?: string; name?: string }> } };
|
||||
}>;
|
||||
|
||||
for (const entry of history) {
|
||||
const time = new Date(entry.time).toLocaleTimeString();
|
||||
const type = entry.type.padEnd(12);
|
||||
|
||||
let content = '';
|
||||
if (entry.event.message?.content) {
|
||||
for (const c of entry.event.message.content) {
|
||||
if (c.type === 'text' && c.text) {
|
||||
content = c.text.substring(0, 60).replace(/\n/g, ' ');
|
||||
if (c.text.length > 60) content += '...';
|
||||
} else if (c.type === 'tool_use' && c.name) {
|
||||
content = `[Tool: ${c.name}]`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[${time}] ${type} ${content}`);
|
||||
}
|
||||
} else {
|
||||
console.error(`Failed: ${result.error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Pause a persistent session
|
||||
program
|
||||
.command('session-pause <name>')
|
||||
.description('Pause a persistent session (saves state)')
|
||||
.action(async (name: string) => {
|
||||
const result = await apiCall('/session/pause', 'POST', { name });
|
||||
if (result.ok) {
|
||||
console.log(`Session '${name}' paused.`);
|
||||
console.log(`Resume with: claude-code-skill session-resume ${name}`);
|
||||
} else {
|
||||
console.error(`Failed: ${result.error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Resume a paused session
|
||||
program
|
||||
.command('session-resume-paused <name>')
|
||||
.description('Resume a paused persistent session')
|
||||
.action(async (name: string) => {
|
||||
const result = await apiCall('/session/resume', 'POST', { name });
|
||||
if (result.ok) {
|
||||
console.log(`Session '${name}' resumed.`);
|
||||
} else {
|
||||
console.error(`Failed: ${result.error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Fork a session
|
||||
program
|
||||
.command('session-fork <name> <newName>')
|
||||
.description('Fork an existing session to create a branch')
|
||||
.action(async (name: string, newName: string) => {
|
||||
const result = await apiCall('/session/fork', 'POST', { name, newName });
|
||||
if (result.ok) {
|
||||
console.log(`Session '${name}' forked to '${newName}'`);
|
||||
if (result.claudeSessionId) {
|
||||
console.log(`New Claude Session ID: ${result.claudeSessionId}`);
|
||||
}
|
||||
} else {
|
||||
console.error(`Failed: ${result.error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Search sessions
|
||||
program
|
||||
.command('session-search [query]')
|
||||
.description('Search sessions by project path, summary, or time')
|
||||
.option('-p, --project <path>', 'Filter by project path')
|
||||
.option('-s, --since <time>', 'Show sessions since (e.g., "1h", "2d", "2024-01-01")')
|
||||
.option('-n, --limit <n>', 'Limit results', '20')
|
||||
.action(async (query: string | undefined, options: { project?: string; since?: string; limit: string }) => {
|
||||
const result = await apiCall('/session/search', 'POST', {
|
||||
query,
|
||||
project: options.project,
|
||||
since: options.since,
|
||||
limit: parseInt(options.limit)
|
||||
});
|
||||
|
||||
if (result.ok && Array.isArray(result.sessions)) {
|
||||
if (result.sessions.length === 0) {
|
||||
console.log('No sessions found.');
|
||||
} else {
|
||||
console.log(`Found ${result.sessions.length} session(s):\n`);
|
||||
for (const s of result.sessions) {
|
||||
const sess = s as { name: string; cwd?: string; created?: string; summary?: string };
|
||||
console.log(` ${sess.name}`);
|
||||
console.log(` CWD: ${sess.cwd || 'N/A'}`);
|
||||
console.log(` Created: ${sess.created || 'N/A'}`);
|
||||
if (sess.summary) console.log(` Summary: ${sess.summary}`);
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error(`Failed: ${result.error || 'Unknown error'}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Batch read files
|
||||
program
|
||||
.command('batch-read <patterns...>')
|
||||
.description('Read multiple files at once by glob patterns')
|
||||
.option('-p, --path <dir>', 'Base directory')
|
||||
.action(async (patterns: string[], options: { path?: string }) => {
|
||||
const result = await apiCall('/batch-read', 'POST', {
|
||||
patterns,
|
||||
basePath: options.path
|
||||
});
|
||||
|
||||
if (result.ok && result.files) {
|
||||
const files = result.files as Array<{ path: string; content: string; error?: string }>;
|
||||
console.log(`Read ${files.length} file(s):\n`);
|
||||
for (const file of files) {
|
||||
console.log(`=== ${file.path} ===`);
|
||||
if (file.error) {
|
||||
console.error(`Error: ${file.error}`);
|
||||
} else {
|
||||
console.log(file.content);
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
} else {
|
||||
console.error(`Failed: ${result.error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Restart a failed session
|
||||
program
|
||||
.command('session-restart <name>')
|
||||
.description('Restart a failed or stopped session')
|
||||
.action(async (name: string) => {
|
||||
const result = await apiCall('/session/restart', 'POST', { name });
|
||||
if (result.ok) {
|
||||
console.log(`Session '${name}' restarted.`);
|
||||
} else {
|
||||
console.error(`Failed: ${result.error}`);
|
||||
}
|
||||
});
|
||||
|
||||
program.parse();
|
||||
365
skills/openclaw-claude-code/src/mcp/actions.ts
Normal file
365
skills/openclaw-claude-code/src/mcp/actions.ts
Normal file
@@ -0,0 +1,365 @@
|
||||
/**
|
||||
* MCP Actions - Server-side actions for managing MCP clients
|
||||
*/
|
||||
|
||||
import {
|
||||
createClient,
|
||||
executeRequest,
|
||||
listTools,
|
||||
removeClient,
|
||||
} from "./client";
|
||||
import { MCPClientLogger } from "./logger";
|
||||
import {
|
||||
DEFAULT_MCP_CONFIG,
|
||||
McpClientData,
|
||||
McpConfigData,
|
||||
McpRequestMessage,
|
||||
ServerConfig,
|
||||
ServerStatusResponse,
|
||||
} from "./types";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
const logger = new MCPClientLogger("MCP Actions");
|
||||
|
||||
// Default config path - can be overridden
|
||||
let CONFIG_PATH = path.join(process.cwd(), "mcp_config.json");
|
||||
|
||||
const clientsMap = new Map<string, McpClientData>();
|
||||
|
||||
// Set custom config path
|
||||
export function setConfigPath(configPath: string) {
|
||||
CONFIG_PATH = configPath;
|
||||
}
|
||||
|
||||
// Get clients status
|
||||
export async function getClientsStatus(): Promise<
|
||||
Record<string, ServerStatusResponse>
|
||||
> {
|
||||
const config = await getMcpConfigFromFile();
|
||||
const result: Record<string, ServerStatusResponse> = {};
|
||||
|
||||
for (const clientId of Object.keys(config.mcpServers)) {
|
||||
const status = clientsMap.get(clientId);
|
||||
const serverConfig = config.mcpServers[clientId];
|
||||
|
||||
if (!serverConfig) {
|
||||
result[clientId] = { status: "undefined", errorMsg: null };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (serverConfig.status === "paused") {
|
||||
result[clientId] = { status: "paused", errorMsg: null };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!status) {
|
||||
result[clientId] = { status: "undefined", errorMsg: null };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
status.client === null &&
|
||||
status.tools === null &&
|
||||
status.errorMsg === null
|
||||
) {
|
||||
result[clientId] = { status: "initializing", errorMsg: null };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (status.errorMsg) {
|
||||
result[clientId] = { status: "error", errorMsg: status.errorMsg };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (status.client) {
|
||||
result[clientId] = { status: "active", errorMsg: null };
|
||||
continue;
|
||||
}
|
||||
|
||||
result[clientId] = { status: "error", errorMsg: "Client not found" };
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Get client tools
|
||||
export async function getClientTools(clientId: string) {
|
||||
return clientsMap.get(clientId)?.tools ?? null;
|
||||
}
|
||||
|
||||
// Get available clients count
|
||||
export async function getAvailableClientsCount() {
|
||||
let count = 0;
|
||||
clientsMap.forEach((map) => !map.errorMsg && count++);
|
||||
return count;
|
||||
}
|
||||
|
||||
// Get all tools from all clients
|
||||
export async function getAllTools() {
|
||||
const result = [];
|
||||
for (const [clientId, status] of clientsMap.entries()) {
|
||||
result.push({
|
||||
clientId,
|
||||
tools: status.tools,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Initialize single client
|
||||
async function initializeSingleClient(
|
||||
clientId: string,
|
||||
serverConfig: ServerConfig,
|
||||
) {
|
||||
if (serverConfig.status === "paused") {
|
||||
logger.info(`Skipping initialization for paused client [${clientId}]`);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Initializing client [${clientId}]...`);
|
||||
|
||||
clientsMap.set(clientId, {
|
||||
client: null,
|
||||
tools: null,
|
||||
errorMsg: null,
|
||||
});
|
||||
|
||||
createClient(clientId, serverConfig)
|
||||
.then(async (client) => {
|
||||
const tools = await listTools(client);
|
||||
logger.info(
|
||||
`Supported tools for [${clientId}]: ${JSON.stringify(tools, null, 2)}`,
|
||||
);
|
||||
clientsMap.set(clientId, { client, tools, errorMsg: null });
|
||||
logger.success(`Client [${clientId}] initialized successfully`);
|
||||
})
|
||||
.catch((error) => {
|
||||
clientsMap.set(clientId, {
|
||||
client: null,
|
||||
tools: null,
|
||||
errorMsg: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
logger.error(`Failed to initialize client [${clientId}]: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize MCP system
|
||||
export async function initializeMcpSystem() {
|
||||
logger.info("MCP Actions starting...");
|
||||
try {
|
||||
if (clientsMap.size > 0) {
|
||||
logger.info("MCP system already initialized, skipping...");
|
||||
return;
|
||||
}
|
||||
|
||||
const config = await getMcpConfigFromFile();
|
||||
for (const [clientId, serverConfig] of Object.entries(config.mcpServers)) {
|
||||
await initializeSingleClient(clientId, serverConfig);
|
||||
}
|
||||
return config;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to initialize MCP system: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Add MCP server
|
||||
export async function addMcpServer(clientId: string, config: ServerConfig) {
|
||||
try {
|
||||
const currentConfig = await getMcpConfigFromFile();
|
||||
const isNewServer = !(clientId in currentConfig.mcpServers);
|
||||
|
||||
if (isNewServer && !config.status) {
|
||||
config.status = "active";
|
||||
}
|
||||
|
||||
const newConfig = {
|
||||
...currentConfig,
|
||||
mcpServers: {
|
||||
...currentConfig.mcpServers,
|
||||
[clientId]: config,
|
||||
},
|
||||
};
|
||||
await updateMcpConfig(newConfig);
|
||||
|
||||
if (isNewServer || config.status === "active") {
|
||||
await initializeSingleClient(clientId, config);
|
||||
}
|
||||
|
||||
return newConfig;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to add server [${clientId}]: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Pause MCP server
|
||||
export async function pauseMcpServer(clientId: string) {
|
||||
try {
|
||||
const currentConfig = await getMcpConfigFromFile();
|
||||
const serverConfig = currentConfig.mcpServers[clientId];
|
||||
if (!serverConfig) {
|
||||
throw new Error(`Server ${clientId} not found`);
|
||||
}
|
||||
|
||||
const newConfig: McpConfigData = {
|
||||
...currentConfig,
|
||||
mcpServers: {
|
||||
...currentConfig.mcpServers,
|
||||
[clientId]: {
|
||||
...serverConfig,
|
||||
status: "paused",
|
||||
},
|
||||
},
|
||||
};
|
||||
await updateMcpConfig(newConfig);
|
||||
|
||||
const client = clientsMap.get(clientId);
|
||||
if (client?.client) {
|
||||
await removeClient(client.client);
|
||||
}
|
||||
clientsMap.delete(clientId);
|
||||
|
||||
return newConfig;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to pause server [${clientId}]: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Resume MCP server
|
||||
export async function resumeMcpServer(clientId: string): Promise<void> {
|
||||
try {
|
||||
const currentConfig = await getMcpConfigFromFile();
|
||||
const serverConfig = currentConfig.mcpServers[clientId];
|
||||
if (!serverConfig) {
|
||||
throw new Error(`Server ${clientId} not found`);
|
||||
}
|
||||
|
||||
logger.info(`Trying to initialize client [${clientId}]...`);
|
||||
try {
|
||||
const client = await createClient(clientId, serverConfig);
|
||||
const tools = await listTools(client);
|
||||
clientsMap.set(clientId, { client, tools, errorMsg: null });
|
||||
logger.success(`Client [${clientId}] initialized successfully`);
|
||||
|
||||
const newConfig: McpConfigData = {
|
||||
...currentConfig,
|
||||
mcpServers: {
|
||||
...currentConfig.mcpServers,
|
||||
[clientId]: {
|
||||
...serverConfig,
|
||||
status: "active" as const,
|
||||
},
|
||||
},
|
||||
};
|
||||
await updateMcpConfig(newConfig);
|
||||
} catch (error) {
|
||||
const currentConfig = await getMcpConfigFromFile();
|
||||
const serverConfig = currentConfig.mcpServers[clientId];
|
||||
|
||||
if (serverConfig) {
|
||||
serverConfig.status = "error";
|
||||
await updateMcpConfig(currentConfig);
|
||||
}
|
||||
|
||||
clientsMap.set(clientId, {
|
||||
client: null,
|
||||
tools: null,
|
||||
errorMsg: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
logger.error(`Failed to initialize client [${clientId}]: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to resume server [${clientId}]: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove MCP server
|
||||
export async function removeMcpServer(clientId: string) {
|
||||
try {
|
||||
const currentConfig = await getMcpConfigFromFile();
|
||||
const { [clientId]: _, ...rest } = currentConfig.mcpServers;
|
||||
const newConfig = {
|
||||
...currentConfig,
|
||||
mcpServers: rest,
|
||||
};
|
||||
await updateMcpConfig(newConfig);
|
||||
|
||||
const client = clientsMap.get(clientId);
|
||||
if (client?.client) {
|
||||
await removeClient(client.client);
|
||||
}
|
||||
clientsMap.delete(clientId);
|
||||
|
||||
return newConfig;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to remove server [${clientId}]: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Restart all clients
|
||||
export async function restartAllClients() {
|
||||
logger.info("Restarting all clients...");
|
||||
try {
|
||||
for (const client of clientsMap.values()) {
|
||||
if (client.client) {
|
||||
await removeClient(client.client);
|
||||
}
|
||||
}
|
||||
|
||||
clientsMap.clear();
|
||||
|
||||
const config = await getMcpConfigFromFile();
|
||||
for (const [clientId, serverConfig] of Object.entries(config.mcpServers)) {
|
||||
await initializeSingleClient(clientId, serverConfig);
|
||||
}
|
||||
return config;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to restart clients: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute MCP action
|
||||
export async function executeMcpAction(
|
||||
clientId: string,
|
||||
request: McpRequestMessage,
|
||||
) {
|
||||
try {
|
||||
const client = clientsMap.get(clientId);
|
||||
if (!client?.client) {
|
||||
throw new Error(`Client ${clientId} not found`);
|
||||
}
|
||||
logger.info(`Executing request for [${clientId}]`);
|
||||
return await executeRequest(client.client, request);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to execute request for [${clientId}]: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Get MCP config from file
|
||||
export async function getMcpConfigFromFile(): Promise<McpConfigData> {
|
||||
try {
|
||||
const configStr = await fs.readFile(CONFIG_PATH, "utf-8");
|
||||
return JSON.parse(configStr);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to load MCP config, using default config: ${error}`);
|
||||
return DEFAULT_MCP_CONFIG;
|
||||
}
|
||||
}
|
||||
|
||||
// Update MCP config file
|
||||
async function updateMcpConfig(config: McpConfigData): Promise<void> {
|
||||
try {
|
||||
await fs.mkdir(path.dirname(CONFIG_PATH), { recursive: true });
|
||||
await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
59
skills/openclaw-claude-code/src/mcp/client.ts
Normal file
59
skills/openclaw-claude-code/src/mcp/client.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* MCP Client - Creates and manages MCP client connections
|
||||
*/
|
||||
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
||||
import { MCPClientLogger } from "./logger";
|
||||
import { ListToolsResponse, McpRequestMessage, ServerConfig } from "./types";
|
||||
import { z } from "zod";
|
||||
|
||||
const logger = new MCPClientLogger();
|
||||
|
||||
export async function createClient(
|
||||
id: string,
|
||||
config: ServerConfig,
|
||||
): Promise<Client> {
|
||||
logger.info(`Creating client for ${id}...`);
|
||||
|
||||
const transport = new StdioClientTransport({
|
||||
command: config.command,
|
||||
args: config.args,
|
||||
env: {
|
||||
...Object.fromEntries(
|
||||
Object.entries(process.env)
|
||||
.filter(([_, v]) => v !== undefined)
|
||||
.map(([k, v]) => [k, v as string]),
|
||||
),
|
||||
...(config.env || {}),
|
||||
},
|
||||
});
|
||||
|
||||
const client = new Client(
|
||||
{
|
||||
name: `openclaw-mcp-client-${id}`,
|
||||
version: "1.0.0",
|
||||
},
|
||||
{
|
||||
capabilities: {},
|
||||
},
|
||||
);
|
||||
await client.connect(transport);
|
||||
return client;
|
||||
}
|
||||
|
||||
export async function removeClient(client: Client) {
|
||||
logger.info(`Removing client...`);
|
||||
await client.close();
|
||||
}
|
||||
|
||||
export async function listTools(client: Client): Promise<ListToolsResponse> {
|
||||
return client.listTools();
|
||||
}
|
||||
|
||||
export async function executeRequest(
|
||||
client: Client,
|
||||
request: McpRequestMessage,
|
||||
) {
|
||||
return client.request(request, z.any());
|
||||
}
|
||||
9
skills/openclaw-claude-code/src/mcp/index.ts
Normal file
9
skills/openclaw-claude-code/src/mcp/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* MCP Module - Model Context Protocol integration
|
||||
*/
|
||||
|
||||
export * from "./types";
|
||||
export * from "./logger";
|
||||
export * from "./client";
|
||||
export * from "./actions";
|
||||
export * from "./utils";
|
||||
60
skills/openclaw-claude-code/src/mcp/logger.ts
Normal file
60
skills/openclaw-claude-code/src/mcp/logger.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* MCP Client Logger with ANSI color support
|
||||
*/
|
||||
|
||||
const colors = {
|
||||
reset: "\x1b[0m",
|
||||
bright: "\x1b[1m",
|
||||
dim: "\x1b[2m",
|
||||
green: "\x1b[32m",
|
||||
yellow: "\x1b[33m",
|
||||
red: "\x1b[31m",
|
||||
blue: "\x1b[34m",
|
||||
};
|
||||
|
||||
export class MCPClientLogger {
|
||||
private readonly prefix: string;
|
||||
private readonly debugMode: boolean;
|
||||
|
||||
constructor(
|
||||
prefix: string = "OpenClaw MCP Client",
|
||||
debugMode: boolean = false,
|
||||
) {
|
||||
this.prefix = prefix;
|
||||
this.debugMode = debugMode;
|
||||
}
|
||||
|
||||
info(message: any) {
|
||||
this.print(colors.blue, message);
|
||||
}
|
||||
|
||||
success(message: any) {
|
||||
this.print(colors.green, message);
|
||||
}
|
||||
|
||||
error(message: any) {
|
||||
this.print(colors.red, message);
|
||||
}
|
||||
|
||||
warn(message: any) {
|
||||
this.print(colors.yellow, message);
|
||||
}
|
||||
|
||||
debug(message: any) {
|
||||
if (this.debugMode) {
|
||||
this.print(colors.dim, message);
|
||||
}
|
||||
}
|
||||
|
||||
private formatMessage(message: any): string {
|
||||
return typeof message === "object"
|
||||
? JSON.stringify(message, null, 2)
|
||||
: message;
|
||||
}
|
||||
|
||||
private print(color: string, message: any) {
|
||||
const formattedMessage = this.formatMessage(message);
|
||||
const logMessage = `${color}${colors.bright}[${this.prefix}]${colors.reset} ${formattedMessage}`;
|
||||
console.log(logMessage);
|
||||
}
|
||||
}
|
||||
162
skills/openclaw-claude-code/src/mcp/types.ts
Normal file
162
skills/openclaw-claude-code/src/mcp/types.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* MCP (Model Context Protocol) Type Definitions
|
||||
* Reference: https://spec.modelcontextprotocol.io/specification/basic/messages/
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
|
||||
// MCP Request Message
|
||||
export interface McpRequestMessage {
|
||||
jsonrpc?: "2.0";
|
||||
id?: string | number;
|
||||
method: "tools/call" | string;
|
||||
params?: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
export const McpRequestMessageSchema: z.ZodType<McpRequestMessage> = z.object({
|
||||
jsonrpc: z.literal("2.0").optional(),
|
||||
id: z.union([z.string(), z.number()]).optional(),
|
||||
method: z.string(),
|
||||
params: z.record(z.unknown()).optional(),
|
||||
});
|
||||
|
||||
// MCP Response Message
|
||||
export interface McpResponseMessage {
|
||||
jsonrpc?: "2.0";
|
||||
id?: string | number;
|
||||
result?: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
error?: {
|
||||
code: number;
|
||||
message: string;
|
||||
data?: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
export const McpResponseMessageSchema: z.ZodType<McpResponseMessage> = z.object(
|
||||
{
|
||||
jsonrpc: z.literal("2.0").optional(),
|
||||
id: z.union([z.string(), z.number()]).optional(),
|
||||
result: z.record(z.unknown()).optional(),
|
||||
error: z
|
||||
.object({
|
||||
code: z.number(),
|
||||
message: z.string(),
|
||||
data: z.unknown().optional(),
|
||||
})
|
||||
.optional(),
|
||||
},
|
||||
);
|
||||
|
||||
// MCP Notifications
|
||||
export interface McpNotifications {
|
||||
jsonrpc?: "2.0";
|
||||
method: string;
|
||||
params?: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
export const McpNotificationsSchema: z.ZodType<McpNotifications> = z.object({
|
||||
jsonrpc: z.literal("2.0").optional(),
|
||||
method: z.string(),
|
||||
params: z.record(z.unknown()).optional(),
|
||||
});
|
||||
|
||||
// List Tools Response
|
||||
export interface ListToolsResponse {
|
||||
tools: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
inputSchema?: object;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
// MCP Client Data States
|
||||
export type McpClientData =
|
||||
| McpActiveClient
|
||||
| McpErrorClient
|
||||
| McpInitializingClient;
|
||||
|
||||
interface McpInitializingClient {
|
||||
client: null;
|
||||
tools: null;
|
||||
errorMsg: null;
|
||||
}
|
||||
|
||||
interface McpActiveClient {
|
||||
client: Client;
|
||||
tools: ListToolsResponse;
|
||||
errorMsg: null;
|
||||
}
|
||||
|
||||
interface McpErrorClient {
|
||||
client: null;
|
||||
tools: null;
|
||||
errorMsg: string;
|
||||
}
|
||||
|
||||
// Server Status Types
|
||||
export type ServerStatus =
|
||||
| "undefined"
|
||||
| "active"
|
||||
| "paused"
|
||||
| "error"
|
||||
| "initializing";
|
||||
|
||||
export interface ServerStatusResponse {
|
||||
status: ServerStatus;
|
||||
errorMsg: string | null;
|
||||
}
|
||||
|
||||
// MCP Server Configuration
|
||||
export interface ServerConfig {
|
||||
command: string;
|
||||
args: string[];
|
||||
env?: Record<string, string>;
|
||||
status?: "active" | "paused" | "error";
|
||||
}
|
||||
|
||||
export interface McpConfigData {
|
||||
mcpServers: Record<string, ServerConfig>;
|
||||
}
|
||||
|
||||
export const DEFAULT_MCP_CONFIG: McpConfigData = {
|
||||
mcpServers: {},
|
||||
};
|
||||
|
||||
// Arguments Mapping for Preset Servers
|
||||
export interface ArgsMapping {
|
||||
type: "spread" | "single" | "env";
|
||||
position?: number;
|
||||
key?: string;
|
||||
}
|
||||
|
||||
// Preset Server Configuration
|
||||
export interface PresetServer {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
repo: string;
|
||||
tags: string[];
|
||||
command: string;
|
||||
baseArgs: string[];
|
||||
configurable: boolean;
|
||||
configSchema?: {
|
||||
properties: Record<
|
||||
string,
|
||||
{
|
||||
type: string;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
minItems?: number;
|
||||
}
|
||||
>;
|
||||
};
|
||||
argsMapping?: Record<string, ArgsMapping>;
|
||||
}
|
||||
21
skills/openclaw-claude-code/src/mcp/utils.ts
Normal file
21
skills/openclaw-claude-code/src/mcp/utils.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* MCP Utility Functions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Check if content contains MCP JSON format
|
||||
*/
|
||||
export function isMcpJson(content: string) {
|
||||
return content.match(/```json:mcp:([^{\s]+)([\s\S]*?)```/);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract MCP JSON from content
|
||||
*/
|
||||
export function extractMcpJson(content: string) {
|
||||
const match = content.match(/```json:mcp:([^{\s]+)([\s\S]*?)```/);
|
||||
if (match && match.length === 3) {
|
||||
return { clientId: match[1], mcp: JSON.parse(match[2]) };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
7
skills/openclaw-claude-code/src/store/index.ts
Normal file
7
skills/openclaw-claude-code/src/store/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Store Module - State management and persistence
|
||||
*/
|
||||
|
||||
export * from "./persist";
|
||||
export * from "./indexeddb-storage";
|
||||
export * from "./sync";
|
||||
71
skills/openclaw-claude-code/src/store/indexeddb-storage.ts
Normal file
71
skills/openclaw-claude-code/src/store/indexeddb-storage.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* IndexedDB Storage - Persistent storage with localStorage fallback
|
||||
*/
|
||||
|
||||
import { StateStorage } from "zustand/middleware";
|
||||
import { get, set, del, clear } from "idb-keyval";
|
||||
|
||||
/**
|
||||
* Safe localStorage wrapper that handles SSR
|
||||
*/
|
||||
function safeLocalStorage(): Storage {
|
||||
if (typeof window !== "undefined" && window.localStorage) {
|
||||
return window.localStorage;
|
||||
}
|
||||
// Return a mock storage for SSR
|
||||
return {
|
||||
length: 0,
|
||||
clear: () => {},
|
||||
getItem: () => null,
|
||||
key: () => null,
|
||||
removeItem: () => {},
|
||||
setItem: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
const localStorage = safeLocalStorage();
|
||||
|
||||
/**
|
||||
* IndexedDB Storage implementation with localStorage fallback
|
||||
*/
|
||||
class IndexedDBStorage implements StateStorage {
|
||||
public async getItem(name: string): Promise<string | null> {
|
||||
try {
|
||||
const value = (await get(name)) || localStorage.getItem(name);
|
||||
return value;
|
||||
} catch (error) {
|
||||
return localStorage.getItem(name);
|
||||
}
|
||||
}
|
||||
|
||||
public async setItem(name: string, value: string): Promise<void> {
|
||||
try {
|
||||
const _value = JSON.parse(value);
|
||||
if (!_value?.state?._hasHydrated) {
|
||||
console.warn("skip setItem", name);
|
||||
return;
|
||||
}
|
||||
await set(name, value);
|
||||
} catch (error) {
|
||||
localStorage.setItem(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
public async removeItem(name: string): Promise<void> {
|
||||
try {
|
||||
await del(name);
|
||||
} catch (error) {
|
||||
localStorage.removeItem(name);
|
||||
}
|
||||
}
|
||||
|
||||
public async clear(): Promise<void> {
|
||||
try {
|
||||
await clear();
|
||||
} catch (error) {
|
||||
localStorage.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const indexedDBStorage = new IndexedDBStorage();
|
||||
93
skills/openclaw-claude-code/src/store/persist.ts
Normal file
93
skills/openclaw-claude-code/src/store/persist.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Persistent Store Factory - Creates Zustand stores with persistence
|
||||
*/
|
||||
|
||||
import { create } from "zustand";
|
||||
import { combine, persist, createJSONStorage, StateStorage } from "zustand/middleware";
|
||||
|
||||
type SecondParam<T> = T extends (
|
||||
_f: infer _F,
|
||||
_s: infer S,
|
||||
...args: infer _U
|
||||
) => any
|
||||
? S
|
||||
: never;
|
||||
|
||||
type MakeUpdater<T> = {
|
||||
lastUpdateTime: number;
|
||||
_hasHydrated: boolean;
|
||||
|
||||
markUpdate: () => void;
|
||||
update: (updater: (state: T) => void) => void;
|
||||
setHasHydrated: (state: boolean) => void;
|
||||
};
|
||||
|
||||
type SetStoreState<T> = (
|
||||
partial: T | Partial<T> | ((state: T) => T | Partial<T>),
|
||||
replace?: boolean | undefined,
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Deep clone utility
|
||||
*/
|
||||
function deepClone<T>(obj: T): T {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a persistent store with automatic hydration tracking
|
||||
*/
|
||||
export function createPersistStore<T extends object, M>(
|
||||
state: T,
|
||||
methods: (
|
||||
set: SetStoreState<T & MakeUpdater<T>>,
|
||||
get: () => T & MakeUpdater<T>,
|
||||
) => M,
|
||||
persistOptions: SecondParam<typeof persist<T & M & MakeUpdater<T>>>,
|
||||
storage?: StateStorage,
|
||||
) {
|
||||
if (storage) {
|
||||
persistOptions.storage = createJSONStorage(() => storage);
|
||||
}
|
||||
|
||||
const oldOnRehydrateStorage = persistOptions?.onRehydrateStorage;
|
||||
persistOptions.onRehydrateStorage = (state) => {
|
||||
oldOnRehydrateStorage?.(state);
|
||||
return () => state.setHasHydrated(true);
|
||||
};
|
||||
|
||||
return create(
|
||||
persist(
|
||||
combine(
|
||||
{
|
||||
...state,
|
||||
lastUpdateTime: 0,
|
||||
_hasHydrated: false,
|
||||
},
|
||||
(set, get) => {
|
||||
return {
|
||||
...methods(set, get as any),
|
||||
|
||||
markUpdate() {
|
||||
set({ lastUpdateTime: Date.now() } as Partial<
|
||||
T & M & MakeUpdater<T>
|
||||
>);
|
||||
},
|
||||
update(updater: (state: T) => void) {
|
||||
const state = deepClone(get());
|
||||
updater(state);
|
||||
set({
|
||||
...state,
|
||||
lastUpdateTime: Date.now(),
|
||||
});
|
||||
},
|
||||
setHasHydrated: (state: boolean) => {
|
||||
set({ _hasHydrated: state } as Partial<T & M & MakeUpdater<T>>);
|
||||
},
|
||||
} as M & MakeUpdater<T>;
|
||||
},
|
||||
),
|
||||
persistOptions as any,
|
||||
),
|
||||
);
|
||||
}
|
||||
128
skills/openclaw-claude-code/src/store/sync.ts
Normal file
128
skills/openclaw-claude-code/src/store/sync.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* State Sync Utilities - Merge local and remote states
|
||||
*/
|
||||
|
||||
/**
|
||||
* Deep merge utility
|
||||
*/
|
||||
export function merge<T extends object>(target: T, source: Partial<T>): T {
|
||||
for (const key of Object.keys(source) as (keyof T)[]) {
|
||||
const sourceValue = source[key];
|
||||
const targetValue = target[key];
|
||||
|
||||
if (
|
||||
sourceValue !== undefined &&
|
||||
typeof sourceValue === "object" &&
|
||||
sourceValue !== null &&
|
||||
!Array.isArray(sourceValue)
|
||||
) {
|
||||
if (typeof targetValue !== "object" || targetValue === null) {
|
||||
(target as any)[key] = {};
|
||||
}
|
||||
merge(target[key] as object, sourceValue as object);
|
||||
} else if (sourceValue !== undefined) {
|
||||
(target as any)[key] = sourceValue;
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get non-function fields from an object
|
||||
*/
|
||||
export function getNonFunctionFields<T extends object>(obj: T) {
|
||||
const ret: any = {};
|
||||
|
||||
Object.entries(obj).map(([k, v]) => {
|
||||
if (typeof v !== "function") {
|
||||
ret[k] = v;
|
||||
}
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge state with lastUpdateTime - older state will be overridden
|
||||
*/
|
||||
export function mergeWithUpdate<T extends { lastUpdateTime?: number }>(
|
||||
localState: T,
|
||||
remoteState: T,
|
||||
): T {
|
||||
const localUpdateTime = localState.lastUpdateTime ?? 0;
|
||||
const remoteUpdateTime = remoteState.lastUpdateTime ?? 1;
|
||||
|
||||
if (localUpdateTime < remoteUpdateTime) {
|
||||
merge(remoteState, localState);
|
||||
return { ...remoteState };
|
||||
} else {
|
||||
merge(localState, remoteState);
|
||||
return { ...localState };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Session-like data structure for chat history
|
||||
*/
|
||||
export interface Session {
|
||||
id: string;
|
||||
messages: Array<{ id: string; date: string | Date; [key: string]: any }>;
|
||||
lastUpdate: string | Date;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge sessions - combines messages from local and remote
|
||||
*/
|
||||
export function mergeSessions(
|
||||
localSessions: Session[],
|
||||
remoteSessions: Session[],
|
||||
): Session[] {
|
||||
const localSessionMap: Record<string, Session> = {};
|
||||
localSessions.forEach((s) => (localSessionMap[s.id] = s));
|
||||
|
||||
remoteSessions.forEach((remoteSession) => {
|
||||
// Skip empty chats
|
||||
if (remoteSession.messages.length === 0) return;
|
||||
|
||||
const localSession = localSessionMap[remoteSession.id];
|
||||
if (!localSession) {
|
||||
// Remote session is new, add it
|
||||
localSessions.push(remoteSession);
|
||||
} else {
|
||||
// Both have same session id, merge messages
|
||||
const localMessageIds = new Set(localSession.messages.map((v) => v.id));
|
||||
remoteSession.messages.forEach((m) => {
|
||||
if (!localMessageIds.has(m.id)) {
|
||||
localSession.messages.push(m);
|
||||
}
|
||||
});
|
||||
|
||||
// Sort messages by date ascending
|
||||
localSession.messages.sort(
|
||||
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Sort sessions by lastUpdate descending
|
||||
localSessions.sort(
|
||||
(a, b) =>
|
||||
new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime(),
|
||||
);
|
||||
|
||||
return localSessions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge key-value stores - remote values first, then local overrides
|
||||
*/
|
||||
export function mergeKeyValueStore<T extends Record<string, any>>(
|
||||
localStore: T,
|
||||
remoteStore: T,
|
||||
): T {
|
||||
return {
|
||||
...remoteStore,
|
||||
...localStore,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user