mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-15 02:42:45 +08:00
feat: Add global relationships management to GlobalSymbolIndex
- Introduced a new schema version (v2) with a global_relationships table. - Implemented CRUD operations for file relationships, including update and delete functionalities. - Added query capabilities for relationships by target and symbols. - Created migration logic from v1 to v2 schema. - Enhanced tests for global relationships, covering various scenarios including insertion, querying, and deletion. docs: Add update-single command for generating module documentation - Created a new command to generate manual-style documentation (CLAUDE.md) for a single module. - Detailed execution process and implementation phases for the command. - Included usage examples and error handling guidelines. feat: Implement team command for CLI interface - Added a new team command for logging and retrieving messages in a team message bus. - Supported subcommands for logging, reading, listing, and checking status of messages. - Included error handling and JSON output options. test: Add comprehensive tests for global relationships - Developed extensive tests for the global_relationships table in GlobalSymbolIndex. - Covered schema creation, migration, CRUD operations, and performance benchmarks. - Ensured project isolation and validated query functionalities for relationships.
This commit is contained in:
@@ -142,12 +142,12 @@ export function ObservabilityPanel() {
|
||||
<label className="block text-xs font-medium text-muted-foreground mb-1">
|
||||
{formatMessage({ id: 'issues.observability.filters.type' })}
|
||||
</label>
|
||||
<Select value={type} onValueChange={(v) => setType(v)}>
|
||||
<Select value={type || '__all__'} onValueChange={(v) => setType(v === '__all__' ? '' : v)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={formatMessage({ id: 'issues.observability.filters.typeAll' })} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="">{formatMessage({ id: 'issues.observability.filters.typeAll' })}</SelectItem>
|
||||
<SelectItem value="__all__">{formatMessage({ id: 'issues.observability.filters.typeAll' })}</SelectItem>
|
||||
{EVENT_TYPES.map((t) => (
|
||||
<SelectItem key={t} value={t}>
|
||||
{t}
|
||||
|
||||
@@ -209,7 +209,7 @@ export function QueuePanel() {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(historyIndex.queues || []).length === 0 ? (
|
||||
<SelectItem value="" disabled>
|
||||
<SelectItem value="__none__" disabled>
|
||||
{formatMessage({ id: 'issues.queue.history.empty' })}
|
||||
</SelectItem>
|
||||
) : (
|
||||
|
||||
@@ -215,7 +215,7 @@ export function QueueExecuteInSession({ item, className }: { item: QueueItem; cl
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sessions.length === 0 ? (
|
||||
<SelectItem value="" disabled>
|
||||
<SelectItem value="__none__" disabled>
|
||||
{formatMessage({ id: 'issues.terminal.session.none' })}
|
||||
</SelectItem>
|
||||
) : (
|
||||
|
||||
@@ -267,7 +267,7 @@ export function QueueSendToOrchestrator({ item, className }: { item: QueueItem;
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sessions.length === 0 ? (
|
||||
<SelectItem value="" disabled>
|
||||
<SelectItem value="__none__" disabled>
|
||||
{formatMessage({ id: 'issues.terminal.session.none' })}
|
||||
</SelectItem>
|
||||
) : (
|
||||
|
||||
@@ -248,7 +248,7 @@ function ContextContent({
|
||||
<div className="space-y-0.5 pl-2 max-h-32 overflow-y-auto">
|
||||
{ctx.relevant_files.map((f, i) => {
|
||||
const filePath = typeof f === 'string' ? f : f.path;
|
||||
const reason = typeof f === 'string' ? undefined : (f.rationale || f.reason);
|
||||
const reason = typeof f === 'string' ? undefined : f.reason;
|
||||
return (
|
||||
<div key={i} className="group flex items-start gap-1 text-muted-foreground hover:bg-muted/30 rounded px-1 py-0.5">
|
||||
<span className="text-primary/50 shrink-0">{i + 1}.</span>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
Server,
|
||||
Plus,
|
||||
@@ -226,6 +226,7 @@ export function McpManagerPage() {
|
||||
const [saveTemplateDialogOpen, setSaveTemplateDialogOpen] = useState(false);
|
||||
const [serverToSaveAsTemplate, setServerToSaveAsTemplate] = useState<McpServer | undefined>(undefined);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const notifications = useNotifications();
|
||||
|
||||
const {
|
||||
@@ -352,15 +353,47 @@ export function McpManagerPage() {
|
||||
};
|
||||
|
||||
const handleToggleCcwTool = async (tool: string, enabled: boolean) => {
|
||||
// Read latest from cache to avoid stale closures
|
||||
const currentConfig = queryClient.getQueryData<CcwMcpConfig>(['ccwMcpConfig']) ?? ccwConfig;
|
||||
const currentTools = currentConfig.enabledTools;
|
||||
const previousConfig = queryClient.getQueryData<CcwMcpConfig>(['ccwMcpConfig']);
|
||||
|
||||
const updatedTools = enabled
|
||||
? [...ccwConfig.enabledTools, tool]
|
||||
: ccwConfig.enabledTools.filter((t) => t !== tool);
|
||||
await updateCcwConfig({ enabledTools: updatedTools });
|
||||
? (currentTools.includes(tool) ? currentTools : [...currentTools, tool])
|
||||
: currentTools.filter((t) => t !== tool);
|
||||
|
||||
// Optimistic cache update for immediate UI response
|
||||
queryClient.setQueryData(['ccwMcpConfig'], (old: CcwMcpConfig | undefined) => {
|
||||
if (!old) return old;
|
||||
return { ...old, enabledTools: updatedTools };
|
||||
});
|
||||
|
||||
try {
|
||||
await updateCcwConfig({ ...currentConfig, enabledTools: updatedTools });
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle CCW tool:', error);
|
||||
queryClient.setQueryData(['ccwMcpConfig'], previousConfig);
|
||||
}
|
||||
ccwMcpQuery.refetch();
|
||||
};
|
||||
|
||||
const handleUpdateCcwConfig = async (config: Partial<CcwMcpConfig>) => {
|
||||
await updateCcwConfig(config);
|
||||
// Read BEFORE optimistic update to capture actual server state
|
||||
const currentConfig = queryClient.getQueryData<CcwMcpConfig>(['ccwMcpConfig']) ?? ccwConfig;
|
||||
const previousConfig = queryClient.getQueryData<CcwMcpConfig>(['ccwMcpConfig']);
|
||||
|
||||
// Optimistic cache update for immediate UI response
|
||||
queryClient.setQueryData(['ccwMcpConfig'], (old: CcwMcpConfig | undefined) => {
|
||||
if (!old) return old;
|
||||
return { ...old, ...config };
|
||||
});
|
||||
|
||||
try {
|
||||
await updateCcwConfig({ ...currentConfig, ...config });
|
||||
} catch (error) {
|
||||
console.error('Failed to update CCW config:', error);
|
||||
queryClient.setQueryData(['ccwMcpConfig'], previousConfig);
|
||||
}
|
||||
ccwMcpQuery.refetch();
|
||||
};
|
||||
|
||||
@@ -378,15 +411,48 @@ export function McpManagerPage() {
|
||||
};
|
||||
|
||||
const handleToggleCcwToolCodex = async (tool: string, enabled: boolean) => {
|
||||
const currentConfig = queryClient.getQueryData<CcwMcpConfig>(['ccwMcpConfigCodex']) ?? ccwCodexConfig;
|
||||
const currentTools = currentConfig.enabledTools;
|
||||
|
||||
const updatedTools = enabled
|
||||
? [...ccwCodexConfig.enabledTools, tool]
|
||||
: ccwCodexConfig.enabledTools.filter((t) => t !== tool);
|
||||
await updateCcwConfigForCodex({ enabledTools: updatedTools });
|
||||
? [...currentTools, tool]
|
||||
: currentTools.filter((t) => t !== tool);
|
||||
|
||||
queryClient.setQueryData(['ccwMcpConfigCodex'], (old: CcwMcpConfig | undefined) => {
|
||||
if (!old) return old;
|
||||
return { ...old, enabledTools: updatedTools };
|
||||
});
|
||||
|
||||
try {
|
||||
await updateCcwConfigForCodex({
|
||||
enabledTools: updatedTools,
|
||||
projectRoot: currentConfig.projectRoot,
|
||||
allowedDirs: currentConfig.allowedDirs,
|
||||
disableSandbox: currentConfig.disableSandbox,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle CCW tool (Codex):', error);
|
||||
}
|
||||
ccwMcpCodexQuery.refetch();
|
||||
};
|
||||
|
||||
const handleUpdateCcwConfigCodex = async (config: Partial<CcwMcpConfig>) => {
|
||||
await updateCcwConfigForCodex(config);
|
||||
queryClient.setQueryData(['ccwMcpConfigCodex'], (old: CcwMcpConfig | undefined) => {
|
||||
if (!old) return old;
|
||||
return { ...old, ...config };
|
||||
});
|
||||
|
||||
try {
|
||||
const currentConfig = queryClient.getQueryData<CcwMcpConfig>(['ccwMcpConfigCodex']) ?? ccwCodexConfig;
|
||||
await updateCcwConfigForCodex({
|
||||
enabledTools: config.enabledTools ?? currentConfig.enabledTools,
|
||||
projectRoot: config.projectRoot ?? currentConfig.projectRoot,
|
||||
allowedDirs: config.allowedDirs ?? currentConfig.allowedDirs,
|
||||
disableSandbox: config.disableSandbox ?? currentConfig.disableSandbox,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to update CCW config (Codex):', error);
|
||||
}
|
||||
ccwMcpCodexQuery.refetch();
|
||||
};
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import { hookCommand } from './commands/hook.js';
|
||||
import { issueCommand } from './commands/issue.js';
|
||||
import { workflowCommand } from './commands/workflow.js';
|
||||
import { loopCommand } from './commands/loop.js';
|
||||
import { teamCommand } from './commands/team.js';
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
@@ -318,6 +319,22 @@ export function run(argv: string[]): void {
|
||||
.option('--session <name>', 'Specify workflow session')
|
||||
.action((subcommand, args, options) => loopCommand(subcommand, args, options));
|
||||
|
||||
// Team command - Team Message Bus CLI interface
|
||||
program
|
||||
.command('team [subcommand] [args...]')
|
||||
.description('Team message bus for Agent Team communication')
|
||||
.option('--team <name>', 'Team name')
|
||||
.option('--from <role>', 'Sender role name')
|
||||
.option('--to <role>', 'Recipient role name')
|
||||
.option('--type <type>', 'Message type')
|
||||
.option('--summary <text>', 'One-line summary')
|
||||
.option('--ref <path>', 'File path reference')
|
||||
.option('--data <json>', 'JSON structured data')
|
||||
.option('--id <id>', 'Message ID (for read)')
|
||||
.option('--last <n>', 'Last N messages (for list)')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action((subcommand, args, options) => teamCommand(subcommand, args, options));
|
||||
|
||||
// Workflow command - Workflow installation and management
|
||||
program
|
||||
.command('workflow [subcommand] [args...]')
|
||||
|
||||
179
ccw/src/commands/team.ts
Normal file
179
ccw/src/commands/team.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* Team Command - CLI interface for Team Message Bus
|
||||
* Delegates to team-msg.ts handler for JSONL-based persistent messaging
|
||||
*
|
||||
* Commands:
|
||||
* ccw team log --team <name> --from <role> --to <role> --type <type> --summary "..."
|
||||
* ccw team read --team <name> --id <MSG-NNN>
|
||||
* ccw team list --team <name> [--from <role>] [--to <role>] [--type <type>] [--last <n>]
|
||||
* ccw team status --team <name>
|
||||
* ccw team delete --team <name> --id <MSG-NNN>
|
||||
* ccw team clear --team <name>
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { handler } from '../tools/team-msg.js';
|
||||
|
||||
interface TeamOptions {
|
||||
team?: string;
|
||||
from?: string;
|
||||
to?: string;
|
||||
type?: string;
|
||||
summary?: string;
|
||||
ref?: string;
|
||||
data?: string;
|
||||
id?: string;
|
||||
last?: string;
|
||||
json?: boolean;
|
||||
}
|
||||
|
||||
export async function teamCommand(
|
||||
subcommand: string,
|
||||
args: string | string[],
|
||||
options: TeamOptions
|
||||
): Promise<void> {
|
||||
if (!subcommand) {
|
||||
printHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!options.team) {
|
||||
console.error(chalk.red('Error: --team is required'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Build params for handler
|
||||
const params: Record<string, unknown> = {
|
||||
operation: subcommand,
|
||||
team: options.team,
|
||||
};
|
||||
|
||||
if (options.from) params.from = options.from;
|
||||
if (options.to) params.to = options.to;
|
||||
if (options.type) params.type = options.type;
|
||||
if (options.summary) params.summary = options.summary;
|
||||
if (options.ref) params.ref = options.ref;
|
||||
if (options.id) params.id = options.id;
|
||||
if (options.last) params.last = parseInt(options.last, 10);
|
||||
|
||||
// Parse --data as JSON
|
||||
if (options.data) {
|
||||
try {
|
||||
params.data = JSON.parse(options.data);
|
||||
} catch {
|
||||
console.error(chalk.red('Error: --data must be valid JSON'));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await handler(params);
|
||||
|
||||
if (!result.success) {
|
||||
console.error(chalk.red(`Error: ${result.error}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// JSON output mode
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(result.result, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
// Formatted output by operation
|
||||
switch (subcommand) {
|
||||
case 'log': {
|
||||
const r = result.result as { id: string; message: string };
|
||||
console.log(chalk.green(`✓ ${r.message}`));
|
||||
break;
|
||||
}
|
||||
case 'read': {
|
||||
const msg = result.result as { id: string; ts: string; from: string; to: string; type: string; summary: string; ref?: string; data?: unknown };
|
||||
console.log(chalk.bold(`${msg.id} [${msg.ts}]`));
|
||||
console.log(` ${chalk.cyan(msg.from)} → ${chalk.yellow(msg.to)} (${msg.type})`);
|
||||
console.log(` ${msg.summary}`);
|
||||
if (msg.ref) console.log(chalk.gray(` ref: ${msg.ref}`));
|
||||
if (msg.data) console.log(chalk.gray(` data: ${JSON.stringify(msg.data)}`));
|
||||
break;
|
||||
}
|
||||
case 'list': {
|
||||
const r = result.result as { formatted: string; total: number; showing: number };
|
||||
console.log(chalk.gray(`Showing ${r.showing} of ${r.total} messages\n`));
|
||||
console.log(r.formatted);
|
||||
break;
|
||||
}
|
||||
case 'status': {
|
||||
const r = result.result as { formatted?: string; summary?: string; total_messages?: number };
|
||||
if (r.summary) {
|
||||
console.log(chalk.yellow(r.summary));
|
||||
} else {
|
||||
console.log(chalk.gray(`Total messages: ${r.total_messages}\n`));
|
||||
console.log(r.formatted);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
const r = result.result as { message: string };
|
||||
console.log(chalk.green(`✓ ${r.message}`));
|
||||
break;
|
||||
}
|
||||
case 'clear': {
|
||||
const r = result.result as { message: string };
|
||||
console.log(chalk.green(`✓ ${r.message}`));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
console.error(chalk.red(`Unknown subcommand: ${subcommand}`));
|
||||
printHelp();
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error: ${(error as Error).message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function printHelp(): void {
|
||||
console.log(chalk.bold.cyan('\n CCW Team Message Bus\n'));
|
||||
console.log(' CLI interface for team message logging and retrieval.\n');
|
||||
console.log(' Subcommands:');
|
||||
console.log(chalk.gray(' log Log a team message'));
|
||||
console.log(chalk.gray(' read Read a specific message by ID'));
|
||||
console.log(chalk.gray(' list List recent messages with filters'));
|
||||
console.log(chalk.gray(' status Show team member activity summary'));
|
||||
console.log(chalk.gray(' delete Delete a specific message by ID'));
|
||||
console.log(chalk.gray(' clear Clear all messages for a team'));
|
||||
console.log();
|
||||
console.log(' Required:');
|
||||
console.log(chalk.gray(' --team <name> Team name'));
|
||||
console.log();
|
||||
console.log(' Log Options:');
|
||||
console.log(chalk.gray(' --from <role> Sender role name'));
|
||||
console.log(chalk.gray(' --to <role> Recipient role name'));
|
||||
console.log(chalk.gray(' --type <type> Message type (plan_ready, impl_complete, etc.)'));
|
||||
console.log(chalk.gray(' --summary <text> One-line summary'));
|
||||
console.log(chalk.gray(' --ref <path> File path reference'));
|
||||
console.log(chalk.gray(' --data <json> JSON structured data'));
|
||||
console.log();
|
||||
console.log(' Read/Delete Options:');
|
||||
console.log(chalk.gray(' --id <MSG-NNN> Message ID'));
|
||||
console.log();
|
||||
console.log(' List Options:');
|
||||
console.log(chalk.gray(' --from <role> Filter by sender'));
|
||||
console.log(chalk.gray(' --to <role> Filter by recipient'));
|
||||
console.log(chalk.gray(' --type <type> Filter by message type'));
|
||||
console.log(chalk.gray(' --last <n> Number of messages (default: 20)'));
|
||||
console.log();
|
||||
console.log(' General:');
|
||||
console.log(chalk.gray(' --json Output as JSON'));
|
||||
console.log();
|
||||
console.log(' Examples:');
|
||||
console.log(chalk.gray(' ccw team log --team my-team --from executor --to coordinator --type impl_complete --summary "Task done"'));
|
||||
console.log(chalk.gray(' ccw team list --team my-team --last 5'));
|
||||
console.log(chalk.gray(' ccw team read --team my-team --id MSG-003'));
|
||||
console.log(chalk.gray(' ccw team status --team my-team'));
|
||||
console.log(chalk.gray(' ccw team delete --team my-team --id MSG-003'));
|
||||
console.log(chalk.gray(' ccw team clear --team my-team'));
|
||||
console.log(chalk.gray(' ccw team log --team my-team --from planner --to coordinator --type plan_ready --summary "Plan ready" --json'));
|
||||
console.log();
|
||||
}
|
||||
@@ -6,11 +6,13 @@
|
||||
* - read: Read message(s) by ID
|
||||
* - list: List recent messages with optional filters (from/to/type/last N)
|
||||
* - status: Summarize team member activity from message history
|
||||
* - delete: Delete a specific message by ID
|
||||
* - clear: Clear all messages for a team
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { ToolSchema, ToolResult } from '../types/tool.js';
|
||||
import { existsSync, mkdirSync, readFileSync, appendFileSync } from 'fs';
|
||||
import { existsSync, mkdirSync, readFileSync, appendFileSync, writeFileSync, rmSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { getProjectRoot } from '../utils/path-validator.js';
|
||||
|
||||
@@ -37,7 +39,7 @@ export interface StatusEntry {
|
||||
// --- Zod Schema ---
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
operation: z.enum(['log', 'read', 'list', 'status']).describe('Operation to perform'),
|
||||
operation: z.enum(['log', 'read', 'list', 'status', 'delete', 'clear']).describe('Operation to perform'),
|
||||
team: z.string().describe('Team name (maps to .workflow/.team-msg/{team}/messages.jsonl)'),
|
||||
|
||||
// log params
|
||||
@@ -69,6 +71,8 @@ Operations:
|
||||
team_msg(operation="list", team="my-team")
|
||||
team_msg(operation="list", team="my-team", from="tester", last=5)
|
||||
team_msg(operation="status", team="my-team")
|
||||
team_msg(operation="delete", team="my-team", id="MSG-003")
|
||||
team_msg(operation="clear", team="my-team")
|
||||
|
||||
Message types: plan_ready, plan_approved, plan_revision, task_unblocked, impl_complete, impl_progress, test_result, review_result, fix_required, error, shutdown`,
|
||||
inputSchema: {
|
||||
@@ -76,8 +80,8 @@ Message types: plan_ready, plan_approved, plan_revision, task_unblocked, impl_co
|
||||
properties: {
|
||||
operation: {
|
||||
type: 'string',
|
||||
enum: ['log', 'read', 'list', 'status'],
|
||||
description: 'Operation: log | read | list | status',
|
||||
enum: ['log', 'read', 'list', 'status', 'delete', 'clear'],
|
||||
description: 'Operation: log | read | list | status | delete | clear',
|
||||
},
|
||||
team: {
|
||||
type: 'string',
|
||||
@@ -250,6 +254,37 @@ function opStatus(params: Params): ToolResult {
|
||||
};
|
||||
}
|
||||
|
||||
function opDelete(params: Params): ToolResult {
|
||||
if (!params.id) return { success: false, error: 'delete requires "id"' };
|
||||
|
||||
const messages = readAllMessages(params.team);
|
||||
const idx = messages.findIndex(m => m.id === params.id);
|
||||
|
||||
if (idx === -1) {
|
||||
return { success: false, error: `Message ${params.id} not found in team "${params.team}"` };
|
||||
}
|
||||
|
||||
const removed = messages.splice(idx, 1)[0];
|
||||
const logPath = ensureLogFile(params.team);
|
||||
writeFileSync(logPath, messages.map(m => JSON.stringify(m)).join('\n') + (messages.length > 0 ? '\n' : ''), 'utf-8');
|
||||
|
||||
return { success: true, result: { deleted: removed.id, message: `Deleted ${removed.id}: [${removed.from} → ${removed.to}] ${removed.summary}` } };
|
||||
}
|
||||
|
||||
function opClear(params: Params): ToolResult {
|
||||
const logPath = getLogPath(params.team);
|
||||
const dir = getLogDir(params.team);
|
||||
|
||||
if (!existsSync(logPath)) {
|
||||
return { success: true, result: { message: `Team "${params.team}" has no messages to clear.` } };
|
||||
}
|
||||
|
||||
const count = readAllMessages(params.team).length;
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
|
||||
return { success: true, result: { cleared: count, message: `Cleared ${count} messages for team "${params.team}".` } };
|
||||
}
|
||||
|
||||
// --- Handler ---
|
||||
|
||||
export async function handler(params: Record<string, unknown>): Promise<ToolResult> {
|
||||
@@ -265,6 +300,8 @@ export async function handler(params: Record<string, unknown>): Promise<ToolResu
|
||||
case 'read': return opRead(p);
|
||||
case 'list': return opList(p);
|
||||
case 'status': return opStatus(p);
|
||||
case 'delete': return opDelete(p);
|
||||
case 'clear': return opClear(p);
|
||||
default:
|
||||
return { success: false, error: `Unknown operation: ${p.operation}` };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user