Compare commits

...

2 Commits

Author SHA1 Message Date
e0feb53437 feat: omo-paseo-bridge — auto-register OMO subagents as Paseo agents
Bridge script that calls paseo import <session-id> --provider opencode
--label omo=true on task() child sessions. Supports import, archive,
ls commands with --dry-run verification. Skill at .opencode/skills/
is gitignored (user-level) — copy from scripts/ on setup.
2026-06-08 01:11:00 +00:00
3c5b2c2bcf feat(server): Domain 2 Phase 1 — boocontext MCP client + 4 new code intelligence tools
Shared boocontext MCP client (boocontext_client.ts) wrapping the existing
mcp-client.ts callTool() infrastructure with 32KB truncation and error
handling. Used by get_code_health.

4 new first-class agent tools backed by the boocontext MCP server:
- get_code_health — A-F grades per file across 7 dimensions, project health
  summary, refactoring candidates (wraps boocontext_health)
- get_code_impact — merged symbol trace + blast radius in one call (wraps
  boocontext_impact, replaces two-step get_symbol_info+get_blast_radius)
- get_type_info — TypeScript type recovery via type-inject MCP (wraps
  boocontext_types, returns signatures, interfaces, generics, JSDoc)
- get_code_map — DCP-compressed context map with compress toggle (wraps
  boocontext_map, 10x token reduction vs full scan)

All 4 registered in ALL_TOOLS as read-only tools.
2026-06-08 00:45:46 +00:00
8 changed files with 1029 additions and 0 deletions

View File

@@ -0,0 +1,110 @@
/**
* v2.7.18: shared MCP client wrapper for the boocontext sidecar.
*
* Calls into the existing multi-server MCP client infrastructure
* (services/mcp-client.ts) which connects to boocontext as a stdio
* MCP process defined in data/mcp.json (server name "boocontext",
* command: `node /opt/forks/boocontext/dist/standalone.js`).
*
* The boocontext MCP server is initialized once at app boot in
* index.ts via initMcp() and the actual MCP tool call routing is
* handled by mcp-client.ts:callTool() — this module is a thin
* convenience wrapper that prepends the "boocontext_" server prefix,
* normalises the response, and applies inline truncation matching
* the same pattern as codecontext_client.ts.
*
* Usage:
* import { callBoocontext } from './services/boocontext_client.js';
* const resp = await callBoocontext({
* toolName: 'codesight_get_summary',
* args: { directory: '/opt/boocode' },
* });
*/
import { callTool } from './mcp-client.js';
import { truncateIfNeeded } from './truncate.js';
// ---- Exported types ----
export interface BoocontextRequest {
/** Unprefixed tool name as defined on the boocontext MCP server
* (e.g. "codesight_scan", "boocontext_overview", "codesight_get_summary"). */
toolName: string;
/** Arguments to pass to the tool. */
args: Record<string, unknown>;
}
export interface BoocontextResponse {
/** The tool output text. */
result: string;
/** Whether the result was truncated to fit the inline limit. */
truncated: boolean;
/** Opaque id pointing at the full pre-slice content on tmpfs, set when
* truncated=true and storage succeeded. */
outputPath?: string;
}
// ---- Constants ----
/** Must match the server name in data/mcp.json. */
const BOOCONTEXT_SERVER_NAME = 'boocontext';
/** Inline truncation limit, matching codecontext_client.ts. */
const TRUNCATION_LIMIT = 32_000;
// ---- Public API ----
/**
* Call a boocontext MCP tool by its unprefixed name.
*
* Prepends the "boocontext_" server prefix, delegates to the
* multi-server MCP client's callTool(), and normalises the response
* into a BoocontextResponse with inline truncation.
*
* @param req The tool name and arguments.
* @param log Optional Fastify-compatible logger (for debug traces).
* @returns The tool result, possibly truncated.
* @throws If the boocontext server is not connected or the tool
* returns an MCP-level error.
*/
export async function callBoocontext(
req: BoocontextRequest,
log?: { debug?: (obj: object, msg: string) => void; warn?: (obj: object, msg: string) => void },
): Promise<BoocontextResponse> {
const prefixedName = `${BOOCONTEXT_SERVER_NAME}_${req.toolName}`;
log?.debug?.({ tool: prefixedName }, 'boocontext: calling tool');
const raw = await callTool(prefixedName, req.args);
// callTool returns { error: true, output: string } on failure (both
// for MCP-level isError and for network/protocol exceptions).
if (typeof raw === 'object' && raw !== null && (raw as Record<string, unknown>).error === true) {
const errOutput = (raw as Record<string, unknown>).output ?? 'Unknown MCP error';
throw new Error(`boocontext error: ${String(errOutput)}`);
}
const result = typeof raw === 'string' ? raw : JSON.stringify(raw);
// Inline truncation at 32 kB, matching codecontext_client.ts.
// The model gets a clear hint about how to narrow the next call
// rather than a silent cut.
if (result.length > TRUNCATION_LIMIT) {
const truncated = result.slice(0, TRUNCATION_LIMIT);
const omitted = result.length - TRUNCATION_LIMIT;
const slicedWithMarker =
`${truncated}\n\n[truncated, ${omitted} chars omitted; narrow with additional filters]`;
const wrapped = await truncateIfNeeded({
fullContent: result,
slicedContent: slicedWithMarker,
wasTruncated: true,
});
return {
result: wrapped.content,
truncated: wrapped.truncated,
...(wrapped.outputPath ? { outputPath: wrapped.outputPath } : {}),
};
}
return { result, truncated: false };
}

