@oh-my-pi/pi-coding-agent 1.338.0 → 1.341.0
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 +60 -1
- package/package.json +3 -3
- package/src/cli/args.ts +8 -0
- package/src/core/agent-session.ts +32 -14
- package/src/core/export-html/index.ts +48 -15
- package/src/core/export-html/template.html +3 -11
- package/src/core/mcp/client.ts +43 -16
- package/src/core/mcp/config.ts +152 -6
- package/src/core/mcp/index.ts +6 -2
- package/src/core/mcp/loader.ts +30 -3
- package/src/core/mcp/manager.ts +69 -10
- package/src/core/mcp/types.ts +9 -3
- package/src/core/model-resolver.ts +101 -0
- package/src/core/sdk.ts +65 -18
- package/src/core/session-manager.ts +117 -14
- package/src/core/settings-manager.ts +107 -19
- package/src/core/title-generator.ts +94 -0
- package/src/core/tools/bash.ts +1 -2
- package/src/core/tools/edit-diff.ts +2 -2
- package/src/core/tools/edit.ts +43 -5
- package/src/core/tools/grep.ts +3 -2
- package/src/core/tools/index.ts +73 -13
- package/src/core/tools/lsp/client.ts +45 -20
- package/src/core/tools/lsp/config.ts +708 -34
- package/src/core/tools/lsp/index.ts +423 -23
- package/src/core/tools/lsp/types.ts +5 -0
- package/src/core/tools/task/bundled-agents/explore.md +1 -1
- package/src/core/tools/task/bundled-agents/reviewer.md +1 -1
- package/src/core/tools/task/model-resolver.ts +52 -3
- package/src/core/tools/write.ts +67 -4
- package/src/index.ts +5 -0
- package/src/main.ts +23 -2
- package/src/modes/interactive/components/model-selector.ts +96 -18
- package/src/modes/interactive/components/session-selector.ts +20 -7
- package/src/modes/interactive/components/settings-defs.ts +59 -2
- package/src/modes/interactive/components/settings-selector.ts +8 -11
- package/src/modes/interactive/components/tool-execution.ts +18 -0
- package/src/modes/interactive/components/tree-selector.ts +2 -2
- package/src/modes/interactive/components/welcome.ts +40 -3
- package/src/modes/interactive/interactive-mode.ts +87 -10
- package/src/core/export-html/vendor/highlight.min.js +0 -1213
- package/src/core/export-html/vendor/marked.min.js +0 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,64 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [1.341.0] - 2026-01-03
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Added interruptMode setting to control when queued messages are processed during tool execution.
|
|
9
|
+
- Implemented getter and setter methods in SettingsManager for interrupt mode persistence.
|
|
10
|
+
- Exposed interruptMode configuration in interactive settings UI with immediate/wait options.
|
|
11
|
+
- Wired interrupt mode through AgentSession and SDK to enable runtime configuration.
|
|
12
|
+
- Model roles: Configure different models for different purposes (default, smol, slow) via `/model` selector
|
|
13
|
+
- Model selector key bindings: Enter sets default, S sets smol, L sets slow, Escape closes
|
|
14
|
+
- Model selector shows role markers: ✓ for default, ⚡ for smol, 🧠 for slow
|
|
15
|
+
- `pi/<role>` model aliases in Task tool agent definitions (e.g., `model: pi/smol, haiku, flash, mini`)
|
|
16
|
+
- Smol model auto-discovery using priority chain: haiku > flash > mini
|
|
17
|
+
- Slow model auto-discovery using priority chain: gpt-5.2-codex > codex > gpt > opus > pro
|
|
18
|
+
- CLI args for model roles: `--smol <model>` and `--slow <model>` (ephemeral, not persisted)
|
|
19
|
+
- Env var overrides: `PI_SMOL_MODEL` and `PI_SLOW_MODEL`
|
|
20
|
+
- Title generation now uses configured smol model from settings
|
|
21
|
+
- LSP diagnostics on edit: Edit tool can now return LSP diagnostics after editing code files. Disabled by default to avoid noise during multi-edit sequences. Enable via `lsp.diagnosticsOnEdit` setting.
|
|
22
|
+
- LSP workspace diagnostics: New `lsp action=workspace_diagnostics` command checks the entire project for errors. Auto-detects project type and uses appropriate checker (rust-analyzer/cargo for Rust, tsc for TypeScript, go build for Go, pyright for Python).
|
|
23
|
+
- LSP local binary resolution: LSP servers installed in project-local directories are now discovered automatically. Checks `node_modules/.bin/` for Node.js projects, `.venv/bin/`/`venv/bin/` for Python projects, and `vendor/bundle/bin/` for Ruby projects before falling back to `$PATH`.
|
|
24
|
+
- LSP format on write: Write tool now automatically formats code files using LSP after writing. Uses the language server's built-in formatter (e.g., rustfmt for Rust, gofmt for Go). Controlled via `lsp.formatOnWrite` setting (enabled by default).
|
|
25
|
+
- LSP diagnostics on write: Write tool now returns LSP diagnostics (errors/warnings) after writing code files. This gives immediate feedback on syntax errors and type issues. Controlled via `lsp.diagnosticsOnWrite` setting (enabled by default).
|
|
26
|
+
- LSP server warmup at startup: LSP servers are now started at launch to avoid cold-start delays when first writing files.
|
|
27
|
+
- LSP server status in welcome banner: Shows which language servers are active and ready.
|
|
28
|
+
- Edit fuzzy match setting: Added `edit.fuzzyMatch` setting (enabled by default) to control whether the edit tool accepts high-confidence fuzzy matches for whitespace/indentation differences. Toggle via `/settings`.
|
|
29
|
+
- Multi-server LSP diagnostics: Diagnostics now query all applicable language servers for a file type. For TypeScript/JavaScript projects with Biome, this means both type errors (from tsserver) and lint errors (from Biome) are reported together.
|
|
30
|
+
- Comprehensive LSP server configurations for 40+ languages including Rust, Go, Python, Java, Kotlin, Scala, Haskell, OCaml, Elixir, Ruby, PHP, C#, Lua, Nix, and many more. Each server includes sensible defaults for args, settings, and init options.
|
|
31
|
+
- Extended LSP config file search paths: Now searches for `lsp.json`, `.lsp.json` in project root and `.pi/` subdirectory, plus user-level configs in `~/.pi/` and home directory.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- LSP settings moved to dedicated "LSP" tab in `/settings` for better organization
|
|
36
|
+
- Improved grep tool description to document pagination options (`headLimit`, `offset`) and clarify recursive search behavior
|
|
37
|
+
- LSP idle timeout now disabled by default. Configure via `idleTimeoutMs` in lsp.json to auto-shutdown inactive servers.
|
|
38
|
+
- Model settings now use role-based storage (`modelRoles` map) instead of single `defaultProvider`/`defaultModel` fields. Supports multiple model roles (default, small, etc.)
|
|
39
|
+
- Session model persistence now uses `"provider/modelId"` string format with optional role field
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- Recent sessions now show in welcome banner (was never wired up).
|
|
44
|
+
- Auto-generated session titles: Sessions are now automatically titled based on the first message using a small model (Haiku/GPT-4o-mini/Flash). Titles are shown in the terminal window title, recent sessions list, and --resume picker. The resume picker shows title with dimmed first message preview below.
|
|
45
|
+
|
|
46
|
+
## [1.340.0] - 2026-01-03
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- Replaced vendored highlight.js and marked.js with CDN-hosted versions for smaller exports
|
|
51
|
+
- Added runtime minification for HTML, CSS, and JS in session exports
|
|
52
|
+
- Session share URL now uses gistpreview.github.io instead of shittycodingagent.ai
|
|
53
|
+
|
|
54
|
+
## [1.339.0] - 2026-01-03
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
|
|
58
|
+
- MCP project config setting to disable loading `.mcp.json`/`mcp.json` from project root
|
|
59
|
+
- Support for both `mcp.json` and `.mcp.json` filenames (prefers `mcp.json` if both exist)
|
|
60
|
+
- Automatic Exa MCP server filtering with API key extraction for native integration
|
|
61
|
+
|
|
3
62
|
## [1.338.0] - 2026-01-03
|
|
4
63
|
|
|
5
64
|
### Added
|
|
@@ -1268,4 +1327,4 @@ Initial public release.
|
|
|
1268
1327
|
- Git branch display in footer
|
|
1269
1328
|
- Message queueing during streaming responses
|
|
1270
1329
|
- OAuth integration for Gmail and Google Calendar access
|
|
1271
|
-
- HTML export with syntax highlighting and collapsible sections
|
|
1330
|
+
- HTML export with syntax highlighting and collapsible sections
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.341.0",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"piConfig": {
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"clean": "rm -rf dist",
|
|
34
34
|
"build": "tsgo -p tsconfig.build.json && chmod +x dist/cli.js && npm run copy-assets",
|
|
35
35
|
"build:binary": "npm run build && bun build --compile ./dist/cli.js --outfile dist/pi && npm run copy-binary-assets",
|
|
36
|
-
"copy-assets": "mkdir -p dist/modes/interactive/theme && cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && mkdir -p dist/core/export-html
|
|
37
|
-
"copy-binary-assets": "cp package.json dist/ && cp README.md dist/ && cp CHANGELOG.md dist/ && mkdir -p dist/theme && cp src/modes/interactive/theme/*.json dist/theme/ && mkdir -p dist/export-html
|
|
36
|
+
"copy-assets": "mkdir -p dist/modes/interactive/theme && cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && mkdir -p dist/core/export-html && cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/core/export-html/",
|
|
37
|
+
"copy-binary-assets": "cp package.json dist/ && cp README.md dist/ && cp CHANGELOG.md dist/ && mkdir -p dist/theme && cp src/modes/interactive/theme/*.json dist/theme/ && mkdir -p dist/export-html && cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/export-html/ && cp -r docs dist/ && cp -r examples dist/",
|
|
38
38
|
"test": "vitest --run",
|
|
39
39
|
"prepublishOnly": "npm run clean && npm run build"
|
|
40
40
|
},
|
package/src/cli/args.ts
CHANGED
|
@@ -12,6 +12,8 @@ export type Mode = "text" | "json" | "rpc";
|
|
|
12
12
|
export interface Args {
|
|
13
13
|
provider?: string;
|
|
14
14
|
model?: string;
|
|
15
|
+
smol?: string;
|
|
16
|
+
slow?: string;
|
|
15
17
|
apiKey?: string;
|
|
16
18
|
systemPrompt?: string;
|
|
17
19
|
appendSystemPrompt?: string;
|
|
@@ -69,6 +71,10 @@ export function parseArgs(args: string[]): Args {
|
|
|
69
71
|
result.provider = args[++i];
|
|
70
72
|
} else if (arg === "--model" && i + 1 < args.length) {
|
|
71
73
|
result.model = args[++i];
|
|
74
|
+
} else if (arg === "--smol" && i + 1 < args.length) {
|
|
75
|
+
result.smol = args[++i];
|
|
76
|
+
} else if (arg === "--slow" && i + 1 < args.length) {
|
|
77
|
+
result.slow = args[++i];
|
|
72
78
|
} else if (arg === "--api-key" && i + 1 < args.length) {
|
|
73
79
|
result.apiKey = args[++i];
|
|
74
80
|
} else if (arg === "--system-prompt" && i + 1 < args.length) {
|
|
@@ -148,6 +154,8 @@ ${chalk.bold("Usage:")}
|
|
|
148
154
|
${chalk.bold("Options:")}
|
|
149
155
|
--provider <name> Provider name (default: google)
|
|
150
156
|
--model <id> Model ID (default: gemini-2.5-flash)
|
|
157
|
+
--smol <id> Smol/fast model for lightweight tasks (or PI_SMOL_MODEL env)
|
|
158
|
+
--slow <id> Slow/reasoning model for thorough analysis (or PI_SLOW_MODEL env)
|
|
151
159
|
--api-key <key> API key (defaults to env vars)
|
|
152
160
|
--system-prompt <text> System prompt (default: coding assistant prompt)
|
|
153
161
|
--append-system-prompt <text> Append text or file contents to the system prompt
|
|
@@ -424,6 +424,11 @@ export class AgentSession {
|
|
|
424
424
|
return this.agent.getQueueMode();
|
|
425
425
|
}
|
|
426
426
|
|
|
427
|
+
/** Current interrupt mode */
|
|
428
|
+
get interruptMode(): "immediate" | "wait" {
|
|
429
|
+
return this.agent.getInterruptMode();
|
|
430
|
+
}
|
|
431
|
+
|
|
427
432
|
/** Current session file path, or undefined if sessions are disabled */
|
|
428
433
|
get sessionFile(): string | undefined {
|
|
429
434
|
return this.sessionManager.getSessionFile();
|
|
@@ -701,15 +706,15 @@ export class AgentSession {
|
|
|
701
706
|
* Validates API key, saves to session and settings.
|
|
702
707
|
* @throws Error if no API key available for the model
|
|
703
708
|
*/
|
|
704
|
-
async setModel(model: Model<any
|
|
709
|
+
async setModel(model: Model<any>, role: string = "default"): Promise<void> {
|
|
705
710
|
const apiKey = await this._modelRegistry.getApiKey(model);
|
|
706
711
|
if (!apiKey) {
|
|
707
712
|
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
708
713
|
}
|
|
709
714
|
|
|
710
715
|
this.agent.setModel(model);
|
|
711
|
-
this.sessionManager.appendModelChange(model.provider
|
|
712
|
-
this.settingsManager.
|
|
716
|
+
this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, role);
|
|
717
|
+
this.settingsManager.setModelRole(role, `${model.provider}/${model.id}`);
|
|
713
718
|
|
|
714
719
|
// Re-clamp thinking level for new model's capabilities
|
|
715
720
|
this.setThinkingLevel(this.thinkingLevel);
|
|
@@ -747,8 +752,8 @@ export class AgentSession {
|
|
|
747
752
|
|
|
748
753
|
// Apply model
|
|
749
754
|
this.agent.setModel(next.model);
|
|
750
|
-
this.sessionManager.appendModelChange(next.model.provider
|
|
751
|
-
this.settingsManager.
|
|
755
|
+
this.sessionManager.appendModelChange(`${next.model.provider}/${next.model.id}`);
|
|
756
|
+
this.settingsManager.setModelRole("default", `${next.model.provider}/${next.model.id}`);
|
|
752
757
|
|
|
753
758
|
// Apply thinking level (setThinkingLevel clamps to model capabilities)
|
|
754
759
|
this.setThinkingLevel(next.thinkingLevel);
|
|
@@ -774,8 +779,8 @@ export class AgentSession {
|
|
|
774
779
|
}
|
|
775
780
|
|
|
776
781
|
this.agent.setModel(nextModel);
|
|
777
|
-
this.sessionManager.appendModelChange(nextModel.provider
|
|
778
|
-
this.settingsManager.
|
|
782
|
+
this.sessionManager.appendModelChange(`${nextModel.provider}/${nextModel.id}`);
|
|
783
|
+
this.settingsManager.setModelRole("default", `${nextModel.provider}/${nextModel.id}`);
|
|
779
784
|
|
|
780
785
|
// Re-clamp thinking level for new model's capabilities
|
|
781
786
|
this.setThinkingLevel(this.thinkingLevel);
|
|
@@ -861,6 +866,15 @@ export class AgentSession {
|
|
|
861
866
|
this.settingsManager.setQueueMode(mode);
|
|
862
867
|
}
|
|
863
868
|
|
|
869
|
+
/**
|
|
870
|
+
* Set interrupt mode.
|
|
871
|
+
* Saves to settings.
|
|
872
|
+
*/
|
|
873
|
+
setInterruptMode(mode: "immediate" | "wait"): void {
|
|
874
|
+
this.agent.setInterruptMode(mode);
|
|
875
|
+
this.settingsManager.setInterruptMode(mode);
|
|
876
|
+
}
|
|
877
|
+
|
|
864
878
|
// =========================================================================
|
|
865
879
|
// Compaction
|
|
866
880
|
// =========================================================================
|
|
@@ -1472,13 +1486,17 @@ export class AgentSession {
|
|
|
1472
1486
|
this.agent.replaceMessages(sessionContext.messages);
|
|
1473
1487
|
|
|
1474
1488
|
// Restore model if saved
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
const
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
this.
|
|
1489
|
+
const defaultModelStr = sessionContext.models.default;
|
|
1490
|
+
if (defaultModelStr) {
|
|
1491
|
+
const slashIdx = defaultModelStr.indexOf("/");
|
|
1492
|
+
if (slashIdx > 0) {
|
|
1493
|
+
const provider = defaultModelStr.slice(0, slashIdx);
|
|
1494
|
+
const modelId = defaultModelStr.slice(slashIdx + 1);
|
|
1495
|
+
const availableModels = await this._modelRegistry.getAvailable();
|
|
1496
|
+
const match = availableModels.find((m) => m.provider === provider && m.id === modelId);
|
|
1497
|
+
if (match) {
|
|
1498
|
+
this.agent.setModel(match);
|
|
1499
|
+
}
|
|
1482
1500
|
}
|
|
1483
1501
|
}
|
|
1484
1502
|
|
|
@@ -5,6 +5,34 @@ import { APP_NAME, getExportTemplateDir } from "../../config.js";
|
|
|
5
5
|
import { getResolvedThemeColors, getThemeExportColors } from "../../modes/interactive/theme/theme.js";
|
|
6
6
|
import { SessionManager } from "../session-manager.js";
|
|
7
7
|
|
|
8
|
+
// Cached minified assets (populated on first use)
|
|
9
|
+
let cachedTemplate: string | null = null;
|
|
10
|
+
let cachedJs: string | null = null;
|
|
11
|
+
|
|
12
|
+
/** Minify CSS by removing comments, unnecessary whitespace, and newlines. */
|
|
13
|
+
function minifyCss(css: string): string {
|
|
14
|
+
return css
|
|
15
|
+
.replace(/\/\*[\s\S]*?\*\//g, "") // Remove comments
|
|
16
|
+
.replace(/\s+/g, " ") // Collapse whitespace
|
|
17
|
+
.replace(/\s*([{}:;,>+~])\s*/g, "$1") // Remove space around punctuation
|
|
18
|
+
.replace(/;}/g, "}") // Remove trailing semicolons
|
|
19
|
+
.trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Minify JS using Bun's transpiler. */
|
|
23
|
+
function minifyJs(js: string): string {
|
|
24
|
+
const transpiler = new Bun.Transpiler({ loader: "js", minifyWhitespace: true });
|
|
25
|
+
return transpiler.transformSync(js);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Minify HTML by collapsing whitespace outside of tags. */
|
|
29
|
+
function minifyHtml(html: string): string {
|
|
30
|
+
return html
|
|
31
|
+
.replace(/>\s+</g, "><") // Remove whitespace between tags
|
|
32
|
+
.replace(/\s{2,}/g, " ") // Collapse multiple spaces
|
|
33
|
+
.trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
8
36
|
export interface ExportOptions {
|
|
9
37
|
outputPath?: string;
|
|
10
38
|
themeName?: string;
|
|
@@ -111,11 +139,16 @@ interface SessionData {
|
|
|
111
139
|
*/
|
|
112
140
|
function generateHtml(sessionData: SessionData, themeName?: string): string {
|
|
113
141
|
const templateDir = getExportTemplateDir();
|
|
114
|
-
|
|
142
|
+
|
|
143
|
+
// Load and minify assets on first use
|
|
144
|
+
if (!cachedTemplate) {
|
|
145
|
+
cachedTemplate = minifyHtml(readFileSync(join(templateDir, "template.html"), "utf-8"));
|
|
146
|
+
}
|
|
147
|
+
if (!cachedJs) {
|
|
148
|
+
cachedJs = minifyJs(readFileSync(join(templateDir, "template.js"), "utf-8"));
|
|
149
|
+
}
|
|
150
|
+
|
|
115
151
|
const templateCss = readFileSync(join(templateDir, "template.css"), "utf-8");
|
|
116
|
-
const templateJs = readFileSync(join(templateDir, "template.js"), "utf-8");
|
|
117
|
-
const markedJs = readFileSync(join(templateDir, "vendor", "marked.min.js"), "utf-8");
|
|
118
|
-
const hljsJs = readFileSync(join(templateDir, "vendor", "highlight.min.js"), "utf-8");
|
|
119
152
|
|
|
120
153
|
const themeVars = generateThemeVars(themeName);
|
|
121
154
|
const colors = getResolvedThemeColors(themeName);
|
|
@@ -127,19 +160,19 @@ function generateHtml(sessionData: SessionData, themeName?: string): string {
|
|
|
127
160
|
// Base64 encode session data to avoid escaping issues
|
|
128
161
|
const sessionDataBase64 = Buffer.from(JSON.stringify(sessionData)).toString("base64");
|
|
129
162
|
|
|
130
|
-
// Build the CSS with theme variables injected
|
|
131
|
-
const css =
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
163
|
+
// Build and minify the CSS with theme variables injected
|
|
164
|
+
const css = minifyCss(
|
|
165
|
+
templateCss
|
|
166
|
+
.replace("{{THEME_VARS}}", themeVars)
|
|
167
|
+
.replace("{{BODY_BG}}", bodyBg)
|
|
168
|
+
.replace("{{CONTAINER_BG}}", containerBg)
|
|
169
|
+
.replace("{{INFO_BG}}", infoBg),
|
|
170
|
+
);
|
|
136
171
|
|
|
137
|
-
return
|
|
172
|
+
return cachedTemplate
|
|
138
173
|
.replace("{{CSS}}", css)
|
|
139
|
-
.replace("{{JS}}",
|
|
140
|
-
.replace("{{SESSION_DATA}}", sessionDataBase64)
|
|
141
|
-
.replace("{{MARKED_JS}}", markedJs)
|
|
142
|
-
.replace("{{HIGHLIGHT_JS}}", hljsJs);
|
|
174
|
+
.replace("{{JS}}", cachedJs)
|
|
175
|
+
.replace("{{SESSION_DATA}}", sessionDataBase64);
|
|
143
176
|
}
|
|
144
177
|
|
|
145
178
|
/**
|
|
@@ -39,16 +39,8 @@
|
|
|
39
39
|
</div>
|
|
40
40
|
|
|
41
41
|
<script id="session-data" type="application/json">{{SESSION_DATA}}</script>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
<script>{{
|
|
45
|
-
|
|
46
|
-
<!-- highlight.js -->
|
|
47
|
-
<script>{{HIGHLIGHT_JS}}</script>
|
|
48
|
-
|
|
49
|
-
<!-- Main application code -->
|
|
50
|
-
<script>
|
|
51
|
-
{{JS}}
|
|
52
|
-
</script>
|
|
42
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/15.0.4/marked.min.js" integrity="sha512-VmLxPVdDGeR+F0DzUHVqzHwaR4ZSSh1g/7aYXwKT1PAGVxunOEcysta+4H5Utvmpr2xExEPybZ8q+iM9F1tGdw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
|
43
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js" integrity="sha512-D9gUyxqja7hBtkWpPWGt9wfbfaMGVt9gnyCvYa+jojwwPHLCzUm5i8rpk7vD7wNee9bA35eYIjobYPaQuKS1MQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
|
44
|
+
<script>{{JS}}</script>
|
|
53
45
|
</body>
|
|
54
46
|
</html>
|
package/src/core/mcp/client.ts
CHANGED
|
@@ -25,12 +25,32 @@ import type {
|
|
|
25
25
|
/** MCP protocol version we support */
|
|
26
26
|
const PROTOCOL_VERSION = "2025-03-26";
|
|
27
27
|
|
|
28
|
+
/** Default connection timeout in ms */
|
|
29
|
+
const CONNECTION_TIMEOUT_MS = 30_000;
|
|
30
|
+
|
|
28
31
|
/** Client info sent during initialization */
|
|
29
32
|
const CLIENT_INFO = {
|
|
30
33
|
name: "pi-coding-agent",
|
|
31
34
|
version: "1.0.0",
|
|
32
35
|
};
|
|
33
36
|
|
|
37
|
+
/** Wrap a promise with a timeout */
|
|
38
|
+
function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const timer = setTimeout(() => reject(new Error(message)), ms);
|
|
41
|
+
promise.then(
|
|
42
|
+
(value) => {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
resolve(value);
|
|
45
|
+
},
|
|
46
|
+
(error) => {
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
reject(error);
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
34
54
|
/**
|
|
35
55
|
* Create a transport for the given server config.
|
|
36
56
|
*/
|
|
@@ -73,24 +93,31 @@ async function initializeConnection(transport: MCPTransport): Promise<MCPInitial
|
|
|
73
93
|
|
|
74
94
|
/**
|
|
75
95
|
* Connect to an MCP server.
|
|
96
|
+
* Has a 30 second timeout to prevent blocking startup.
|
|
76
97
|
*/
|
|
77
98
|
export async function connectToServer(name: string, config: MCPServerConfig): Promise<MCPServerConnection> {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
99
|
+
const timeoutMs = config.timeout ?? CONNECTION_TIMEOUT_MS;
|
|
100
|
+
|
|
101
|
+
const connect = async (): Promise<MCPServerConnection> => {
|
|
102
|
+
const transport = await createTransport(config);
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const initResult = await initializeConnection(transport);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
name,
|
|
109
|
+
config,
|
|
110
|
+
transport,
|
|
111
|
+
serverInfo: initResult.serverInfo,
|
|
112
|
+
capabilities: initResult.capabilities,
|
|
113
|
+
};
|
|
114
|
+
} catch (error) {
|
|
115
|
+
await transport.close();
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return withTimeout(connect(), timeoutMs, `Connection to MCP server "${name}" timed out after ${timeoutMs}ms`);
|
|
94
121
|
}
|
|
95
122
|
|
|
96
123
|
/**
|
package/src/core/mcp/config.ts
CHANGED
|
@@ -91,11 +91,17 @@ export interface MCPConfigLocations {
|
|
|
91
91
|
*/
|
|
92
92
|
export function getMCPConfigPaths(cwd: string): MCPConfigLocations {
|
|
93
93
|
const home = homedir();
|
|
94
|
+
|
|
95
|
+
// Project-level: check both mcp.json and .mcp.json (prefer mcp.json if both exist)
|
|
96
|
+
const mcpJson = join(cwd, "mcp.json");
|
|
97
|
+
const dotMcpJson = join(cwd, ".mcp.json");
|
|
98
|
+
const projectPath = existsSync(mcpJson) ? mcpJson : dotMcpJson;
|
|
99
|
+
|
|
94
100
|
return {
|
|
95
101
|
// User-level: ~/.pi/mcp.json (our standard)
|
|
96
102
|
user: join(home, ".pi", "mcp.json"),
|
|
97
|
-
// Project-level: .mcp.json at project root
|
|
98
|
-
project:
|
|
103
|
+
// Project-level: mcp.json or .mcp.json at project root
|
|
104
|
+
project: projectPath,
|
|
99
105
|
};
|
|
100
106
|
}
|
|
101
107
|
|
|
@@ -115,17 +121,157 @@ export function mergeMCPConfigs(...configs: (MCPConfigFile | null)[]): Record<st
|
|
|
115
121
|
return result;
|
|
116
122
|
}
|
|
117
123
|
|
|
124
|
+
/** Options for loading MCP configs */
|
|
125
|
+
export interface LoadMCPConfigsOptions {
|
|
126
|
+
/** Additional environment variables for expansion */
|
|
127
|
+
extraEnv?: Record<string, string>;
|
|
128
|
+
/** Whether to load project-level config (default: true) */
|
|
129
|
+
enableProjectConfig?: boolean;
|
|
130
|
+
/** Whether to filter out Exa MCP servers (default: true) */
|
|
131
|
+
filterExa?: boolean;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Result of loading MCP configs */
|
|
135
|
+
export interface LoadMCPConfigsResult {
|
|
136
|
+
/** Loaded server configs */
|
|
137
|
+
configs: Record<string, MCPServerConfig>;
|
|
138
|
+
/** Extracted Exa API keys (if any were filtered) */
|
|
139
|
+
exaApiKeys: string[];
|
|
140
|
+
}
|
|
141
|
+
|
|
118
142
|
/**
|
|
119
143
|
* Load all MCP server configs from standard locations.
|
|
120
144
|
* Returns merged config with project overriding user.
|
|
145
|
+
*
|
|
146
|
+
* @param cwd Working directory (project root)
|
|
147
|
+
* @param options Load options or extraEnv for backwards compatibility
|
|
121
148
|
*/
|
|
122
|
-
export function loadAllMCPConfigs(
|
|
149
|
+
export function loadAllMCPConfigs(
|
|
150
|
+
cwd: string,
|
|
151
|
+
options?: LoadMCPConfigsOptions | Record<string, string>,
|
|
152
|
+
): LoadMCPConfigsResult {
|
|
153
|
+
// Support old signature: loadAllMCPConfigs(cwd, extraEnv)
|
|
154
|
+
const opts: LoadMCPConfigsOptions =
|
|
155
|
+
options && ("extraEnv" in options || "enableProjectConfig" in options || "filterExa" in options)
|
|
156
|
+
? (options as LoadMCPConfigsOptions)
|
|
157
|
+
: { extraEnv: options as Record<string, string> | undefined };
|
|
158
|
+
|
|
159
|
+
const enableProjectConfig = opts.enableProjectConfig ?? true;
|
|
160
|
+
const filterExa = opts.filterExa ?? true;
|
|
161
|
+
|
|
123
162
|
const paths = getMCPConfigPaths(cwd);
|
|
124
163
|
|
|
125
|
-
const userConfig = paths.user ? loadMCPConfigFile(paths.user, extraEnv) : null;
|
|
126
|
-
const projectConfig = paths.project ? loadMCPConfigFile(paths.project, extraEnv) : null;
|
|
164
|
+
const userConfig = paths.user ? loadMCPConfigFile(paths.user, opts.extraEnv) : null;
|
|
165
|
+
const projectConfig = enableProjectConfig && paths.project ? loadMCPConfigFile(paths.project, opts.extraEnv) : null;
|
|
166
|
+
|
|
167
|
+
let configs = mergeMCPConfigs(userConfig, projectConfig);
|
|
168
|
+
let exaApiKeys: string[] = [];
|
|
169
|
+
|
|
170
|
+
if (filterExa) {
|
|
171
|
+
const result = filterExaMCPServers(configs);
|
|
172
|
+
configs = result.configs;
|
|
173
|
+
exaApiKeys = result.exaApiKeys;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { configs, exaApiKeys };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Pattern to match Exa MCP servers */
|
|
180
|
+
const EXA_MCP_URL_PATTERN = /mcp\.exa\.ai/i;
|
|
181
|
+
const EXA_API_KEY_PATTERN = /exaApiKey=([^&\s]+)/i;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Check if a server config is an Exa MCP server.
|
|
185
|
+
*/
|
|
186
|
+
export function isExaMCPServer(name: string, config: MCPServerConfig): boolean {
|
|
187
|
+
// Check by server name
|
|
188
|
+
if (name.toLowerCase() === "exa") {
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Check by URL for HTTP/SSE servers
|
|
193
|
+
if (config.type === "http" || config.type === "sse") {
|
|
194
|
+
const httpConfig = config as { url?: string };
|
|
195
|
+
if (httpConfig.url && EXA_MCP_URL_PATTERN.test(httpConfig.url)) {
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Check by args for stdio servers (e.g., mcp-remote to exa)
|
|
201
|
+
if (!config.type || config.type === "stdio") {
|
|
202
|
+
const stdioConfig = config as { args?: string[] };
|
|
203
|
+
if (stdioConfig.args?.some((arg) => EXA_MCP_URL_PATTERN.test(arg))) {
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Extract Exa API key from an MCP server config.
|
|
213
|
+
*/
|
|
214
|
+
export function extractExaApiKey(config: MCPServerConfig): string | undefined {
|
|
215
|
+
// Check URL for HTTP/SSE servers
|
|
216
|
+
if (config.type === "http" || config.type === "sse") {
|
|
217
|
+
const httpConfig = config as { url?: string };
|
|
218
|
+
if (httpConfig.url) {
|
|
219
|
+
const match = EXA_API_KEY_PATTERN.exec(httpConfig.url);
|
|
220
|
+
if (match) return match[1];
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Check args for stdio servers
|
|
225
|
+
if (!config.type || config.type === "stdio") {
|
|
226
|
+
const stdioConfig = config as { args?: string[] };
|
|
227
|
+
if (stdioConfig.args) {
|
|
228
|
+
for (const arg of stdioConfig.args) {
|
|
229
|
+
const match = EXA_API_KEY_PATTERN.exec(arg);
|
|
230
|
+
if (match) return match[1];
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Check env vars
|
|
236
|
+
if ("env" in config && config.env) {
|
|
237
|
+
const envConfig = config as { env: Record<string, string> };
|
|
238
|
+
if (envConfig.env.EXA_API_KEY) {
|
|
239
|
+
return envConfig.env.EXA_API_KEY;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Result of filtering Exa MCP servers */
|
|
247
|
+
export interface ExaFilterResult {
|
|
248
|
+
/** Configs with Exa servers removed */
|
|
249
|
+
configs: Record<string, MCPServerConfig>;
|
|
250
|
+
/** Extracted Exa API keys (if any) */
|
|
251
|
+
exaApiKeys: string[];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Filter out Exa MCP servers and extract their API keys.
|
|
256
|
+
* Since we have native Exa integration, we don't need the MCP server.
|
|
257
|
+
*/
|
|
258
|
+
export function filterExaMCPServers(configs: Record<string, MCPServerConfig>): ExaFilterResult {
|
|
259
|
+
const filtered: Record<string, MCPServerConfig> = {};
|
|
260
|
+
const exaApiKeys: string[] = [];
|
|
261
|
+
|
|
262
|
+
for (const [name, config] of Object.entries(configs)) {
|
|
263
|
+
if (isExaMCPServer(name, config)) {
|
|
264
|
+
// Extract API key before filtering
|
|
265
|
+
const apiKey = extractExaApiKey(config);
|
|
266
|
+
if (apiKey) {
|
|
267
|
+
exaApiKeys.push(apiKey);
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
filtered[name] = config;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
127
273
|
|
|
128
|
-
return
|
|
274
|
+
return { configs: filtered, exaApiKeys };
|
|
129
275
|
}
|
|
130
276
|
|
|
131
277
|
/**
|
package/src/core/mcp/index.ts
CHANGED
|
@@ -9,19 +9,23 @@
|
|
|
9
9
|
export { callTool, connectToServer, disconnectServer, listTools, serverSupportsTools } from "./client.js";
|
|
10
10
|
|
|
11
11
|
// Config
|
|
12
|
+
export type { ExaFilterResult, LoadMCPConfigsOptions, LoadMCPConfigsResult } from "./config.js";
|
|
12
13
|
export {
|
|
13
14
|
expandEnvVars,
|
|
15
|
+
extractExaApiKey,
|
|
16
|
+
filterExaMCPServers,
|
|
14
17
|
getMCPConfigPaths,
|
|
18
|
+
isExaMCPServer,
|
|
15
19
|
loadAllMCPConfigs,
|
|
16
20
|
loadMCPConfigFile,
|
|
17
21
|
mergeMCPConfigs,
|
|
18
22
|
validateServerConfig,
|
|
19
23
|
} from "./config.js";
|
|
20
24
|
// Loader (for SDK integration)
|
|
21
|
-
export type { MCPToolsLoadResult } from "./loader.js";
|
|
25
|
+
export type { MCPToolsLoadOptions, MCPToolsLoadResult } from "./loader.js";
|
|
22
26
|
export { discoverAndLoadMCPTools } from "./loader.js";
|
|
23
27
|
// Manager
|
|
24
|
-
export type { MCPLoadResult } from "./manager.js";
|
|
28
|
+
export type { MCPDiscoverOptions, MCPLoadResult } from "./manager.js";
|
|
25
29
|
export { createMCPManager, MCPManager } from "./manager.js";
|
|
26
30
|
// Tool bridge
|
|
27
31
|
export type { MCPToolDetails } from "./tool-bridge.js";
|