feat(loop): support external CLI tools (cli-wrapper) in task management

- Fix missing i18n translations: loop.add, loop.save, loop.cancel
- Replace hardcoded validTools with dynamic tool loading from cli-tools.json
- Support external CLI wrappers (like doubao) in task creation and updates
- Add getEnabledToolsList() helper to fetch enabled tools dynamically
- Update mapIssueToolToLoopTool() to accept any string tool name
- Update validateTool() to use dynamic tool list
- Change LoopTask.tool type from specific strings to string (accepts any tool)

This allows tasks to use any enabled CLI tool from configuration,
including builtin tools, cli-wrappers, and api-endpoints, not just
the hardcoded ['bash', 'gemini', 'codex', 'qwen', 'claude'].
This commit is contained in:
catlog22
2026-01-22 12:43:37 +08:00
parent 21fbdbc55e
commit 5fa7524ad7
3 changed files with 35 additions and 8 deletions

View File

@@ -29,11 +29,13 @@
import { join } from 'path';
import { randomBytes } from 'crypto';
import * as os from 'os';
import type { RouteContext } from './types.js';
import { LoopStatus } from '../../types/loop.js';
import type { LoopState } from '../../types/loop.js';
import { TaskStorageManager, type TaskCreateRequest, type TaskUpdateRequest, type TaskReorderRequest } from '../../tools/loop-task-manager.js';
import { executeCliTool } from '../../tools/cli-executor.js';
import { loadClaudeCliTools } from '../../tools/claude-cli-tools.js';
/**
* V2 Loop Create Request
@@ -710,9 +712,17 @@ export async function handleLoopV2Routes(ctx: RouteContext): Promise<boolean> {
return { success: false, error: 'tool is required', status: 400 };
}
const validTools = ['bash', 'gemini', 'codex', 'qwen', 'claude'];
// Get enabled tools from cli-tools.json dynamically
const cliToolsConfig = loadClaudeCliTools(os.homedir());
const enabledTools = Object.entries(cliToolsConfig.tools || {})
.filter(([_, config]) => config.enabled === true)
.map(([name]) => name);
// Also allow 'bash' as a special case (built-in tool)
const validTools = ['bash', ...enabledTools];
if (!validTools.includes(tool)) {
return { success: false, error: `tool must be one of: ${validTools.join(', ')}`, status: 400 };
return { success: false, error: `tool must be one of enabled tools: ${validTools.join(', ')}`, status: 400 };
}
if (!mode || typeof mode !== 'string') {
@@ -1303,12 +1313,23 @@ function isValidId(id: string): boolean {
return true;
}
/**
* Get enabled tools list
*/
function getEnabledToolsList(): string[] {
const cliToolsConfig = loadClaudeCliTools(os.homedir());
const enabledTools = Object.entries(cliToolsConfig.tools || {})
.filter(([_, config]) => config.enabled === true)
.map(([name]) => name);
return ['bash', ...enabledTools];
}
/**
* Map issue tool to loop tool
*/
function mapIssueToolToLoopTool(tool: any): 'bash' | 'gemini' | 'codex' | 'qwen' | 'claude' | null {
const validTools = ['bash', 'gemini', 'codex', 'qwen', 'claude'];
if (validTools.includes(tool)) return tool as any;
function mapIssueToolToLoopTool(tool: any): string | null {
const validTools = getEnabledToolsList();
if (validTools.includes(tool)) return tool;
// Map aliases
if (tool === 'ccw') return 'gemini';
if (tool === 'ai') return 'gemini';
@@ -1343,7 +1364,7 @@ function mapIssueOnError(onError: any): 'continue' | 'pause' | 'fail_fast' | und
* Validate tool value
*/
function validateTool(tool: any): boolean {
const validTools = ['bash', 'gemini', 'codex', 'qwen', 'claude'];
const validTools = getEnabledToolsList();
return validTools.includes(tool);
}

View File

@@ -2202,6 +2202,9 @@ const i18n = {
'loop.kanban.noBoardData': 'No tasks to display',
'loop.listView': 'List View',
'loop.addTask': 'Add Task',
'loop.add': 'Add',
'loop.save': 'Save',
'loop.cancel': 'Cancel',
// Navigation & Grouping
'loop.nav.groupBy': 'Group By',
@@ -4796,6 +4799,9 @@ const i18n = {
'loop.kanban.noBoardData': '没有要显示的任务',
'loop.listView': '列表视图',
'loop.addTask': '添加任务',
'loop.add': '添加',
'loop.save': '保存',
'loop.cancel': '取消',
// Navigation & Grouping
'loop.nav.groupBy': '分组',

View File

@@ -22,8 +22,8 @@ export interface LoopTask {
/** Task description (what to do) */
description: string;
/** CLI tool to use */
tool: 'bash' | 'gemini' | 'codex' | 'qwen' | 'claude';
/** CLI tool to use (bash, builtin tools, cli-wrapper, api-endpoint) */
tool: string;
/** Execution mode */
mode: 'analysis' | 'write' | 'review';