View File

@@ -0,0 +1,62 @@
import { z } from 'zod';
import type { ToolDef } from '../types.js';
import { callBoocontext } from '../../boocontext_client.js';
export const GetCodeHealthInput = z.object({
directory: z.string().optional().describe('Directory to analyze (defaults to project root)'),
file: z.string().optional().describe('Optional: specific file to analyze'),
});
export type GetCodeHealthInputT = z.infer<typeof GetCodeHealthInput>;
const DESCRIPTION =
'Code health analysis. Returns AF grades per file across 7 dimensions ' +
'(cohesion, coupling, complexity, documentation, duplication, unit size, test coverage). ' +
'Includes project health summary and refactoring candidates.';
/**
* Standalone execute function — calls the boocontext MCP server's
* boocontext_health tool and returns the raw report text.
*
* Structured for direct test access: accepts input + projectPath,
* no side effects beyond the MCP call.
*/
export async function executeGetCodeHealth(
input: GetCodeHealthInputT,
projectPath: string,
): Promise<string> {
const args: Record<string, unknown> = {};
if (input.directory) args['directory'] = input.directory;
if (input.file) args['file'] = input.file;
const resp = await callBoocontext({ toolName: 'boocontext_health', args });
return resp.result;
}
export const getCodeHealth: ToolDef<GetCodeHealthInputT> = {
name: 'get_code_health',
description: DESCRIPTION,
inputSchema: GetCodeHealthInput,
jsonSchema: {
type: 'function',
function: {
name: 'get_code_health',
description: DESCRIPTION,
parameters: {
type: 'object',
properties: {
directory: {
type: 'string',
description: 'Directory to analyze (defaults to project root)',
},
file: {
type: 'string',
description: 'Optional: specific file to analyze',
},
},
additionalProperties: false,
},
},
},
async execute(input, projectRoot) {
return executeGetCodeHealth(input, projectRoot);
},
};

View File

