Wire the new tools, the mcp hooks, and the farewell into the app

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Muhammad Zakir Ramadhan
2026-09-05 17:32:14 +07:00
co-authored by Sisyphus
parent 5feec4b5c7
commit 2dc7b91c29
2 changed files with 213 additions and 496 deletions
+67 -4
View File
@@ -5,10 +5,12 @@ import type { LanguageModel, ModelMessage } from 'ai';
import { resolveAgent, VARIANTS, isThinkingLevel, type AgentVariant } from './agents';
import { configPath, loadConfig, missingKeyMessage, resolveModel, writeConfigFile, type Config } from './config';
import type { FallbackEvent } from './fallback';
import { farewell } from './farewell';
import { readStdin, runHeadless } from './headless';
import { INIT_PROMPT, loadInstructions } from './instructions';
import { walk } from './ignore';
import { connectMcp } from './mcp';
import { createCommitMessageTool } from './commit';
import { Memory, KIND_LABEL } from './memory';
import { costOf } from './pricing';
import { BUILTIN_PLUGINS, DEFAULT_ENABLED } from './plugins-builtin';
@@ -32,7 +34,6 @@ const HELP = `shiro-neko ${VERSION} - agentic coding CLI
usage: shiro [options]
shiro -p "prompt" headless, prints to stdout
cat file | shiro -p prompt read from stdin
options:
-p, --print [prompt] headless mode; requires --yolo for tool use
--json with -p, emit one JSON event per line
@@ -67,6 +68,7 @@ env: SHIRO_PROVIDER SHIRO_MODEL SHIRO_BASE_URL SHIRO_API_KEY
skills: builtin, plus ~/.shiro-neko/skills/*.md and .shiro/skills/*.md
registry: /registry to browse and install external skills and plugins
mcp: /mcp to add a local or remote server, or list what is configured
sessions: ${store.sessionsDir()}
in-session: /help for the command list`;
@@ -276,6 +278,10 @@ const session = new Session({
...(cfg.maxRetries !== undefined ? { maxRetries: cfg.maxRetries } : {}),
extraTools: {
...(mcp?.tools ?? {}),
git_commit_message: createCommitMessageTool({
model: languageModel ?? unconfiguredModel,
...(headless ? {} : { cwd: process.cwd() }),
}),
...(has('--no-subagent')
? {}
: {
@@ -289,7 +295,7 @@ const session = new Session({
}),
}),
},
autoApprove: ['task'],
autoApprove: ['task', 'git_commit_message'],
messages: [...record.messages],
onChange: (messages) => {
// Debounced so a long tool loop does not hit the disk on every step.
@@ -306,7 +312,6 @@ async function shutdown(code: number): Promise<never> {
await mcp?.close();
process.exit(code);
}
const printArg = flag('-p', '--print');
if (printArg !== undefined) {
const prompt = printArg || (await readStdin());
@@ -388,6 +393,51 @@ const hooks: AppHooks = {
throw new Error(`nothing installed under the name "${bare}"`);
},
},
mcp: {
names: () => Object.keys(cfg.mcpServers ?? {}),
list: () => {
const servers = Object.entries(cfg.mcpServers ?? {});
if (servers.length === 0) return 'no MCP servers configured\n\n`/mcp add` sets one up.';
const live = new Map<string, number>();
for (const name of Object.keys(mcp?.tools ?? {})) {
const server = /^mcp__([^_]+(?:_[^_]+)*)__/.exec(name)?.[1];
if (server) live.set(server, (live.get(server) ?? 0) + 1);
}
const failed = new Map((mcp?.errors ?? []).map((e) => [e.server, e.message]));
const rows = servers.map(([name, config]) => {
const where = 'url' in config ? config.url : [config.command, ...(config.args ?? [])].join(' ');
const state = failed.has(name)
? `failed: ${failed.get(name)}`
: live.has(name)
? `${live.get(name)} tools`
: has('--no-mcp')
? 'not connected (--no-mcp)'
: 'not connected this session';
return `- \`${name}\` (${'url' in config ? 'remote' : 'local'}) - ${state}\n ${where}`;
});
return [...rows, '', `configured in ${configPath()}`].join('\n');
},
add: async (result) => {
const servers = { ...(cfg.mcpServers ?? {}), [result.name]: result.config };
cfg = { ...cfg, mcpServers: servers };
const path = await writeConfigFile({ mcpServers: servers });
const where = 'url' in result.config ? result.config.url : result.config.command;
// Connected at boot, like the servers already in the file: a mid-turn connect
// would change the tool list under a turn that is already running.
return `added mcp server ${result.name} (${where})\nsaved to ${path}\nrestart shiro to connect it`;
},
remove: async (name) => {
const servers = { ...(cfg.mcpServers ?? {}) };
if (!(name in servers)) throw new Error(`no MCP server named "${name}"`);
delete servers[name];
cfg = { ...cfg, mcpServers: servers };
const path = await writeConfigFile({ mcpServers: servers });
return `removed mcp server ${name}\nsaved to ${path}\nrestart shiro to disconnect it`;
},
},
initPrompt: INIT_PROMPT,
history: promptHistory,
recordPrompt: (text) => void store.appendHistory(text),
@@ -513,12 +563,15 @@ const header = [
...plugins.errors.map((e) => `plugin ${e.plugin}: ${e.message}`),
memory && memory.all().length > 0 ? `memory: ${memory.all().length} notes about this project` : undefined,
mcp && Object.keys(mcp.tools).length > 0 ? `mcp: ${Object.keys(mcp.tools).length} tools` : undefined,
!mcp && cfg.mcpServers && Object.keys(cfg.mcpServers).length > 0
? `mcp: ${Object.keys(cfg.mcpServers).length} configured, not connected (--no-mcp)`
: undefined,
...(mcp?.errors ?? []).map((e) => `mcp ${e.server} failed: ${e.message}`),
yolo
? 'approvals: OFF (--yolo), but deny rules and the guard still apply'
: cfg.permission
? `approvals: rules for ${Object.keys(cfg.permission).join(', ')}, defaults elsewhere`
: 'approvals: ask for write_file, edit_file, multi_edit, bash, mcp__*',
: 'approvals: ask for write_file, edit_file, multi_edit, apply_patch, move_file, delete_file, bash, web_fetch, mcp__*',
cfg.toolSets ? `tool sets: core, ${cfg.toolSets.join(', ')}` : undefined,
'/help for commands',
]
@@ -541,4 +594,14 @@ const app = render(
{ exitOnCtrlC: false },
);
await app.waitUntilExit();
// Printed after Ink has released the screen, so it survives the final repaint. The
// title comes from the messages rather than `record`, whose own title is only
// refreshed by the debounced save and may not have run yet.
console.log(
farewell({
id: record.id,
messages: session.messages.length,
title: store.titleOf(session.messages),
}),
);
await shutdown(0);