mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat: add change_title MCP tool support to OpenCode
This commit is contained in:
@@ -19,6 +19,7 @@ type AutoApprovalRuleSet = {
|
||||
const AUTO_APPROVE_TOOL_NAME_HINTS = [
|
||||
'change_title',
|
||||
'happy__change_title',
|
||||
'hapi_change_title', // OpenCode MCP tool pattern
|
||||
'geminireasoning',
|
||||
'codexreasoning',
|
||||
'think',
|
||||
|
||||
@@ -5,6 +5,9 @@ import { Future } from '@/utils/future';
|
||||
import { getLocalLaunchExitReason } from '@/agent/localLaunchPolicy';
|
||||
import { ensureOpencodeHookPlugin } from './utils/hookPlugin';
|
||||
import { buildOpencodeEnv } from './utils/config';
|
||||
import { ensureOpencodeConfig } from './utils/opencodeConfig';
|
||||
import { TITLE_INSTRUCTION } from './utils/systemPrompt';
|
||||
import { buildHapiMcpBridge } from '@/codex/utils/buildHapiMcpBridge';
|
||||
import type { OpencodeHookEvent } from './types';
|
||||
import type { OpencodeHookServer } from './utils/startOpencodeHookServer';
|
||||
import { createOpencodeStorageScanner, type OpencodeStorageScannerHandle } from './utils/opencodeStorageScanner';
|
||||
@@ -263,6 +266,21 @@ export async function opencodeLocalLauncher(
|
||||
const opencodeConfigDir = resolveOpencodeConfigDir(session);
|
||||
ensureOpencodeHookPlugin(opencodeConfigDir, hookUrl, opts.hookServer.token);
|
||||
|
||||
// Start the hapi MCP server for change_title support (optional feature)
|
||||
let happyServer: { url: string; stop: () => void } | null = null;
|
||||
let opencodeConfigPath: string | null = null;
|
||||
try {
|
||||
const bridge = await buildHapiMcpBridge(session.client);
|
||||
happyServer = bridge.server;
|
||||
logger.debug(`[opencode-local]: Started hapi MCP server at ${happyServer.url}`);
|
||||
|
||||
// Generate opencode.json config with MCP server and instructions
|
||||
const { configPath } = ensureOpencodeConfig(opencodeConfigDir, bridge.mcpServers.hapi, TITLE_INSTRUCTION);
|
||||
opencodeConfigPath = configPath;
|
||||
} catch (error) {
|
||||
logger.debug('[opencode-local]: Failed to start hapi MCP server (change_title will be unavailable)', error);
|
||||
}
|
||||
|
||||
let storageScanner: OpencodeStorageScannerHandle | null = null;
|
||||
const messageRoles = new Map<string, string>();
|
||||
const sentTextParts = new Set<string>();
|
||||
@@ -617,6 +635,10 @@ export async function opencodeLocalLauncher(
|
||||
if (!env.OPENCODE_CONFIG_DIR) {
|
||||
env.OPENCODE_CONFIG_DIR = opencodeConfigDir;
|
||||
}
|
||||
// Set OPENCODE_CONFIG to point to our generated config file (if MCP server started)
|
||||
if (!env.OPENCODE_CONFIG && opencodeConfigPath) {
|
||||
env.OPENCODE_CONFIG = opencodeConfigPath;
|
||||
}
|
||||
|
||||
await opencodeLocal({
|
||||
path: session.path,
|
||||
@@ -659,6 +681,10 @@ export async function opencodeLocalLauncher(
|
||||
if (storageScanner) {
|
||||
await storageScanner.cleanup();
|
||||
}
|
||||
if (happyServer) {
|
||||
happyServer.stop();
|
||||
logger.debug('[opencode-local]: Stopped hapi MCP server');
|
||||
}
|
||||
}
|
||||
|
||||
return exitReason || 'exit';
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { OpencodeSession } from './session';
|
||||
import type { PermissionMode } from './types';
|
||||
import { createOpencodeBackend } from './utils/opencodeBackend';
|
||||
import { OpencodePermissionHandler } from './utils/permissionHandler';
|
||||
import { TITLE_INSTRUCTION } from './utils/systemPrompt';
|
||||
|
||||
class OpencodeRemoteLauncher extends RemoteLauncherBase {
|
||||
private readonly session: OpencodeSession;
|
||||
@@ -17,6 +18,7 @@ class OpencodeRemoteLauncher extends RemoteLauncherBase {
|
||||
private happyServer: { stop: () => void } | null = null;
|
||||
private abortController = new AbortController();
|
||||
private displayPermissionMode: PermissionMode | null = null;
|
||||
private instructionsSent = false;
|
||||
|
||||
constructor(session: OpencodeSession) {
|
||||
super(process.env.DEBUG ? session.logPath : undefined);
|
||||
@@ -112,9 +114,16 @@ class OpencodeRemoteLauncher extends RemoteLauncherBase {
|
||||
this.applyDisplayMode(batch.mode.permissionMode);
|
||||
messageBuffer.addMessage(batch.message, 'user');
|
||||
|
||||
// Inject title instructions on first prompt
|
||||
let messageText = batch.message;
|
||||
if (!this.instructionsSent) {
|
||||
messageText = `${TITLE_INSTRUCTION}\n\n${batch.message}`;
|
||||
this.instructionsSent = true;
|
||||
}
|
||||
|
||||
const promptContent: PromptContent[] = [{
|
||||
type: 'text',
|
||||
text: batch.message
|
||||
text: messageText
|
||||
}];
|
||||
|
||||
session.onThinkingChange(true);
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* OpenCode configuration file generator.
|
||||
*
|
||||
* Generates opencode.json with MCP server configuration and instructions
|
||||
* for the hapi change_title tool.
|
||||
*/
|
||||
|
||||
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const CONFIG_FILENAME = 'opencode.json';
|
||||
const INSTRUCTIONS_FILENAME = 'hapi-instructions.md';
|
||||
|
||||
interface McpServerEntry {
|
||||
command: string;
|
||||
args: string[];
|
||||
}
|
||||
|
||||
interface OpencodeConfig {
|
||||
$schema: string;
|
||||
mcp: Record<string, {
|
||||
type: string;
|
||||
command: string[];
|
||||
enabled: boolean;
|
||||
}>;
|
||||
instructions: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the opencode.json config file exists with MCP server and instructions.
|
||||
*
|
||||
* @param rootPath - The OPENCODE_CONFIG_DIR path
|
||||
* @param mcpServer - The hapi MCP server command configuration
|
||||
* @param instructions - The instruction text to write to the instructions file
|
||||
*/
|
||||
export function ensureOpencodeConfig(
|
||||
rootPath: string,
|
||||
mcpServer: McpServerEntry,
|
||||
instructions: string
|
||||
): { configPath: string; instructionsPath: string } {
|
||||
mkdirSync(rootPath, { recursive: true });
|
||||
|
||||
// Write instructions file
|
||||
const instructionsPath = join(rootPath, INSTRUCTIONS_FILENAME);
|
||||
writeFileSafe(instructionsPath, instructions);
|
||||
|
||||
// Build opencode.json config
|
||||
// Use absolute path for instructions since OpenCode resolves paths relative to project root
|
||||
const config: OpencodeConfig = {
|
||||
$schema: 'https://opencode.ai/config.json',
|
||||
mcp: {
|
||||
hapi: {
|
||||
type: 'local',
|
||||
command: [mcpServer.command, ...mcpServer.args],
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
instructions: [instructionsPath]
|
||||
};
|
||||
|
||||
const configPath = join(rootPath, CONFIG_FILENAME);
|
||||
const configJson = JSON.stringify(config, null, 2);
|
||||
writeFileSafe(configPath, configJson);
|
||||
|
||||
return { configPath, instructionsPath };
|
||||
}
|
||||
|
||||
/**
|
||||
* Write file only if content has changed.
|
||||
*/
|
||||
function writeFileSafe(filePath: string, content: string): void {
|
||||
try {
|
||||
const current = readFileSync(filePath, 'utf-8');
|
||||
if (current === content) {
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Ignore missing or unreadable file
|
||||
}
|
||||
writeFileSync(filePath, content, 'utf-8');
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* OpenCode-specific system prompt for change_title tool.
|
||||
*
|
||||
* OpenCode exposes MCP tools with the naming pattern: <server-name>_<tool-name>
|
||||
* The hapi MCP server exposes `change_title`, so it's called as `hapi_change_title`.
|
||||
*/
|
||||
|
||||
import { trimIdent } from '@/utils/trimIdent';
|
||||
|
||||
/**
|
||||
* Title instruction for OpenCode to call the hapi MCP tool.
|
||||
*/
|
||||
export const TITLE_INSTRUCTION = trimIdent(`
|
||||
ALWAYS when you start a new chat - you must call the tool "hapi_change_title" to set a chat title. When you think chat title is not relevant anymore - call the tool again to change it. When chat name is too generic and you have a chance to make it more specific - call the tool again to change it. This title is needed to easily find the chat in the future. Help human.
|
||||
`);
|
||||
|
||||
/**
|
||||
* The system prompt to inject for OpenCode sessions.
|
||||
*/
|
||||
export const opencodeSystemPrompt = TITLE_INSTRUCTION;
|
||||
Reference in New Issue
Block a user