@@ -0,0 +1,228 @@
import { spawn } from 'node:child_process';
import { resolve } from 'node:path';
import { z } from 'zod';
import type { ToolDef } from '../types.js';
import type { CodecontextResponse } from '../../codecontext_client.js';
// ======================= MCP Client =======================
const BOOCONTEXT_PATH = resolve('/opt/forks/boocontext/dist/standalone.js');
const TOOL_CALL_TIMEOUT_MS = 60_000;
interface JsonRpcMessage {
jsonrpc: '2.0';
id?: number | string;
result?: {
content?: Array<{ type: string; text: string }>;
};
error?: { code?: number; message: string };
}
/**
* Single-shot MCP JSON-RPC client for boocontext.
* Spawns the process, sends initialize + tools/call over NDJSON, returns the
* text result from the content array. The boocontext MCP server auto-detects
* newline-delimited JSON transport when the first input lacks Content-Length
* headers, which is exactly what we send.
*/
async function callBoocontext(
toolName: string,
args: Record<string, unknown>,
): Promise<string> {
return new Promise<string>((resolvePromise, reject) => {
const child = spawn(process.execPath, [BOOCONTEXT_PATH], {
stdio: ['pipe', 'pipe', 'pipe'],
timeout: TOOL_CALL_TIMEOUT_MS,
});
let stdout = '';
let stderr = '';
let resolved = false;
function finalize(err?: Error, result?: string): void {
if (resolved) return;
resolved = true;
if (err) reject(err);
else resolvePromise(result!);
child.kill();
}
child.stdout!.on('data', (chunk: Buffer) => {
stdout += chunk.toString();
});
child.stderr!.on('data', (chunk: Buffer) => {
stderr += chunk.toString();
});
child.on('error', (err: Error) => {
finalize(new Error(`boocontext spawn error: ${err.message}`));
});
child.on('close', (code: number | null) => {
if (resolved) return;
// Parse newline-delimited JSON responses from stdout
const lines = stdout.split('\n').filter((l) => l.trim().length > 0);
let toolText: string | undefined;
let toolError: string | undefined;
for (const line of lines) {
try {
const msg = JSON.parse(line) as JsonRpcMessage;
if (msg.id === 2) {
if (msg.error) {
toolError = msg.error.message ?? 'boocontext tool call failed';
} else if (msg.result?.content?.[0]?.text !== undefined) {
toolText = msg.result.content[0].text;
}
}
} catch {
// skip malformed JSON lines
}
}
if (toolError) {
finalize(new Error(toolError));
} else if (toolText !== undefined) {
finalize(undefined, toolText);
} else {
const errSuffix =
stderr.length > 0 ? ` stderr: ${stderr.slice(0, 500)}` : '';
finalize(
new Error(`boocontext MCP call failed (exit ${code})${errSuffix}`),
);
}
});
// Step 1: initialize — establishes MCP protocol version + capabilities
child.stdin!.write(
JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'initialize',
params: {
protocolVersion: '2024-11-05',
capabilities: {},
clientInfo: { name: 'boocode-server', version: '1.0.0' },
},
}) + '\n',
);
// Step 2: tools/call — invoke the named boocontext tool
child.stdin!.write(
JSON.stringify({
jsonrpc: '2.0',
id: 2,
method: 'tools/call',
params: { name: toolName, arguments: args },
}) + '\n',
);
child.stdin!.end();
// Safety timeout — prevent hung processes
setTimeout(() => {
finalize(
new Error(
`boocontext call timed out after ${TOOL_CALL_TIMEOUT_MS}ms`,
),
);
}, TOOL_CALL_TIMEOUT_MS);
});
}
// ======================= Tool Definition =======================
const TRUNCATION_LIMIT = 32_000;
export const GetCodeImpactInput = z.object({
symbol: z.string().min(1).describe('Symbol name for TSA trace_impact'),
file: z.string().optional().describe('File path for codesight blast_radius'),
directory: z
.string()
.optional()
.describe('Directory (defaults to project root)'),
depth: z
.number()
.int()
.min(1)
.max(5)
.optional()
.describe('Max blast-radius traversal depth (default 1)'),
});
export type GetCodeImpactInputT = z.infer<typeof GetCodeImpactInput>;
const DESCRIPTION =
'Impact analysis. Merges symbol-level call trace with file-level blast radius. ' +
'Use before making changes to understand change propagation. ' +
'Single call replaces separate get_symbol_info + get_blast_radius steps.';
/**
* Standalone execute function — calls the boocontext MCP `boocontext_impact`
* tool via a short-lived child process, then wraps the result in the standard
* CodecontextResponse shape with inline truncation at 32 KB.
*/
export async function executeGetCodeImpact(
input: GetCodeImpactInputT,
projectPath: string,
): Promise<CodecontextResponse> {
const args: Record<string, unknown> = {
symbol: input.symbol,
directory: input.directory ?? projectPath,
};
if (input.file) args['file'] = input.file;
const text = await callBoocontext('boocontext_impact', args);
// Inline truncation matching codecontext_client.ts patterns (32 KB ceiling).
if (text.length > TRUNCATION_LIMIT) {
const sliced = text.slice(0, TRUNCATION_LIMIT);
const omitted = text.length - TRUNCATION_LIMIT;
return {
result: `${sliced}\n\n[truncated, ${omitted} chars omitted; narrow with symbol or file parameters]`,
truncated: true,
};
}
return { result: text, truncated: false };
}
export const getCodeImpact: ToolDef<GetCodeImpactInputT> = {
name: 'get_code_impact',
description: DESCRIPTION,
inputSchema: GetCodeImpactInput,
jsonSchema: {
type: 'function',
function: {
name: 'get_code_impact',
description: DESCRIPTION,
parameters: {
type: 'object',
properties: {
symbol: {
type: 'string',
description: 'Symbol name for TSA trace_impact',
},
file: {
type: 'string',
description: 'File path for codesight blast_radius',
},
directory: {
type: 'string',
description: 'Directory (defaults to project root)',
},
depth: {
type: 'number',
description: 'Max blast-radius traversal depth (default 1)',
},
},
required: ['symbol'],
additionalProperties: false,
},
},
},
execute(input, projectRoot) {
return executeGetCodeImpact(input, projectRoot);
},
};

