@oh-my-pi/pi-coding-agent 10.3.1 → 10.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/docs/config-usage.md +41 -41
- package/package.json +7 -7
- package/src/cli/args.ts +2 -2
- package/src/config.ts +1 -4
- package/src/extensibility/custom-tools/types.ts +6 -1
- package/src/extensibility/custom-tools/wrapper.ts +7 -2
- package/src/extensibility/extensions/types.ts +6 -1
- package/src/extensibility/extensions/wrapper.ts +4 -3
- package/src/extensibility/hooks/tool-wrapper.ts +2 -2
- package/src/mcp/render.ts +339 -0
- package/src/mcp/tool-bridge.ts +24 -1
- package/src/sdk.ts +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [10.3.2] - 2026-02-03
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Added `renderCall` and `renderResult` methods to MCP tools for structured TUI display of tool calls and results
|
|
9
|
+
- Added new `mcp/render.ts` module providing JSON tree rendering for MCP tool output with collapsible/expandable views
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Updated `renderResult` signature in custom tools and extensions to accept optional `args` parameter for context-aware rendering
|
|
14
|
+
- Changed environment variable from `ENV_AGENT_DIR` constant to hardcoded `OMP_CODING_AGENT_DIR` string in config and CLI help text
|
|
15
|
+
- Fixed method binding in extension and hook tool wrappers to preserve `this` context for `renderCall` and `renderResult` methods
|
|
16
|
+
|
|
5
17
|
## [10.3.1] - 2026-02-03
|
|
6
18
|
### Fixed
|
|
7
19
|
|
package/docs/config-usage.md
CHANGED
|
@@ -8,7 +8,7 @@ This document shows how each file uses the config module and what subpaths they
|
|
|
8
8
|
┌─────────────────────────────────────────────────────────────────────────────────┐
|
|
9
9
|
│ config.ts exports │
|
|
10
10
|
├─────────────────────────────────────────────────────────────────────────────────┤
|
|
11
|
-
│ Constants: APP_NAME, CONFIG_DIR_NAME, VERSION
|
|
11
|
+
│ Constants: APP_NAME, CONFIG_DIR_NAME, VERSION │
|
|
12
12
|
│ Single paths: getAgentDir, getAuthPath, getModelsPath, getCommandsDir, ... │
|
|
13
13
|
│ Multi-config: getConfigDirs, getConfigDirPaths, findConfigFile, │
|
|
14
14
|
│ readConfigFile, findNearestProjectConfigDir, ... │
|
|
@@ -19,51 +19,51 @@ This document shows how each file uses the config module and what subpaths they
|
|
|
19
19
|
|
|
20
20
|
### 1. Display/Branding Only (no file I/O)
|
|
21
21
|
|
|
22
|
-
| File
|
|
23
|
-
|
|
24
|
-
| `cli/args.ts`
|
|
25
|
-
| `cli/plugin-cli.ts`
|
|
26
|
-
| `cli/update-cli.ts`
|
|
27
|
-
| `core/export-html/index.ts`
|
|
28
|
-
| `modes/interactive/components/welcome.ts` | `APP_NAME`
|
|
29
|
-
| `utils/tools-manager.ts`
|
|
22
|
+
| File | Imports | Purpose |
|
|
23
|
+
| ----------------------------------------- | ----------------------------- | ------------------------ |
|
|
24
|
+
| `cli/args.ts` | `APP_NAME`, `CONFIG_DIR_NAME` | Help text, env var names |
|
|
25
|
+
| `cli/plugin-cli.ts` | `APP_NAME` | Command output |
|
|
26
|
+
| `cli/update-cli.ts` | `APP_NAME`, `VERSION` | Update messages |
|
|
27
|
+
| `core/export-html/index.ts` | `APP_NAME` | HTML export title |
|
|
28
|
+
| `modes/interactive/components/welcome.ts` | `APP_NAME` | Welcome banner |
|
|
29
|
+
| `utils/tools-manager.ts` | `APP_NAME` | Tool download messages |
|
|
30
30
|
|
|
31
31
|
### 2. Single Fixed Paths (user-level only)
|
|
32
32
|
|
|
33
|
-
| File
|
|
34
|
-
|
|
35
|
-
| `core/logger.ts`
|
|
36
|
-
| `core/agent-session.ts`
|
|
37
|
-
| `core/session-manager.ts`
|
|
38
|
-
| `modes/interactive/theme/theme.ts`
|
|
39
|
-
| `modes/interactive/interactive-mode.ts` | `getAuthPath`, `getDebugLogPath`
|
|
40
|
-
| `utils/changelog.ts`
|
|
41
|
-
| `core/system-prompt.ts`
|
|
42
|
-
| `migrations.ts`
|
|
43
|
-
| `core/plugins/installer.ts`
|
|
44
|
-
| `core/plugins/paths.ts`
|
|
33
|
+
| File | Imports | Path | Purpose |
|
|
34
|
+
| --------------------------------------- | ---------------------------------------------------------------- | -------------------------- | ---------------------- |
|
|
35
|
+
| `core/logger.ts` | `CONFIG_DIR_NAME` | `~/.omp/logs/` | Log file directory |
|
|
36
|
+
| `core/agent-session.ts` | `getAuthPath` | `~/.omp/agent/auth.json` | Error messages |
|
|
37
|
+
| `core/session-manager.ts` | `getAgentDir` | `~/.omp/agent/sessions/` | Session storage |
|
|
38
|
+
| `modes/interactive/theme/theme.ts` | `getCustomThemesDir` | `~/.omp/agent/themes/` | Custom themes |
|
|
39
|
+
| `modes/interactive/interactive-mode.ts` | `getAuthPath`, `getDebugLogPath` | auth.json, debug log | Status messages |
|
|
40
|
+
| `utils/changelog.ts` | `getChangelogPath` | Package CHANGELOG.md | Re-exports |
|
|
41
|
+
| `core/system-prompt.ts` | `getAgentDir`, `getDocsPath`, `getExamplesPath`, `getReadmePath` | Package assets + AGENTS.md | System prompt building |
|
|
42
|
+
| `migrations.ts` | `getAgentDir` | `~/.omp/agent/` | Auth/session migration |
|
|
43
|
+
| `core/plugins/installer.ts` | `getAgentDir` | `~/.omp/agent/` | Plugin installation |
|
|
44
|
+
| `core/plugins/paths.ts` | `CONFIG_DIR_NAME` | `~/.omp/plugins/` | Plugin directories |
|
|
45
45
|
|
|
46
46
|
### 3. Multi-Config Discovery (with fallbacks)
|
|
47
47
|
|
|
48
48
|
These use the new helpers to check `.omp`, `.pi`, `.claude` directories:
|
|
49
49
|
|
|
50
|
-
| File
|
|
51
|
-
|
|
52
|
-
| `main.ts`
|
|
53
|
-
| `core/sdk.ts`
|
|
54
|
-
| `core/settings-manager.ts`
|
|
55
|
-
| `core/skills.ts`
|
|
56
|
-
| `core/slash-commands.ts`
|
|
57
|
-
| `core/hooks/loader.ts`
|
|
58
|
-
| `core/custom-tools/loader.ts`
|
|
59
|
-
| `core/custom-commands/loader.ts`
|
|
60
|
-
| `core/plugins/paths.ts`
|
|
61
|
-
| `core/mcp/config.ts`
|
|
62
|
-
| `core/tools/lsp/config.ts`
|
|
63
|
-
| `core/tools/task/commands.ts`
|
|
64
|
-
| `core/tools/task/discovery.ts`
|
|
65
|
-
| `core/tools/task/model-resolver.ts` | `readConfigFile`
|
|
66
|
-
| `core/tools/web-search/auth.ts`
|
|
50
|
+
| File | Helper Used | Subpath(s) | Levels |
|
|
51
|
+
| ----------------------------------- | ------------------------------------------------------ | ------------------------------------ | ------------ |
|
|
52
|
+
| `main.ts` | `findConfigFile` | `SYSTEM.md` | project |
|
|
53
|
+
| `core/sdk.ts` | `getConfigDirPaths` | `auth.json`, `models.json` | user |
|
|
54
|
+
| `core/settings-manager.ts` | `readConfigFile` | `settings.json` | user+project |
|
|
55
|
+
| `core/skills.ts` | `getConfigDirPaths` | `skills/` | user+project |
|
|
56
|
+
| `core/slash-commands.ts` | `getConfigDirPaths` | `commands/` | project |
|
|
57
|
+
| `core/hooks/loader.ts` | `getConfigDirPaths` | `hooks/` | project |
|
|
58
|
+
| `core/custom-tools/loader.ts` | `getConfigDirPaths` | `tools/` | project |
|
|
59
|
+
| `core/custom-commands/loader.ts` | `getConfigDirPaths` | `commands/` | project |
|
|
60
|
+
| `core/plugins/paths.ts` | `getConfigDirPaths` | `plugin-overrides.json` | project |
|
|
61
|
+
| `core/mcp/config.ts` | `getConfigDirPaths` | `mcp.json` | user+project |
|
|
62
|
+
| `core/tools/lsp/config.ts` | `getConfigDirPaths` | `lsp.json`, `.lsp.json` | user+project |
|
|
63
|
+
| `core/tools/task/commands.ts` | `getConfigDirPaths`, `findAllNearestProjectConfigDirs` | `commands/` | user+project |
|
|
64
|
+
| `core/tools/task/discovery.ts` | `getConfigDirs`, `findAllNearestProjectConfigDirs` | `agents/` | user+project |
|
|
65
|
+
| `core/tools/task/model-resolver.ts` | `readConfigFile` | `settings.json` | user |
|
|
66
|
+
| `core/tools/web-search/auth.ts` | `getConfigDirPaths` | `` (root for models.json, auth.json) | user |
|
|
67
67
|
|
|
68
68
|
## Subpath Summary
|
|
69
69
|
|
|
@@ -107,7 +107,7 @@ Special paths (not under agent/):
|
|
|
107
107
|
|
|
108
108
|
These files construct paths manually because they only use the primary config dir:
|
|
109
109
|
|
|
110
|
-
| File
|
|
111
|
-
|
|
112
|
-
| `core/logger.ts`
|
|
110
|
+
| File | Current Approach | Reason |
|
|
111
|
+
| ----------------------- | --------------------------------- | --------------------------------------------------- |
|
|
112
|
+
| `core/logger.ts` | `CONFIG_DIR_NAME` for logs dir | Logs only written to primary (~/.omp/logs/) |
|
|
113
113
|
| `core/plugins/paths.ts` | `CONFIG_DIR_NAME` for plugins dir | Plugins only installed in primary (~/.omp/plugins/) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "10.3.
|
|
3
|
+
"version": "10.3.2",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -79,12 +79,12 @@
|
|
|
79
79
|
"test": "bun test"
|
|
80
80
|
},
|
|
81
81
|
"dependencies": {
|
|
82
|
-
"@oh-my-pi/omp-stats": "10.3.
|
|
83
|
-
"@oh-my-pi/pi-agent-core": "10.3.
|
|
84
|
-
"@oh-my-pi/pi-ai": "10.3.
|
|
85
|
-
"@oh-my-pi/pi-natives": "10.3.
|
|
86
|
-
"@oh-my-pi/pi-tui": "10.3.
|
|
87
|
-
"@oh-my-pi/pi-utils": "10.3.
|
|
82
|
+
"@oh-my-pi/omp-stats": "10.3.2",
|
|
83
|
+
"@oh-my-pi/pi-agent-core": "10.3.2",
|
|
84
|
+
"@oh-my-pi/pi-ai": "10.3.2",
|
|
85
|
+
"@oh-my-pi/pi-natives": "10.3.2",
|
|
86
|
+
"@oh-my-pi/pi-tui": "10.3.2",
|
|
87
|
+
"@oh-my-pi/pi-utils": "10.3.2",
|
|
88
88
|
"@openai/agents": "^0.4.4",
|
|
89
89
|
"@sinclair/typebox": "^0.34.48",
|
|
90
90
|
"ajv": "^8.17.1",
|
package/src/cli/args.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
5
5
|
import chalk from "chalk";
|
|
6
|
-
import { APP_NAME, CONFIG_DIR_NAME
|
|
6
|
+
import { APP_NAME, CONFIG_DIR_NAME } from "../config";
|
|
7
7
|
import { BUILTIN_TOOLS } from "../tools";
|
|
8
8
|
|
|
9
9
|
export type Mode = "text" | "json" | "rpc";
|
|
@@ -286,7 +286,7 @@ ${chalk.bold("Environment Variables:")}
|
|
|
286
286
|
PERPLEXITY_API_KEY - Perplexity search API key
|
|
287
287
|
|
|
288
288
|
${chalk.dim("# Configuration")}
|
|
289
|
-
|
|
289
|
+
OMP_CODING_AGENT_DIR - Session storage directory (default: ~/${CONFIG_DIR_NAME}/agent)
|
|
290
290
|
|
|
291
291
|
${chalk.bold("Available Tools (all enabled by default):")}
|
|
292
292
|
read - Read file contents
|
package/src/config.ts
CHANGED
|
@@ -22,9 +22,6 @@ const priorityList = [
|
|
|
22
22
|
{ dir: ".gemini" },
|
|
23
23
|
];
|
|
24
24
|
|
|
25
|
-
// e.g., OMP_CODING_AGENT_DIR
|
|
26
|
-
export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`;
|
|
27
|
-
|
|
28
25
|
// =============================================================================
|
|
29
26
|
// Package Directory (for optional external docs/examples)
|
|
30
27
|
// =============================================================================
|
|
@@ -56,7 +53,7 @@ export function getChangelogPath(): string {
|
|
|
56
53
|
|
|
57
54
|
/** Get the agent config directory (e.g., ~/.omp/agent/) */
|
|
58
55
|
export function getAgentDir(): string {
|
|
59
|
-
return process.env
|
|
56
|
+
return process.env.OMP_CODING_AGENT_DIR || path.join(os.homedir(), CONFIG_DIR_NAME, "agent");
|
|
60
57
|
}
|
|
61
58
|
|
|
62
59
|
/** Get path to user's custom themes directory */
|
|
@@ -147,7 +147,12 @@ export interface CustomTool<TParams extends TSchema = TSchema, TDetails = any> {
|
|
|
147
147
|
renderCall?: (args: Static<TParams>, theme: Theme) => Component;
|
|
148
148
|
|
|
149
149
|
/** Custom rendering for tool result display - return a Component */
|
|
150
|
-
renderResult?: (
|
|
150
|
+
renderResult?: (
|
|
151
|
+
result: CustomToolResult<TDetails>,
|
|
152
|
+
options: RenderResultOptions,
|
|
153
|
+
theme: Theme,
|
|
154
|
+
args?: Static<TParams>,
|
|
155
|
+
) => Component;
|
|
151
156
|
}
|
|
152
157
|
|
|
153
158
|
/** Factory function that creates a custom tool or array of tools */
|
|
@@ -47,8 +47,13 @@ export class CustomToolAdapter<TParams extends TSchema = TSchema, TDetails = any
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/** Optional custom rendering for tool result display (returns UI component) */
|
|
50
|
-
renderResult(
|
|
51
|
-
|
|
50
|
+
renderResult(
|
|
51
|
+
result: AgentToolResult<TDetails>,
|
|
52
|
+
options: RenderResultOptions,
|
|
53
|
+
theme: TTheme,
|
|
54
|
+
args?: Static<TParams>,
|
|
55
|
+
): Component | undefined {
|
|
56
|
+
return this.tool.renderResult?.(result, options, theme, args);
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
/**
|
|
@@ -255,7 +255,12 @@ export interface ToolDefinition<TParams extends TSchema = TSchema, TDetails = un
|
|
|
255
255
|
renderCall?: (args: Static<TParams>, theme: Theme) => Component;
|
|
256
256
|
|
|
257
257
|
/** Custom rendering for tool result display */
|
|
258
|
-
renderResult?: (
|
|
258
|
+
renderResult?: (
|
|
259
|
+
result: AgentToolResult<TDetails>,
|
|
260
|
+
options: ToolRenderResultOptions,
|
|
261
|
+
theme: Theme,
|
|
262
|
+
args?: Static<TParams>,
|
|
263
|
+
) => Component;
|
|
259
264
|
}
|
|
260
265
|
|
|
261
266
|
// ============================================================================
|
|
@@ -42,11 +42,12 @@ export class RegisteredToolAdapter implements AgentTool<any, any, any> {
|
|
|
42
42
|
return this.registeredTool.definition.renderCall?.(args, theme as Theme);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
renderResult?(result: any, options: any, theme: any) {
|
|
45
|
+
renderResult?(result: any, options: any, theme: any, args?: any) {
|
|
46
46
|
return this.registeredTool.definition.renderResult?.(
|
|
47
47
|
result,
|
|
48
48
|
{ expanded: options.expanded, isPartial: options.isPartial, spinnerFrame: options.spinnerFrame },
|
|
49
49
|
theme as Theme,
|
|
50
|
+
args,
|
|
50
51
|
);
|
|
51
52
|
}
|
|
52
53
|
}
|
|
@@ -82,8 +83,8 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
82
83
|
private tool: AgentTool<TParameters, TDetails>,
|
|
83
84
|
private runner: ExtensionRunner,
|
|
84
85
|
) {
|
|
85
|
-
this.renderCall = tool.renderCall;
|
|
86
|
-
this.renderResult = tool.renderResult;
|
|
86
|
+
this.renderCall = tool.renderCall?.bind(tool);
|
|
87
|
+
this.renderResult = tool.renderResult?.bind(tool);
|
|
87
88
|
this.mergeCallAndResult = (tool as { mergeCallAndResult?: boolean }).mergeCallAndResult;
|
|
88
89
|
this.inline = (tool as { inline?: boolean }).inline;
|
|
89
90
|
}
|
|
@@ -34,8 +34,8 @@ export class HookToolWrapper<TParameters extends TSchema = TSchema, TDetails = u
|
|
|
34
34
|
this.label = tool.label ?? "";
|
|
35
35
|
this.description = tool.description;
|
|
36
36
|
this.parameters = tool.parameters;
|
|
37
|
-
this.renderCall = tool.renderCall;
|
|
38
|
-
this.renderResult = tool.renderResult;
|
|
37
|
+
this.renderCall = tool.renderCall?.bind(tool);
|
|
38
|
+
this.renderResult = tool.renderResult?.bind(tool);
|
|
39
39
|
this.mergeCallAndResult = (tool as { mergeCallAndResult?: boolean }).mergeCallAndResult;
|
|
40
40
|
this.inline = (tool as { inline?: boolean }).inline;
|
|
41
41
|
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI rendering for MCP tools.
|
|
3
|
+
*
|
|
4
|
+
* Provides structured display of MCP tool calls and results,
|
|
5
|
+
* showing args and output in JSON tree format similar to task tool.
|
|
6
|
+
*/
|
|
7
|
+
import type { Component } from "@oh-my-pi/pi-tui";
|
|
8
|
+
import { Text } from "@oh-my-pi/pi-tui";
|
|
9
|
+
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
10
|
+
import type { Theme } from "../modes/theme/theme";
|
|
11
|
+
import { formatExpandHint, truncateToWidth } from "../tools/render-utils";
|
|
12
|
+
import { renderStatusLine } from "../tui";
|
|
13
|
+
import type { MCPToolDetails } from "./tool-bridge";
|
|
14
|
+
|
|
15
|
+
/** Max depth for JSON tree rendering */
|
|
16
|
+
const JSON_TREE_MAX_DEPTH_COLLAPSED = 2;
|
|
17
|
+
const JSON_TREE_MAX_DEPTH_EXPANDED = 6;
|
|
18
|
+
const JSON_TREE_MAX_LINES_COLLAPSED = 6;
|
|
19
|
+
const JSON_TREE_MAX_LINES_EXPANDED = 200;
|
|
20
|
+
const JSON_TREE_SCALAR_LEN_COLLAPSED = 60;
|
|
21
|
+
const JSON_TREE_SCALAR_LEN_EXPANDED = 2000;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Format a scalar value for inline display.
|
|
25
|
+
*/
|
|
26
|
+
function formatScalar(value: unknown, maxLen: number): string {
|
|
27
|
+
if (value === null) return "null";
|
|
28
|
+
if (value === undefined) return "undefined";
|
|
29
|
+
if (typeof value === "boolean") return String(value);
|
|
30
|
+
if (typeof value === "number") return String(value);
|
|
31
|
+
if (typeof value === "string") {
|
|
32
|
+
const escaped = value.replace(/\n/g, "\\n").replace(/\t/g, "\\t");
|
|
33
|
+
const truncated = truncateToWidth(escaped, maxLen);
|
|
34
|
+
return `"${truncated}"`;
|
|
35
|
+
}
|
|
36
|
+
if (Array.isArray(value)) return `[${value.length} items]`;
|
|
37
|
+
if (typeof value === "object") {
|
|
38
|
+
const keys = Object.keys(value);
|
|
39
|
+
return `{${keys.length} keys}`;
|
|
40
|
+
}
|
|
41
|
+
return String(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Format args inline for collapsed view.
|
|
46
|
+
*/
|
|
47
|
+
function formatArgsInline(args: Record<string, unknown>, maxWidth: number): string {
|
|
48
|
+
const entries = Object.entries(args);
|
|
49
|
+
if (entries.length === 0) return "";
|
|
50
|
+
|
|
51
|
+
// Single arg: show key=value
|
|
52
|
+
if (entries.length === 1) {
|
|
53
|
+
const [key, value] = entries[0];
|
|
54
|
+
return `${key}=${formatScalar(value, maxWidth - key.length - 1)}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Multiple args: show key=value, key=value...
|
|
58
|
+
const pairs: string[] = [];
|
|
59
|
+
let totalLen = 0;
|
|
60
|
+
|
|
61
|
+
for (const [key, value] of entries) {
|
|
62
|
+
const valueStr = formatScalar(value, 24);
|
|
63
|
+
const pairStr = `${key}=${valueStr}`;
|
|
64
|
+
const addLen = pairs.length > 0 ? pairStr.length + 2 : pairStr.length;
|
|
65
|
+
|
|
66
|
+
if (totalLen + addLen > maxWidth && pairs.length > 0) {
|
|
67
|
+
pairs.push("…");
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
pairs.push(pairStr);
|
|
72
|
+
totalLen += addLen;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return pairs.join(", ");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Build tree prefix for nested rendering.
|
|
80
|
+
*/
|
|
81
|
+
function buildTreePrefix(ancestors: boolean[], theme: Theme): string {
|
|
82
|
+
return ancestors.map(hasNext => (hasNext ? `${theme.tree.vertical} ` : " ")).join("");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Render a JSON value as tree lines.
|
|
87
|
+
*/
|
|
88
|
+
function renderJsonTreeLines(
|
|
89
|
+
value: unknown,
|
|
90
|
+
theme: Theme,
|
|
91
|
+
maxDepth: number,
|
|
92
|
+
maxLines: number,
|
|
93
|
+
maxScalarLen: number,
|
|
94
|
+
): { lines: string[]; truncated: boolean } {
|
|
95
|
+
const lines: string[] = [];
|
|
96
|
+
let truncated = false;
|
|
97
|
+
|
|
98
|
+
const iconObject = theme.styledSymbol("icon.folder", "muted");
|
|
99
|
+
const iconArray = theme.styledSymbol("icon.package", "muted");
|
|
100
|
+
const iconScalar = theme.styledSymbol("icon.file", "muted");
|
|
101
|
+
|
|
102
|
+
const pushLine = (line: string): boolean => {
|
|
103
|
+
if (lines.length >= maxLines) {
|
|
104
|
+
truncated = true;
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
lines.push(line);
|
|
108
|
+
return true;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const renderNode = (val: unknown, key: string | undefined, ancestors: boolean[], isLast: boolean, depth: number) => {
|
|
112
|
+
if (lines.length >= maxLines) {
|
|
113
|
+
truncated = true;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const connector = isLast ? theme.tree.last : theme.tree.branch;
|
|
118
|
+
const prefix = `${buildTreePrefix(ancestors, theme)}${theme.fg("dim", connector)} `;
|
|
119
|
+
|
|
120
|
+
// Handle scalars
|
|
121
|
+
if (val === null || val === undefined || typeof val !== "object") {
|
|
122
|
+
const label = key ? theme.fg("muted", key) : theme.fg("muted", "value");
|
|
123
|
+
|
|
124
|
+
// Special handling for multiline strings
|
|
125
|
+
if (typeof val === "string" && val.includes("\n")) {
|
|
126
|
+
const strLines = val.split("\n");
|
|
127
|
+
const maxStrLines = Math.min(strLines.length, Math.max(1, maxLines - lines.length - 1));
|
|
128
|
+
const continuePrefix = buildTreePrefix([...ancestors, !isLast], theme);
|
|
129
|
+
|
|
130
|
+
// First line with label
|
|
131
|
+
const firstLine = truncateToWidth(strLines[0], maxScalarLen);
|
|
132
|
+
pushLine(`${prefix}${iconScalar} ${label}: ${theme.fg("dim", `"${firstLine}`)}`);
|
|
133
|
+
|
|
134
|
+
// Subsequent lines indented
|
|
135
|
+
for (let i = 1; i < maxStrLines; i++) {
|
|
136
|
+
if (lines.length >= maxLines) {
|
|
137
|
+
truncated = true;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
const line = truncateToWidth(strLines[i], maxScalarLen);
|
|
141
|
+
pushLine(`${continuePrefix} ${theme.fg("dim", ` ${line}`)}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Show truncation and closing quote
|
|
145
|
+
if (strLines.length > maxStrLines) {
|
|
146
|
+
truncated = true;
|
|
147
|
+
pushLine(`${continuePrefix} ${theme.fg("dim", ` …(${strLines.length - maxStrLines} more lines)"`)}`);
|
|
148
|
+
} else {
|
|
149
|
+
// Add closing quote to last line - need to modify the last pushed line
|
|
150
|
+
const lastIdx = lines.length - 1;
|
|
151
|
+
lines[lastIdx] = `${lines[lastIdx]}${theme.fg("dim", '"')}`;
|
|
152
|
+
}
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const scalar = formatScalar(val, maxScalarLen);
|
|
157
|
+
pushLine(`${prefix}${iconScalar} ${label}: ${theme.fg("dim", scalar)}`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Handle arrays
|
|
162
|
+
if (Array.isArray(val)) {
|
|
163
|
+
const header = key ? theme.fg("muted", key) : theme.fg("muted", "array");
|
|
164
|
+
pushLine(`${prefix}${iconArray} ${header}`);
|
|
165
|
+
if (val.length === 0) {
|
|
166
|
+
pushLine(
|
|
167
|
+
`${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", "[]")}`,
|
|
168
|
+
);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (depth >= maxDepth) {
|
|
172
|
+
pushLine(
|
|
173
|
+
`${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", "…")}`,
|
|
174
|
+
);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const nextAncestors = [...ancestors, !isLast];
|
|
178
|
+
for (let i = 0; i < val.length; i++) {
|
|
179
|
+
renderNode(val[i], `[${i}]`, nextAncestors, i === val.length - 1, depth + 1);
|
|
180
|
+
if (lines.length >= maxLines) {
|
|
181
|
+
truncated = true;
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Handle objects
|
|
189
|
+
const header = key ? theme.fg("muted", key) : theme.fg("muted", "object");
|
|
190
|
+
pushLine(`${prefix}${iconObject} ${header}`);
|
|
191
|
+
const entries = Object.entries(val as Record<string, unknown>);
|
|
192
|
+
if (entries.length === 0) {
|
|
193
|
+
pushLine(
|
|
194
|
+
`${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", "{}")}`,
|
|
195
|
+
);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (depth >= maxDepth) {
|
|
199
|
+
pushLine(
|
|
200
|
+
`${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", "…")}`,
|
|
201
|
+
);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const nextAncestors = [...ancestors, !isLast];
|
|
205
|
+
for (let i = 0; i < entries.length; i++) {
|
|
206
|
+
const [childKey, child] = entries[i];
|
|
207
|
+
renderNode(child, childKey, nextAncestors, i === entries.length - 1, depth + 1);
|
|
208
|
+
if (lines.length >= maxLines) {
|
|
209
|
+
truncated = true;
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// Render root level
|
|
216
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
217
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
218
|
+
for (let i = 0; i < entries.length; i++) {
|
|
219
|
+
const [childKey, child] = entries[i];
|
|
220
|
+
renderNode(child, childKey, [], i === entries.length - 1, 1);
|
|
221
|
+
if (lines.length >= maxLines) {
|
|
222
|
+
truncated = true;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} else if (Array.isArray(value)) {
|
|
227
|
+
for (let i = 0; i < value.length; i++) {
|
|
228
|
+
renderNode(value[i], `[${i}]`, [], i === value.length - 1, 1);
|
|
229
|
+
if (lines.length >= maxLines) {
|
|
230
|
+
truncated = true;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
renderNode(value, undefined, [], true, 0);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return { lines, truncated };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Render MCP tool call.
|
|
243
|
+
*/
|
|
244
|
+
export function renderMCPCall(args: Record<string, unknown>, theme: Theme, label: string): Component {
|
|
245
|
+
const lines: string[] = [];
|
|
246
|
+
lines.push(renderStatusLine({ icon: "pending", title: label }, theme));
|
|
247
|
+
|
|
248
|
+
if (args && typeof args === "object" && Object.keys(args).length > 0) {
|
|
249
|
+
// Show args inline preview
|
|
250
|
+
const preview = formatArgsInline(args, 70);
|
|
251
|
+
if (preview) {
|
|
252
|
+
lines.push(` ${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", preview)}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Render MCP tool result.
|
|
261
|
+
*/
|
|
262
|
+
export function renderMCPResult(
|
|
263
|
+
result: { content: Array<{ type: string; text?: string }>; details?: MCPToolDetails; isError?: boolean },
|
|
264
|
+
options: RenderResultOptions,
|
|
265
|
+
theme: Theme,
|
|
266
|
+
args?: Record<string, unknown>,
|
|
267
|
+
): Component {
|
|
268
|
+
const { expanded } = options;
|
|
269
|
+
const lines: string[] = [];
|
|
270
|
+
|
|
271
|
+
// Args section (when expanded)
|
|
272
|
+
if (expanded && args && typeof args === "object" && Object.keys(args).length > 0) {
|
|
273
|
+
lines.push(`${theme.fg("dim", "Args")}`);
|
|
274
|
+
const maxDepth = JSON_TREE_MAX_DEPTH_EXPANDED;
|
|
275
|
+
const maxLines = JSON_TREE_MAX_LINES_EXPANDED;
|
|
276
|
+
const tree = renderJsonTreeLines(args, theme, maxDepth, maxLines, JSON_TREE_SCALAR_LEN_EXPANDED);
|
|
277
|
+
for (const line of tree.lines) {
|
|
278
|
+
lines.push(line);
|
|
279
|
+
}
|
|
280
|
+
if (tree.truncated) {
|
|
281
|
+
lines.push(theme.fg("dim", "…"));
|
|
282
|
+
}
|
|
283
|
+
lines.push(""); // Blank line before output
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Output section
|
|
287
|
+
const textContent = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
288
|
+
const trimmedOutput = textContent.trimEnd();
|
|
289
|
+
|
|
290
|
+
if (!trimmedOutput) {
|
|
291
|
+
lines.push(theme.fg("dim", "(no output)"));
|
|
292
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Try to parse as JSON for structured display
|
|
296
|
+
if (trimmedOutput.startsWith("{") || trimmedOutput.startsWith("[")) {
|
|
297
|
+
try {
|
|
298
|
+
const parsed = JSON.parse(trimmedOutput);
|
|
299
|
+
const maxDepth = expanded ? JSON_TREE_MAX_DEPTH_EXPANDED : JSON_TREE_MAX_DEPTH_COLLAPSED;
|
|
300
|
+
const maxLines = expanded ? JSON_TREE_MAX_LINES_EXPANDED : JSON_TREE_MAX_LINES_COLLAPSED;
|
|
301
|
+
const maxScalarLen = expanded ? JSON_TREE_SCALAR_LEN_EXPANDED : JSON_TREE_SCALAR_LEN_COLLAPSED;
|
|
302
|
+
const tree = renderJsonTreeLines(parsed, theme, maxDepth, maxLines, maxScalarLen);
|
|
303
|
+
|
|
304
|
+
if (tree.lines.length > 0) {
|
|
305
|
+
for (const line of tree.lines) {
|
|
306
|
+
lines.push(line);
|
|
307
|
+
}
|
|
308
|
+
// Always show expand hint when collapsed (expanded view shows longer values and deeper nesting)
|
|
309
|
+
if (!expanded) {
|
|
310
|
+
lines.push(formatExpandHint(theme, expanded, true));
|
|
311
|
+
} else if (tree.truncated) {
|
|
312
|
+
lines.push(theme.fg("dim", "…"));
|
|
313
|
+
}
|
|
314
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
315
|
+
}
|
|
316
|
+
} catch {
|
|
317
|
+
// Fall through to raw output
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Raw text output
|
|
322
|
+
const outputLines = trimmedOutput.split("\n");
|
|
323
|
+
const maxOutputLines = expanded ? 12 : 4;
|
|
324
|
+
const displayLines = outputLines.slice(0, maxOutputLines);
|
|
325
|
+
|
|
326
|
+
for (const line of displayLines) {
|
|
327
|
+
lines.push(theme.fg("toolOutput", truncateToWidth(line, 80)));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (outputLines.length > maxOutputLines) {
|
|
331
|
+
const remaining = outputLines.length - maxOutputLines;
|
|
332
|
+
lines.push(`${theme.fg("dim", `… ${remaining} more lines`)} ${formatExpandHint(theme, expanded, true)}`);
|
|
333
|
+
} else if (!expanded) {
|
|
334
|
+
// Show expand hint when collapsed even if all lines shown (lines may be truncated)
|
|
335
|
+
lines.push(formatExpandHint(theme, expanded, true));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
339
|
+
}
|
package/src/mcp/tool-bridge.ts
CHANGED
|
@@ -6,8 +6,15 @@
|
|
|
6
6
|
import type { AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
7
7
|
import type { TSchema } from "@sinclair/typebox";
|
|
8
8
|
import type { SourceMeta } from "../capability/types";
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
CustomTool,
|
|
11
|
+
CustomToolContext,
|
|
12
|
+
CustomToolResult,
|
|
13
|
+
RenderResultOptions,
|
|
14
|
+
} from "../extensibility/custom-tools/types";
|
|
15
|
+
import type { Theme } from "../modes/theme/theme";
|
|
10
16
|
import { callTool } from "./client";
|
|
17
|
+
import { renderMCPCall, renderMCPResult } from "./render";
|
|
11
18
|
import type { MCPContent, MCPServerConnection, MCPToolDefinition } from "./types";
|
|
12
19
|
|
|
13
20
|
/** Details included in MCP tool results for rendering */
|
|
@@ -135,6 +142,14 @@ export class MCPTool implements CustomTool<TSchema, MCPToolDetails> {
|
|
|
135
142
|
this.mcpServerName = connection.name;
|
|
136
143
|
}
|
|
137
144
|
|
|
145
|
+
renderCall(args: unknown, theme: Theme) {
|
|
146
|
+
return renderMCPCall((args ?? {}) as Record<string, unknown>, theme, this.label);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
renderResult(result: CustomToolResult<MCPToolDetails>, options: RenderResultOptions, theme: Theme, args?: unknown) {
|
|
150
|
+
return renderMCPResult(result, options, theme, (args ?? {}) as Record<string, unknown>);
|
|
151
|
+
}
|
|
152
|
+
|
|
138
153
|
async execute(
|
|
139
154
|
_toolCallId: string,
|
|
140
155
|
params: unknown,
|
|
@@ -223,6 +238,14 @@ export class DeferredMCPTool implements CustomTool<TSchema, MCPToolDetails> {
|
|
|
223
238
|
this.fallbackProviderName = source?.providerName;
|
|
224
239
|
}
|
|
225
240
|
|
|
241
|
+
renderCall(args: unknown, theme: Theme) {
|
|
242
|
+
return renderMCPCall((args ?? {}) as Record<string, unknown>, theme, this.label);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
renderResult(result: CustomToolResult<MCPToolDetails>, options: RenderResultOptions, theme: Theme, args?: unknown) {
|
|
246
|
+
return renderMCPResult(result, options, theme, (args ?? {}) as Record<string, unknown>);
|
|
247
|
+
}
|
|
248
|
+
|
|
226
249
|
async execute(
|
|
227
250
|
_toolCallId: string,
|
|
228
251
|
params: unknown,
|
package/src/sdk.ts
CHANGED
|
@@ -33,7 +33,6 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
33
33
|
import { logger, postmortem } from "@oh-my-pi/pi-utils";
|
|
34
34
|
import { YAML } from "bun";
|
|
35
35
|
import chalk from "chalk";
|
|
36
|
-
// Import discovery to register all providers on startup
|
|
37
36
|
import { loadCapability } from "./capability";
|
|
38
37
|
import { type Rule, ruleCapability } from "./capability/rule";
|
|
39
38
|
import { getAgentDir, getConfigDirPaths } from "./config";
|