chore: batch update - cleanup ghost commands, ccw-help index refresh, CLI session/orchestrator enhancements, skill minor fixes

- Add cleanup-ghost-commands.mjs script
- Refresh ccw-help index files (remove stale entries)
- CLI session manager: add instruction assembler and launch registry
- Frontend: orchestrator plan builder, property panel, dashboard toolbar updates
- Flow executor and type updates
- Minor fixes across multiple skills and commands
This commit is contained in:
catlog22
2026-02-17 21:53:51 +08:00
parent 1f53f2de27
commit 357f48a0c3
45 changed files with 751 additions and 1643 deletions

View File

@@ -18,6 +18,7 @@
import type { RouteContext } from './types.js';
import { getCliSessionManager } from '../services/cli-session-manager.js';
import type { InstructionType } from '../services/cli-instruction-assembler.js';
import path from 'path';
import { getCliSessionPolicy } from '../services/cli-session-policy.js';
import { RateLimiter } from '../services/rate-limiter.js';
@@ -91,7 +92,8 @@ export async function handleCliSessionsRoutes(ctx: RouteContext): Promise<boolea
preferredShell,
tool,
model,
resumeKey
resumeKey,
launchMode
} = (body || {}) as any;
if (tool && typeof tool === 'string') {
@@ -115,7 +117,8 @@ export async function handleCliSessionsRoutes(ctx: RouteContext): Promise<boolea
preferredShell: preferredShell === 'pwsh' ? 'pwsh' : 'bash',
tool: typeof tool === 'string' ? tool.trim() : undefined,
model,
resumeKey
resumeKey,
launchMode: launchMode === 'yolo' ? 'yolo' : 'default',
});
appendCliSessionAudit({
@@ -353,7 +356,9 @@ export async function handleCliSessionsRoutes(ctx: RouteContext): Promise<boolea
workingDir,
category,
resumeKey,
resumeStrategy
resumeStrategy,
instructionType,
skillName
} = (body || {}) as any;
if (!tool || typeof tool !== 'string') {
@@ -380,7 +385,9 @@ export async function handleCliSessionsRoutes(ctx: RouteContext): Promise<boolea
workingDir,
category,
resumeKey,
resumeStrategy: resumeStrategy === 'promptConcat' ? 'promptConcat' : 'nativeResume'
resumeStrategy: resumeStrategy === 'promptConcat' ? 'promptConcat' : 'nativeResume',
instructionType: typeof instructionType === 'string' ? instructionType as InstructionType : undefined,
skillName: typeof skillName === 'string' ? skillName : undefined,
});
appendCliSessionAudit({

View File

@@ -0,0 +1,37 @@
// ========================================
// CLI Instruction Assembler
// ========================================
// Assembles the final sendText string based on CLI type and instruction type.
export type InstructionType = 'prompt' | 'skill' | 'command';
/**
* Assemble the text to send to a CLI interactive session.
*
* - prompt → raw content text
* - skill → CLI-specific skill prefix (claude: /, codex: $, others: fallback to prompt)
* - command → raw content text (CLI native command)
*/
export function assembleInstruction(
cliTool: string,
instructionType: InstructionType,
content: string,
skillName?: string,
): string {
if (instructionType === 'prompt' || instructionType === 'command') {
return content;
}
// instructionType === 'skill'
const name = skillName ?? '';
switch (cliTool) {
case 'claude':
return `/${name} ${content}`;
case 'codex':
return `$${name} ${content}`;
default:
// Other CLIs don't support skill syntax — fallback to plain prompt
return content;
}
}

View File

@@ -0,0 +1,53 @@
// ========================================
// CLI Launch Registry
// ========================================
// Defines interactive-mode launch parameters for each CLI tool.
// Supports 'default' and 'yolo' launch modes.
export type CliTool = 'claude' | 'gemini' | 'qwen' | 'codex' | 'opencode';
export type LaunchMode = 'default' | 'yolo';
export interface CliLaunchConfig {
command: string;
args: string[];
env?: Record<string, string>;
}
function parseCommand(raw: string): CliLaunchConfig {
const parts = raw.split(/\s+/);
return { command: parts[0], args: parts.slice(1) };
}
const LAUNCH_CONFIGS: Record<CliTool, Record<LaunchMode, CliLaunchConfig>> = {
claude: {
default: parseCommand('claude'),
yolo: parseCommand('claude --permission-mode bypassPermissions'),
},
gemini: {
default: parseCommand('gemini'),
yolo: parseCommand('gemini --approval-mode yolo'),
},
qwen: {
default: parseCommand('qwen'),
yolo: parseCommand('qwen --approval-mode yolo'),
},
codex: {
default: parseCommand('codex'),
yolo: parseCommand('codex --full-auto'),
},
opencode: {
default: parseCommand('opencode'),
yolo: parseCommand('opencode'),
},
};
const KNOWN_TOOLS = new Set<string>(Object.keys(LAUNCH_CONFIGS));
export function getLaunchConfig(tool: string, launchMode: LaunchMode): CliLaunchConfig {
if (KNOWN_TOOLS.has(tool)) {
return LAUNCH_CONFIGS[tool as CliTool][launchMode];
}
// Unknown tool: treat the tool name itself as the command
return { command: tool, args: [] };
}

View File

@@ -13,6 +13,8 @@ import {
} from './cli-session-command-builder.js';
import { getCliSessionPolicy } from './cli-session-policy.js';
import { appendCliSessionAudit } from './cli-session-audit.js';
import { getLaunchConfig } from './cli-launch-registry.js';
import { assembleInstruction, type InstructionType } from './cli-instruction-assembler.js';
export interface CliSession {
sessionKey: string;
@@ -24,6 +26,8 @@ export interface CliSession {
createdAt: string;
updatedAt: string;
isPaused: boolean;
/** When set, this session is a native CLI interactive process (not a shell). */
cliTool?: string;
}
export interface CreateCliSessionOptions {
@@ -34,6 +38,8 @@ export interface CreateCliSessionOptions {
tool?: string;
model?: string;
resumeKey?: string;
/** Launch mode for native CLI sessions. */
launchMode?: 'default' | 'yolo';
}
export interface ExecuteInCliSessionOptions {
@@ -45,6 +51,10 @@ export interface ExecuteInCliSessionOptions {
category?: 'user' | 'internal' | 'insight';
resumeKey?: string;
resumeStrategy?: CliSessionResumeStrategy;
/** Instruction type for native CLI sessions. */
instructionType?: InstructionType;
/** Skill name for instructionType='skill'. */
skillName?: string;
}
export interface CliSessionOutputEvent {
@@ -202,12 +212,31 @@ export class CliSessionManager {
createSession(options: CreateCliSessionOptions): CliSession {
const workingDir = normalizeWorkingDir(options.workingDir);
const preferredShell = options.preferredShell ?? 'bash';
const { shellKind, file, args } = pickShell(preferredShell);
const sessionKey = createSessionKey();
const createdAt = nowIso();
let shellKind: CliSessionShellKind;
let file: string;
let args: string[];
let cliTool: string | undefined;
if (options.tool) {
// Native CLI interactive session: spawn the CLI process directly
const launchMode = options.launchMode ?? 'default';
const config = getLaunchConfig(options.tool, launchMode);
shellKind = 'git-bash'; // PTY shell kind label (not actually a shell)
file = config.command;
args = config.args;
cliTool = options.tool;
} else {
// Legacy shell session: spawn bash/pwsh
const preferredShell = options.preferredShell ?? 'bash';
const picked = pickShell(preferredShell);
shellKind = picked.shellKind;
file = picked.file;
args = picked.args;
}
const pty = nodePty.spawn(file, args, {
name: 'xterm-256color',
cols: options.cols ?? 120,
@@ -230,6 +259,7 @@ export class CliSessionManager {
bufferBytes: 0,
lastActivityAt: Date.now(),
isPaused: false,
cliTool,
};
pty.onData((data) => {
@@ -272,7 +302,8 @@ export class CliSessionManager {
this.sessions.set(sessionKey, session);
// WSL often ignores Windows cwd; best-effort cd to mounted path.
if (shellKind === 'wsl-bash') {
// Only for legacy shell sessions, not native CLI sessions.
if (!cliTool && shellKind === 'wsl-bash') {
const wslCwd = toWslPath(workingDir.replace(/\\/g, '/'));
this.sendText(sessionKey, `cd ${wslCwd}`, true);
}
@@ -388,27 +419,37 @@ export class CliSessionManager {
? `${resumeKey}-${Date.now()}`
: `exec-${Date.now()}-${randomBytes(3).toString('hex')}`;
const { command } = buildCliSessionExecuteCommand({
projectRoot: this.projectRoot,
shellKind: session.shellKind,
tool: options.tool,
prompt: options.prompt,
mode: options.mode,
model: options.model,
workingDir: options.workingDir ?? session.workingDir,
category: options.category,
resumeStrategy: options.resumeStrategy,
prevExecutionId,
executionId
});
let command: string;
if (session.cliTool) {
// Native CLI session: assemble instruction and sendText directly
const instructionType = options.instructionType ?? 'prompt';
command = assembleInstruction(session.cliTool, instructionType, options.prompt, options.skillName);
this.sendText(sessionKey, command, true);
} else {
// Legacy shell session: build ccw cli pipe command
const result = buildCliSessionExecuteCommand({
projectRoot: this.projectRoot,
shellKind: session.shellKind,
tool: options.tool,
prompt: options.prompt,
mode: options.mode,
model: options.model,
workingDir: options.workingDir ?? session.workingDir,
category: options.category,
resumeStrategy: options.resumeStrategy,
prevExecutionId,
executionId
});
command = result.command;
this.sendText(sessionKey, command, true);
}
// Best-effort: preemptively update mapping so subsequent queue items can chain.
if (resumeMapKey) {
this.resumeKeyLastExecution.set(resumeMapKey, executionId);
}
this.sendText(sessionKey, command, true);
broadcastToClients({
type: 'CLI_SESSION_EXECUTE',
payload: { sessionKey, executionId, command, timestamp: nowIso() }

View File

@@ -21,6 +21,7 @@ import { broadcastToClients } from '../websocket.js';
import { executeCliTool } from '../../tools/cli-executor-core.js';
import { cliSessionMux } from './cli-session-mux.js';
import { appendCliSessionAudit } from './cli-session-audit.js';
import { assembleInstruction, type InstructionType } from './cli-instruction-assembler.js';
import type {
Flow,
FlowNode,
@@ -264,12 +265,30 @@ export class NodeRunner {
error: `Target session not found: ${targetSessionKey}`
};
}
// Resolve instructionType and skillName for native CLI sessions
let instructionType = data.instructionType;
let skillName = data.skillName;
if (!instructionType) {
if (skillName) {
instructionType = 'skill';
} else if (data.slashCommand) {
// Backward compat: map slashCommand to skill
instructionType = 'skill';
skillName = data.slashCommand;
} else {
instructionType = 'prompt';
}
}
const routed = manager.execute(targetSessionKey, {
tool,
prompt: instruction,
mode,
resumeKey: data.resumeKey,
resumeStrategy: data.resumeStrategy === 'promptConcat' ? 'promptConcat' : 'nativeResume'
resumeStrategy: data.resumeStrategy === 'promptConcat' ? 'promptConcat' : 'nativeResume',
instructionType: instructionType as InstructionType,
skillName,
});
// Best-effort: record audit event so Observability panel includes orchestrator-routed executions.