View File

@@ -0,0 +1,192 @@
import { spawn } from 'node:child_process';
import { z } from 'zod';
import type { ToolDef } from '../types.js';
export const GetCodeMapInput = z.object({
directory: z.string().optional().describe('Directory to scan (defaults to project root)'),
compress: z.boolean().optional().describe('Apply DCP compression if payload exceeds threshold (default: true)'),
});
export type GetCodeMapInputT = z.infer<typeof GetCodeMapInput>;
const DESCRIPTION =
'DCP-compressed codebase context map. Returns filenames, sizes, import relationships in a compressed format. ' +
'Use compress=false for full detail, compress=true (default) for token-efficient overview.';
const BOOCONTEXT_PATH = '/opt/forks/boocontext/dist/standalone.js';
const TOOL_TIMEOUT_MS = 30_000;
const MAX_RESULT_BYTES = 32_768;
export interface CodeMapResponse {
result: string;
truncated: boolean;
}
/**
* Calls the boocontext MCP server over stdio JSON-RPC to invoke
* the boocontext_map tool. Spawns the standalone binary, sends
* initialize + tools/call, collects NDJSON responses, and kills
* the child process.
*/
function callBoocontextMap(args: Record<string, unknown>): Promise<CodeMapResponse> {
return new Promise((resolve, reject) => {
const child = spawn('node', [BOOCONTEXT_PATH], {
stdio: ['pipe', 'pipe', 'pipe'],
});
let stdoutBuf = '';
const lines: string[] = [];
let timedOut = false;
let resolved = false;
const timer = setTimeout(() => {
timedOut = true;
child.kill('SIGKILL');
reject(new Error(`boocontext MCP call timed out after ${TOOL_TIMEOUT_MS}ms`));
}, TOOL_TIMEOUT_MS);
function tryParse(): void {
if (resolved || timedOut) return;
// Accumulate complete NDJSON lines
const parts = stdoutBuf.split('\n');
stdoutBuf = parts.pop()! ?? '';
for (const p of parts) {
const t = p.trim();
if (t) lines.push(t);
}
// Need at least 2 responses: initialize + tools/call
if (lines.length < 2) return;
resolved = true;
clearTimeout(timer);
child.kill();
try {
const callResponse = JSON.parse(lines[1]!);
if (callResponse.error) {
reject(new Error(`MCP error: ${callResponse.error.message}`));
return;
}
const content = callResponse.result?.content;
if (!content?.[0]?.text) {
reject(new Error('Unexpected MCP response shape — missing content[0].text'));
return;
}
// content[0].text is JSON-stringified VerdictEnvelope from boocontext
const envelope = JSON.parse(content[0].text as string);
const details = envelope.details;
let result: string;
if (details && typeof details === 'object' && 'data' in details) {
// DcpEnvelope shape: { compressed, originalLength, compressedLength, data }
if (details.compressed) {
// Return the full DcpEnvelope as JSON so the LLM can pass it
// transparently to a decompression step
result = JSON.stringify(details);
} else {
// Uncompressed — data is the raw output
result = details.data;
}
} else {
result = JSON.stringify(details ?? envelope);
}
const truncated = Buffer.byteLength(result, 'utf-8') > MAX_RESULT_BYTES;
if (truncated) {
result = result.substring(0, MAX_RESULT_BYTES);
}
resolve({ result, truncated });
} catch (e: any) {
reject(new Error(`Failed to parse boocontext response: ${e.message}`));
}
}
child.stdout!.on('data', (chunk: Buffer) => {
if (timedOut) return;
stdoutBuf += chunk.toString('utf-8');
tryParse();
});
child.stderr!.on('data', (_chunk: Buffer) => {
// Captured but not surfaced — logged only on parse failure
});
child.on('error', (err: Error) => {
clearTimeout(timer);
if (!resolved) {
resolved = true;
reject(new Error(`boocontext spawn failed: ${err.message}`));
}
});
child.on('close', () => {
clearTimeout(timer);
if (!resolved && !timedOut) {
tryParse();
if (!resolved) {
resolved = true;
reject(new Error('boocontext process closed without producing a valid response'));
}
}
});
// Step 1: initialize
child.stdin!.write(
JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize' }) + '\n',
);
// Step 2: tools/call for boocontext_map
child.stdin!.write(
JSON.stringify({
jsonrpc: '2.0',
id: 2,
method: 'tools/call',
params: { name: 'boocontext_map', arguments: args },
}) + '\n',
);
});
}
export const getCodeMap: ToolDef<GetCodeMapInputT> = {
name: 'get_code_map',
description: DESCRIPTION,
inputSchema: GetCodeMapInput,
jsonSchema: {
type: 'function',
function: {
name: 'get_code_map',
description: DESCRIPTION,
parameters: {
type: 'object',
properties: {
directory: { type: 'string', description: 'Directory to scan (defaults to project root)' },
compress: {
type: 'boolean',
description: 'Apply DCP compression if payload exceeds threshold (default: true)',
},
},
additionalProperties: false,
},
},
},
async execute(input, projectRoot): Promise<CodeMapResponse> {
return callBoocontextMap({
directory: input.directory ?? projectRoot,
compress: input.compress ?? true,
});
},
};
export async function executeGetCodeMap(
input: GetCodeMapInputT,
projectRoot: string,
): Promise<CodeMapResponse> {
return callBoocontextMap({
directory: input.directory ?? projectRoot,
compress: input.compress ?? true,
});
}

View File

@@ -0,0 +1,262 @@
import { z } from 'zod';
import { spawn } from 'node:child_process';
import type { ToolDef } from '../types.js';
import type { CodecontextResponse } from '../../codecontext_client.js';
const BOOCONTEXT_PATH = '/opt/forks/boocontext/dist/standalone.js';
const TRUNCATION_LIMIT = 32_000;
export const GetTypeInfoInput = z.object({
file: z.string().min(1).describe('File path to resolve types in'),
symbol: z.string().optional().describe('Symbol name to resolve (supports regex)'),
directory: z.string().optional().describe('Project directory for type resolution context'),
});
export type GetTypeInfoInputT = z.infer<typeof GetTypeInfoInput>;
const DESCRIPTION =
'TypeScript type recovery. Returns type signatures, interface definitions, ' +
'generic constraints, and JSDoc for symbols in a file. Uses type-inject MCP server.';
// ---- JSON-RPC-over-stdio MCP caller for boocontext --------------------------
async function callBoocontext(
toolName: string,
args: Record<string, unknown>,
): Promise<CodecontextResponse> {
const child = spawn(process.execPath, [BOOCONTEXT_PATH], {
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 60_000,
});
let stderrBuf = '';
child.stderr!.on('data', (chunk: Buffer) => {
stderrBuf += chunk.toString('utf-8');
});
let killed = false;
const killChild = () => {
if (killed) return;
killed = true;
child.kill();
};
try {
// Read one complete JSON-RPC response from stdout (handles both
// Content-Length framed and newline-delimited transport).
async function readResponse(timeoutMs = 30_000): Promise<unknown> {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
cleanup();
reject(new Error('Timeout reading boocontext response'));
}, timeoutMs);
let buf = '';
const cleanup = () => {
clearTimeout(timer);
child.stdout!.removeListener('data', onData);
child.stdout!.removeListener('end', onEnd);
child.stdout!.removeListener('error', onError);
};
const onData = (chunk: Buffer) => {
buf += chunk.toString('utf-8');
const msg = tryExtractMessage(buf);
if (msg !== null) {
cleanup();
resolve(msg);
return;
}
if (buf.length > 1_024 * 1_024) {
cleanup();
reject(new Error('Boocontext response exceeded 1 MB'));
}
};
const onEnd = () => {
cleanup();
if (buf.trim()) {
try {
resolve(JSON.parse(buf.trim()));
} catch {
reject(new Error('Boocontext stream ended with incomplete data'));
}
} else {
reject(new Error('Boocontext stream ended unexpectedly'));
}
};
const onError = (err: Error) => {
cleanup();
reject(err);
};
child.stdout!.on('data', onData);
child.stdout!.on('end', onEnd);
child.stdout!.on('error', onError);
});
}
// Wait for the process to be fully spawned.
await new Promise<void>((resolve, reject) => {
child.on('error', reject);
child.on('spawn', () => resolve());
});
// Step 1 — MCP initialize
let reqId = 0;
reqId++;
child.stdin!.write(
JSON.stringify({ jsonrpc: '2.0', id: reqId, method: 'initialize' }) + '\n',
);
const initResp = await readResponse() as { error?: { message: string } };
if (initResp.error) {
throw new Error(`Boocontext init failed: ${initResp.error.message}`);
}
// Step 2 — tools/call
reqId++;
child.stdin!.write(
JSON.stringify({
jsonrpc: '2.0',
id: reqId,
method: 'tools/call',
params: { name: toolName, arguments: args },
}) + '\n',
);
const callResp = await readResponse() as {
error?: { message: string };
result?: { content?: Array<{ type: string; text: string }> };
};
if (callResp.error) {
throw new Error(`Boocontext tool call failed: ${callResp.error.message}`);
}
// Extract text from the MCP tool result shape:
// { content: [{ type: "text", text: "…" }] }
const content = callResp.result?.content;
let text: string;
if (Array.isArray(content) && content.length > 0 && content[0]!.type === 'text') {
text = content[0]!.text;
} else {
text = JSON.stringify(callResp.result);
}
// Inline truncation at 32 KB.
if (text.length > TRUNCATION_LIMIT) {
const omitted = text.length - TRUNCATION_LIMIT;
return {
result:
text.slice(0, TRUNCATION_LIMIT) +
`\n\n[truncated, ${omitted} chars omitted; narrow with file or symbol filter]`,
truncated: true,
};
}
return { result: text, truncated: false };
} finally {
killChild();
// Give the process a moment to release resources.
await new Promise<void>((resolve) => {
const timer = setTimeout(resolve, 2_000);
child.on('exit', () => {
clearTimeout(timer);
resolve();
});
});
}
}
/**
* Attempt to extract one complete JSON-RPC message from the head of a
* buffer. Handles both Content-Length framed and newline-delimited
* formats. Returns `null` when more data is needed.
*/
function tryExtractMessage(buf: string): unknown | null {
// --- Content-Length framed ---
const headerEnd = buf.indexOf('\r\n\r\n');
if (headerEnd !== -1) {
const header = buf.substring(0, headerEnd);
const lengthMatch = header.match(/Content-Length:\s*(\d+)/i);
if (lengthMatch) {
const contentLength = parseInt(lengthMatch[1]!, 10);
const bodyStart = headerEnd + 4;
if (buf.length >= bodyStart + contentLength) {
const jsonStr = buf.substring(bodyStart, bodyStart + contentLength);
return JSON.parse(jsonStr);
}
return null; // need more data
}
// Has \r\n\r\n but no Content-Length — junk segment; skip and retry.
return tryExtractMessage(buf.substring(headerEnd + 4));
}
// --- Newline-delimited ---
const nlIndex = buf.indexOf('\n');
if (nlIndex !== -1) {
const line = buf.substring(0, nlIndex).trim();
if (line && line.startsWith('{')) {
return JSON.parse(line);
}
// Non-JSON line (e.g. stderr echo), skip and continue.
return tryExtractMessage(buf.substring(nlIndex + 1));
}
return null; // need more data
}
// ---- ToolDef ----------------------------------------------------------------
export const getTypeInfo: ToolDef<GetTypeInfoInputT> = {
name: 'get_type_info',
description: DESCRIPTION,
inputSchema: GetTypeInfoInput,
jsonSchema: {
type: 'function',
function: {
name: 'get_type_info',
description: DESCRIPTION,
parameters: {
type: 'object',
properties: {
file: { type: 'string', description: 'File path to resolve types in' },
symbol: {
type: 'string',
description: 'Symbol name to resolve (supports regex)',
},
directory: {
type: 'string',
description: 'Project directory for type resolution context',
},
},
required: ['file'],
additionalProperties: false,
},
},
},
async execute(input): Promise<CodecontextResponse> {
const args: Record<string, unknown> = { file: input.file };
if (input.symbol) args['symbol'] = input.symbol;
return callBoocontext('boocontext_types', args);
},
};
/**
* Standalone execute function matching the `execute` shape returned by
* `makeCodecontextTool` — useful for direct callers and tests.
*
* Note: unlike the HTTP-backed codecontext tools this does NOT accept a
* `fetcher` override because it communicates over stdio rather than HTTP.
*/
export async function executeGetTypeInfo(
input: GetTypeInfoInputT,
_projectPath?: string,
): Promise<CodecontextResponse> {
const args: Record<string, unknown> = { file: input.file };
if (input.symbol) args['symbol'] = input.symbol;
return callBoocontext('boocontext_types', args);
}

View File

@@ -13,3 +13,8 @@ export { getBlastRadius } from './get_blast_radius.js';
export { getHotFiles } from './get_hot_files.js'; export { getHotFiles } from './get_hot_files.js';
export { getRoutes } from './get_routes.js'; export { getRoutes } from './get_routes.js';
export { getMiddleware } from './get_middleware.js'; export { getMiddleware } from './get_middleware.js';
// v2.8.14-domain2-phase1: boocontext-backed tools.
export { getCodeHealth } from './get_code_health.js';
export { getCodeImpact } from './get_code_impact.js';
export { getTypeInfo } from './get_type_info.js';
export { getCodeMap } from './get_code_map.js';

View File

@@ -19,6 +19,10 @@ import {
getHotFiles, getHotFiles,
getRoutes, getRoutes,
getMiddleware, getMiddleware,
getCodeHealth,
getCodeImpact,
getTypeInfo,
getCodeMap,
} from './codecontext/index.js'; } from './codecontext/index.js';
// v1.13.17-cross-repo-reads: cross-repo read grant request tool. Paired // v1.13.17-cross-repo-reads: cross-repo read grant request tool. Paired
// with the pause-on-pending-grant branch in inference/tool-phase.ts and the // with the pause-on-pending-grant branch in inference/tool-phase.ts and the
@@ -75,6 +79,12 @@ export let ALL_TOOLS: ToolDef<unknown>[] = [
// v2.6.x: read a tab's transcript by its session-scoped tab number. // v2.6.x: read a tab's transcript by its session-scoped tab number.
// Read-only; uses the ToolExecCtx 4th arg for DB/session access. // Read-only; uses the ToolExecCtx 4th arg for DB/session access.
readTabByNumber as ToolDef<unknown>, readTabByNumber as ToolDef<unknown>,
// v2.8.14-domain2-phase1: boocontext-backed tools. Backed by the boocontext
// MCP server. All read-only. Health, impact, types, map analysis.
getCodeHealth as ToolDef<unknown>,
getCodeImpact as ToolDef<unknown>,
getTypeInfo as ToolDef<unknown>,
getCodeMap as ToolDef<unknown>,
].sort((a, b) => a.name.localeCompare(b.name)); ].sort((a, b) => a.name.localeCompare(b.name));
export let TOOLS_BY_NAME: Record<string, ToolDef<unknown>> = Object.fromEntries( export let TOOLS_BY_NAME: Record<string, ToolDef<unknown>> = Object.fromEntries(

160
scripts/omo-paseo-bridge.sh Executable file
View File

@@ -0,0 +1,160 @@
#!/usr/bin/env bash
set -euo pipefail
# =============================================================================
# omo-paseo-bridge.sh — Import OMO task() child sessions as Paseo agents
#
# Automates calling `paseo import` on child session IDs so OMO subagents
# appear in `paseo ls` alongside native Paseo agents.
#
# Usage:
# omo-paseo-bridge.sh import [--type <category>] <session-id>...
# Import session(s) as Paseo agents with omo=true labels
#
# omo-paseo-bridge.sh archive <agent-id>...
# Archive (soft-delete) agent(s) imported by this bridge
#
# omo-paseo-bridge.sh ls [--all]
# List agents tagged omo=true via paseo ls
#
# omo-paseo-bridge.sh --dry-run <command> ...
# Print what would be done without executing
#
# Examples:
# omo-paseo-bridge.sh import ses_abc123 ses_def456
# omo-paseo-bridge.sh import --type research ses_abc123
# omo-paseo-bridge.sh archive agt_789
# omo-paseo-bridge.sh ls
# omo-paseo-bridge.sh --dry-run import ses_abc123
# =============================================================================
SCRIPT_NAME="$(basename "$0")"
PASEO="$(which paseo 2>/dev/null || echo "paseo")"
DRY_RUN=false
# ── helpers ──────────────────────────────────────────────────────────────────
log() { printf "[%s] %s\n" "$SCRIPT_NAME" "$*"; }
warn() { printf "[%s] WARNING: %s\n" "$SCRIPT_NAME" "$*" >&2; }
err() { printf "[%s] ERROR: %s\n" "$SCRIPT_NAME" "$*" >&2; exit 1; }
paseo_cmd() {
if $DRY_RUN; then
log "[DRY-RUN] would run: $PASEO $*"
return 0
fi
"$PASEO" "$@" 2>&1 || warn "'paseo $*' exited with code $?"
}
paseo_import() {
local session_id="$1"
shift
local type_label="${1:-}"
local labels=("--label" "omo=true")
# Add parent session label if OMO_SESSION_ID is set (injected by agent)
if [[ -n "${OMO_SESSION_ID:-}" ]]; then
labels+=("--label" "parent=${OMO_SESSION_ID}")
fi
if [[ -n "$type_label" ]]; then
labels+=("--label" "type=${type_label}")
fi
log "Importing session ${session_id} as Paseo agent ..."
paseo_cmd import "$session_id" --provider opencode "${labels[@]}"
}
paseo_archive() {
local agent_id="$1"
log "Archiving agent ${agent_id} ..."
paseo_cmd archive "$agent_id" --force
}
paseo_list() {
local all_flag="${1:-}"
if [[ "$all_flag" == "--all" ]]; then
paseo_cmd ls --label "omo=true" --all
else
paseo_cmd ls --label "omo=true"
fi
}
# ── usage ────────────────────────────────────────────────────────────────────
usage() {
cat <<EOF
Usage: $SCRIPT_NAME [--dry-run] <command> [options] [args...]
Commands:
import [--type <category>] <session-id>...
Import OMO child session(s) as Paseo agents
archive <agent-id>...
Archive Paseo agent(s) (soft-delete)
ls [--all]
List agents tagged omo=true
Options:
--dry-run Print actions without executing them
-h, --help Show this help
Examples:
$SCRIPT_NAME import --type research ses_abc123
$SCRIPT_NAME archive agt_789
$SCRIPT_NAME ls --all
EOF
exit 0
}
# ── main ─────────────────────────────────────────────────────────────────────
# Peel off global flags
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=true; shift ;;
-h|--help) usage ;;
*) break ;;
esac
done
[[ $# -eq 0 ]] && usage
COMMAND="$1"
shift
case "$COMMAND" in
import)
TYPE_LABEL=""
SESSION_IDS=()
while [[ $# -gt 0 ]]; do
case "$1" in
--type) TYPE_LABEL="$2"; shift 2 ;;
--type=*) TYPE_LABEL="${1#*=}"; shift ;;
-*) err "Unknown option for import: $1" ;;
*) SESSION_IDS+=("$1"); shift ;;
esac
done
[[ ${#SESSION_IDS[@]} -eq 0 ]] && err "import requires at least one session-id"
for sid in "${SESSION_IDS[@]}"; do
paseo_import "$sid" "$TYPE_LABEL"
done
;;
archive)
[[ $# -eq 0 ]] && err "archive requires at least one agent-id"
for aid in "$@"; do
paseo_archive "$aid"
done
;;
ls)
paseo_list "${1:-}"
;;
*)
err "Unknown command: $COMMAND\n$(usage)"
;;
esac