@oh-my-pi/pi-coding-agent 4.2.1 → 4.2.3
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 +30 -0
- package/docs/sdk.md +5 -5
- package/examples/sdk/10-settings.ts +2 -2
- package/package.json +5 -5
- package/src/capability/fs.ts +90 -0
- package/src/capability/index.ts +41 -227
- package/src/capability/types.ts +1 -11
- package/src/cli/args.ts +4 -0
- package/src/core/agent-session.ts +4 -4
- package/src/core/agent-storage.ts +50 -0
- package/src/core/auth-storage.ts +112 -4
- package/src/core/bash-executor.ts +1 -1
- package/src/core/custom-tools/loader.ts +2 -2
- package/src/core/extensions/loader.ts +2 -2
- package/src/core/extensions/types.ts +1 -1
- package/src/core/hooks/loader.ts +2 -2
- package/src/core/mcp/config.ts +2 -2
- package/src/core/model-registry.ts +46 -0
- package/src/core/sdk.ts +37 -29
- package/src/core/settings-manager.ts +152 -135
- package/src/core/skills.ts +72 -51
- package/src/core/slash-commands.ts +3 -3
- package/src/core/system-prompt.ts +10 -10
- package/src/core/tools/edit.ts +7 -4
- package/src/core/tools/find.ts +2 -2
- package/src/core/tools/index.test.ts +16 -0
- package/src/core/tools/index.ts +21 -8
- package/src/core/tools/lsp/index.ts +4 -1
- package/src/core/tools/ssh.ts +6 -6
- package/src/core/tools/task/commands.ts +3 -5
- package/src/core/tools/task/executor.ts +88 -3
- package/src/core/tools/task/index.ts +4 -0
- package/src/core/tools/task/model-resolver.ts +10 -7
- package/src/core/tools/task/worker-protocol.ts +48 -2
- package/src/core/tools/task/worker.ts +152 -7
- package/src/core/tools/write.ts +7 -4
- package/src/discovery/agents-md.ts +13 -19
- package/src/discovery/builtin.ts +367 -247
- package/src/discovery/claude.ts +181 -290
- package/src/discovery/cline.ts +30 -10
- package/src/discovery/codex.ts +185 -244
- package/src/discovery/cursor.ts +106 -121
- package/src/discovery/gemini.ts +72 -97
- package/src/discovery/github.ts +7 -10
- package/src/discovery/helpers.ts +94 -88
- package/src/discovery/index.ts +1 -2
- package/src/discovery/mcp-json.ts +15 -18
- package/src/discovery/ssh.ts +9 -17
- package/src/discovery/vscode.ts +10 -5
- package/src/discovery/windsurf.ts +52 -86
- package/src/main.ts +5 -1
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +24 -11
- package/src/modes/interactive/components/extensions/state-manager.ts +19 -15
- package/src/modes/interactive/controllers/selector-controller.ts +6 -2
- package/src/modes/interactive/interactive-mode.ts +19 -15
- package/src/prompts/agents/plan.md +107 -30
- package/src/utils/shell.ts +2 -2
- package/src/prompts/agents/planner.md +0 -112
package/src/core/skills.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { readdirSync, readFileSync,
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { realpath } from "node:fs/promises";
|
|
2
3
|
import { basename, join } from "node:path";
|
|
3
4
|
import { minimatch } from "minimatch";
|
|
4
5
|
import { skillCapability } from "../capability/skill";
|
|
5
6
|
import type { SourceMeta } from "../capability/types";
|
|
6
7
|
import type { Skill as CapabilitySkill, SkillFrontmatter as ImportedSkillFrontmatter } from "../discovery";
|
|
7
|
-
import {
|
|
8
|
+
import { loadCapability } from "../discovery";
|
|
8
9
|
import { parseFrontmatter } from "../discovery/helpers";
|
|
9
10
|
import type { SkillsSettings } from "./settings-manager";
|
|
10
11
|
|
|
@@ -215,7 +216,7 @@ export interface LoadSkillsOptions extends SkillsSettings {
|
|
|
215
216
|
* Load skills from all configured locations.
|
|
216
217
|
* Returns skills and any validation warnings.
|
|
217
218
|
*/
|
|
218
|
-
export function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult {
|
|
219
|
+
export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadSkillsResult> {
|
|
219
220
|
const {
|
|
220
221
|
cwd = process.cwd(),
|
|
221
222
|
enabled = true,
|
|
@@ -247,7 +248,7 @@ export function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult {
|
|
|
247
248
|
}
|
|
248
249
|
|
|
249
250
|
// Use capability API to load all skills
|
|
250
|
-
const result =
|
|
251
|
+
const result = await loadCapability<CapabilitySkill>(skillCapability.id, { cwd });
|
|
251
252
|
|
|
252
253
|
const skillMap = new Map<string, Skill>();
|
|
253
254
|
const realPathSet = new Set<string>();
|
|
@@ -265,28 +266,33 @@ export function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult {
|
|
|
265
266
|
return ignoredSkills.some((pattern) => minimatch(name, pattern));
|
|
266
267
|
}
|
|
267
268
|
|
|
268
|
-
//
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (matchesIgnorePatterns(capSkill.name))
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
269
|
+
// Filter skills by source and patterns first
|
|
270
|
+
const filteredSkills = result.items.filter((capSkill) => {
|
|
271
|
+
if (!isSourceEnabled(capSkill._source)) return false;
|
|
272
|
+
if (matchesIgnorePatterns(capSkill.name)) return false;
|
|
273
|
+
if (!matchesIncludePatterns(capSkill.name)) return false;
|
|
274
|
+
return true;
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Batch resolve all real paths in parallel
|
|
278
|
+
const realPaths = await Promise.all(
|
|
279
|
+
filteredSkills.map(async (capSkill) => {
|
|
280
|
+
try {
|
|
281
|
+
return await realpath(capSkill.path);
|
|
282
|
+
} catch {
|
|
283
|
+
return capSkill.path;
|
|
284
|
+
}
|
|
285
|
+
}),
|
|
286
|
+
);
|
|
278
287
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
} catch {
|
|
284
|
-
realPath = capSkill.path;
|
|
285
|
-
}
|
|
288
|
+
// Process skills with resolved paths
|
|
289
|
+
for (let i = 0; i < filteredSkills.length; i++) {
|
|
290
|
+
const capSkill = filteredSkills[i];
|
|
291
|
+
const resolvedPath = realPaths[i];
|
|
286
292
|
|
|
287
293
|
// Skip silently if we've already loaded this exact file (via symlink)
|
|
288
|
-
if (realPathSet.has(
|
|
289
|
-
|
|
294
|
+
if (realPathSet.has(resolvedPath)) {
|
|
295
|
+
continue;
|
|
290
296
|
}
|
|
291
297
|
|
|
292
298
|
const existing = skillMap.get(capSkill.name);
|
|
@@ -302,46 +308,61 @@ export function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult {
|
|
|
302
308
|
description: capSkill.frontmatter?.description || "",
|
|
303
309
|
filePath: capSkill.path,
|
|
304
310
|
baseDir: capSkill.path.replace(/\/SKILL\.md$/, ""),
|
|
305
|
-
source: `${
|
|
311
|
+
source: `${capSkill._source.provider}:${capSkill.level}`,
|
|
306
312
|
_source: capSkill._source,
|
|
307
313
|
};
|
|
308
314
|
skillMap.set(capSkill.name, skill);
|
|
309
|
-
realPathSet.add(
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Process skills from capability API
|
|
314
|
-
for (const capSkill of result.items) {
|
|
315
|
-
// Check if this source is enabled
|
|
316
|
-
if (!isSourceEnabled(capSkill._source)) {
|
|
317
|
-
continue;
|
|
315
|
+
realPathSet.add(resolvedPath);
|
|
318
316
|
}
|
|
319
|
-
|
|
320
|
-
addSkill(capSkill, capSkill._source.provider);
|
|
321
317
|
}
|
|
322
318
|
|
|
323
319
|
// Process custom directories - scan directly without using full provider system
|
|
320
|
+
const allCustomSkills: Array<{ skill: Skill; path: string }> = [];
|
|
324
321
|
for (const dir of customDirectories) {
|
|
325
322
|
const customSkills = scanDirectoryForSkills(dir);
|
|
326
323
|
for (const s of customSkills.skills) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
providerName: "Custom",
|
|
337
|
-
path: s.filePath,
|
|
338
|
-
level: "user",
|
|
324
|
+
if (matchesIgnorePatterns(s.name)) continue;
|
|
325
|
+
if (!matchesIncludePatterns(s.name)) continue;
|
|
326
|
+
allCustomSkills.push({
|
|
327
|
+
skill: {
|
|
328
|
+
name: s.name,
|
|
329
|
+
description: s.description,
|
|
330
|
+
filePath: s.filePath,
|
|
331
|
+
baseDir: s.filePath.replace(/\/SKILL\.md$/, ""),
|
|
332
|
+
source: "custom:user",
|
|
333
|
+
_source: { provider: "custom", providerName: "Custom", path: s.filePath, level: "user" },
|
|
339
334
|
},
|
|
340
|
-
|
|
341
|
-
|
|
335
|
+
path: s.filePath,
|
|
336
|
+
});
|
|
342
337
|
}
|
|
343
|
-
|
|
344
|
-
|
|
338
|
+
collisionWarnings.push(...customSkills.warnings);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Batch resolve custom skill paths
|
|
342
|
+
const customRealPaths = await Promise.all(
|
|
343
|
+
allCustomSkills.map(async ({ path }) => {
|
|
344
|
+
try {
|
|
345
|
+
return await realpath(path);
|
|
346
|
+
} catch {
|
|
347
|
+
return path;
|
|
348
|
+
}
|
|
349
|
+
}),
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
for (let i = 0; i < allCustomSkills.length; i++) {
|
|
353
|
+
const { skill } = allCustomSkills[i];
|
|
354
|
+
const resolvedPath = customRealPaths[i];
|
|
355
|
+
if (realPathSet.has(resolvedPath)) continue;
|
|
356
|
+
|
|
357
|
+
const existing = skillMap.get(skill.name);
|
|
358
|
+
if (existing) {
|
|
359
|
+
collisionWarnings.push({
|
|
360
|
+
skillPath: skill.filePath,
|
|
361
|
+
message: `name collision: "${skill.name}" already loaded from ${existing.filePath}, skipping this one`,
|
|
362
|
+
});
|
|
363
|
+
} else {
|
|
364
|
+
skillMap.set(skill.name, skill);
|
|
365
|
+
realPathSet.add(resolvedPath);
|
|
345
366
|
}
|
|
346
367
|
}
|
|
347
368
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { slashCommandCapability } from "../capability/slash-command";
|
|
2
2
|
import type { SlashCommand } from "../discovery";
|
|
3
|
-
import {
|
|
3
|
+
import { loadCapability } from "../discovery";
|
|
4
4
|
import { parseFrontmatter } from "../discovery/helpers";
|
|
5
5
|
import { renderPromptTemplate } from "./prompt-templates";
|
|
6
6
|
import { EMBEDDED_COMMAND_TEMPLATES } from "./tools/task/commands";
|
|
@@ -108,8 +108,8 @@ export interface LoadSlashCommandsOptions {
|
|
|
108
108
|
* Load all custom slash commands using the capability API.
|
|
109
109
|
* Loads from all registered providers (builtin, user, project).
|
|
110
110
|
*/
|
|
111
|
-
export function loadSlashCommands(options: LoadSlashCommandsOptions = {}): FileSlashCommand[] {
|
|
112
|
-
const result =
|
|
111
|
+
export async function loadSlashCommands(options: LoadSlashCommandsOptions = {}): Promise<FileSlashCommand[]> {
|
|
112
|
+
const result = await loadCapability<SlashCommand>(slashCommandCapability.id, { cwd: options.cwd });
|
|
113
113
|
|
|
114
114
|
const fileCommands: FileSlashCommand[] = result.items.map((cmd) => {
|
|
115
115
|
const { description, body } = parseCommandTemplate(cmd.content);
|
|
@@ -8,7 +8,7 @@ import { join } from "node:path";
|
|
|
8
8
|
import chalk from "chalk";
|
|
9
9
|
import { contextFileCapability } from "../capability/context-file";
|
|
10
10
|
import { systemPromptCapability } from "../capability/system-prompt";
|
|
11
|
-
import { type ContextFile,
|
|
11
|
+
import { type ContextFile, loadCapability, type SystemPrompt as SystemPromptFile } from "../discovery/index";
|
|
12
12
|
import customSystemPromptTemplate from "../prompts/system/custom-system-prompt.md" with { type: "text" };
|
|
13
13
|
import systemPromptTemplate from "../prompts/system/system-prompt.md" with { type: "text" };
|
|
14
14
|
import { renderPromptTemplate } from "./prompt-templates";
|
|
@@ -558,12 +558,12 @@ export interface LoadContextFilesOptions {
|
|
|
558
558
|
* Returns {path, content, depth} entries for all discovered context files.
|
|
559
559
|
* Files are sorted by depth (descending) so files closer to cwd appear last/more prominent.
|
|
560
560
|
*/
|
|
561
|
-
export function loadProjectContextFiles(
|
|
561
|
+
export async function loadProjectContextFiles(
|
|
562
562
|
options: LoadContextFilesOptions = {},
|
|
563
|
-
): Array<{ path: string; content: string; depth?: number }
|
|
563
|
+
): Promise<Array<{ path: string; content: string; depth?: number }>> {
|
|
564
564
|
const resolvedCwd = options.cwd ?? process.cwd();
|
|
565
565
|
|
|
566
|
-
const result =
|
|
566
|
+
const result = await loadCapability(contextFileCapability.id, { cwd: resolvedCwd });
|
|
567
567
|
|
|
568
568
|
// Convert ContextFile items and preserve depth info
|
|
569
569
|
const files = result.items.map((item) => {
|
|
@@ -590,10 +590,10 @@ export function loadProjectContextFiles(
|
|
|
590
590
|
* Load system prompt customization files (SYSTEM.md).
|
|
591
591
|
* Returns combined content from all discovered SYSTEM.md files.
|
|
592
592
|
*/
|
|
593
|
-
export function loadSystemPromptFiles(options: LoadContextFilesOptions = {}): string | null {
|
|
593
|
+
export async function loadSystemPromptFiles(options: LoadContextFilesOptions = {}): Promise<string | null> {
|
|
594
594
|
const resolvedCwd = options.cwd ?? process.cwd();
|
|
595
595
|
|
|
596
|
-
const result =
|
|
596
|
+
const result = await loadCapability<SystemPromptFile>(systemPromptCapability.id, { cwd: resolvedCwd });
|
|
597
597
|
|
|
598
598
|
if (result.items.length === 0) return null;
|
|
599
599
|
|
|
@@ -631,7 +631,7 @@ export interface BuildSystemPromptOptions {
|
|
|
631
631
|
}
|
|
632
632
|
|
|
633
633
|
/** Build the system prompt with tools, guidelines, and context */
|
|
634
|
-
export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): string {
|
|
634
|
+
export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): Promise<string> {
|
|
635
635
|
const {
|
|
636
636
|
customPrompt,
|
|
637
637
|
tools,
|
|
@@ -648,7 +648,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|
|
648
648
|
const resolvedAppendPrompt = resolvePromptInput(appendSystemPrompt, "append system prompt");
|
|
649
649
|
|
|
650
650
|
// Load SYSTEM.md customization (prepended to prompt)
|
|
651
|
-
const systemPromptCustomization = loadSystemPromptFiles({ cwd: resolvedCwd });
|
|
651
|
+
const systemPromptCustomization = await loadSystemPromptFiles({ cwd: resolvedCwd });
|
|
652
652
|
|
|
653
653
|
const now = new Date();
|
|
654
654
|
const dateTime = now.toLocaleString("en-US", {
|
|
@@ -663,7 +663,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|
|
663
663
|
});
|
|
664
664
|
|
|
665
665
|
// Resolve context files: use provided or discover
|
|
666
|
-
const contextFiles = providedContextFiles ?? loadProjectContextFiles({ cwd: resolvedCwd });
|
|
666
|
+
const contextFiles = providedContextFiles ?? (await loadProjectContextFiles({ cwd: resolvedCwd }));
|
|
667
667
|
const agentsMdSearch = buildAgentsMdSearch(resolvedCwd);
|
|
668
668
|
|
|
669
669
|
// Build tool descriptions array
|
|
@@ -688,7 +688,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|
|
688
688
|
// Resolve skills: use provided or discover
|
|
689
689
|
const skills =
|
|
690
690
|
providedSkills ??
|
|
691
|
-
(skillsSettings?.enabled !== false ? loadSkills({ ...skillsSettings, cwd: resolvedCwd }).skills : []);
|
|
691
|
+
(skillsSettings?.enabled !== false ? (await loadSkills({ ...skillsSettings, cwd: resolvedCwd })).skills : []);
|
|
692
692
|
|
|
693
693
|
// Get git context
|
|
694
694
|
const git = loadGitContext(resolvedCwd);
|
package/src/core/tools/edit.ts
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
stripBom,
|
|
20
20
|
} from "./edit-diff";
|
|
21
21
|
import type { ToolSession } from "./index";
|
|
22
|
-
import { createLspWritethrough, type FileDiagnosticsResult } from "./lsp/index";
|
|
22
|
+
import { createLspWritethrough, type FileDiagnosticsResult, writethroughNoop } from "./lsp/index";
|
|
23
23
|
import { resolveToCwd } from "./path-utils";
|
|
24
24
|
import { createToolUIKit, getDiffStats, shortenPath, truncateDiffByHunk } from "./render-utils";
|
|
25
25
|
|
|
@@ -43,9 +43,12 @@ export interface EditToolDetails {
|
|
|
43
43
|
|
|
44
44
|
export function createEditTool(session: ToolSession): AgentTool<typeof editSchema> {
|
|
45
45
|
const allowFuzzy = session.settings?.getEditFuzzyMatch() ?? true;
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const
|
|
46
|
+
const enableLsp = session.enableLsp ?? true;
|
|
47
|
+
const enableDiagnostics = enableLsp ? (session.settings?.getLspDiagnosticsOnEdit() ?? false) : false;
|
|
48
|
+
const enableFormat = enableLsp ? (session.settings?.getLspFormatOnWrite() ?? true) : false;
|
|
49
|
+
const writethrough = enableLsp
|
|
50
|
+
? createLspWritethrough(session.cwd, { enableFormat, enableDiagnostics })
|
|
51
|
+
: writethroughNoop;
|
|
49
52
|
return {
|
|
50
53
|
name: "edit",
|
|
51
54
|
label: "Edit",
|
package/src/core/tools/find.ts
CHANGED
|
@@ -20,7 +20,7 @@ const findSchema = Type.Object({
|
|
|
20
20
|
}),
|
|
21
21
|
path: Type.Optional(Type.String({ description: "Directory to search in (default: current directory)" })),
|
|
22
22
|
limit: Type.Optional(Type.Number({ description: "Maximum number of results (default: 1000)" })),
|
|
23
|
-
hidden: Type.Optional(Type.Boolean({ description: "Include hidden files (default:
|
|
23
|
+
hidden: Type.Optional(Type.Boolean({ description: "Include hidden files (default: true)" })),
|
|
24
24
|
sortByMtime: Type.Optional(
|
|
25
25
|
Type.Boolean({ description: "Sort results by modification time, most recent first (default: false)" }),
|
|
26
26
|
),
|
|
@@ -143,7 +143,7 @@ export function createFindTool(session: ToolSession, options?: FindToolOptions):
|
|
|
143
143
|
})();
|
|
144
144
|
const effectiveLimit = limit ?? DEFAULT_LIMIT;
|
|
145
145
|
const effectiveType = type ?? "all";
|
|
146
|
-
const includeHidden = hidden ??
|
|
146
|
+
const includeHidden = hidden ?? true;
|
|
147
147
|
const shouldSortByMtime = sortByMtime ?? false;
|
|
148
148
|
|
|
149
149
|
// If custom operations provided with glob, use that instead of fd
|
|
@@ -34,6 +34,22 @@ describe("createTools", () => {
|
|
|
34
34
|
expect(names).toContain("web_search");
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
+
it("excludes lsp tool when session disables LSP", async () => {
|
|
38
|
+
const session = createTestSession({ enableLsp: false });
|
|
39
|
+
const tools = await createTools(session, ["read", "lsp", "write"]);
|
|
40
|
+
const names = tools.map((t) => t.name);
|
|
41
|
+
|
|
42
|
+
expect(names).toEqual(["read", "write"]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("excludes lsp tool when disabled", async () => {
|
|
46
|
+
const session = createTestSession({ enableLsp: false });
|
|
47
|
+
const tools = await createTools(session);
|
|
48
|
+
const names = tools.map((t) => t.name);
|
|
49
|
+
|
|
50
|
+
expect(names).not.toContain("lsp");
|
|
51
|
+
});
|
|
52
|
+
|
|
37
53
|
it("respects requested tool subset", async () => {
|
|
38
54
|
const session = createTestSession();
|
|
39
55
|
const tools = await createTools(session, ["read", "write"]);
|
package/src/core/tools/index.ts
CHANGED
|
@@ -92,6 +92,8 @@ export interface ToolSession {
|
|
|
92
92
|
cwd: string;
|
|
93
93
|
/** Whether UI is available */
|
|
94
94
|
hasUI: boolean;
|
|
95
|
+
/** Whether LSP integrations are enabled */
|
|
96
|
+
enableLsp?: boolean;
|
|
95
97
|
/** Event bus for tool/extension communication */
|
|
96
98
|
eventBus?: EventBus;
|
|
97
99
|
/** Output schema for structured completion (subagents) */
|
|
@@ -106,6 +108,12 @@ export interface ToolSession {
|
|
|
106
108
|
getModelString?: () => string | undefined;
|
|
107
109
|
/** Get the current session model string, regardless of how it was chosen */
|
|
108
110
|
getActiveModelString?: () => string | undefined;
|
|
111
|
+
/** Auth storage for passing to subagents (avoids re-discovery) */
|
|
112
|
+
authStorage?: import("../auth-storage").AuthStorage;
|
|
113
|
+
/** Model registry for passing to subagents (avoids re-discovery) */
|
|
114
|
+
modelRegistry?: import("../model-registry").ModelRegistry;
|
|
115
|
+
/** MCP manager for proxying MCP calls through parent */
|
|
116
|
+
mcpManager?: import("../mcp/manager").MCPManager;
|
|
109
117
|
/** Settings manager (optional) */
|
|
110
118
|
settings?: {
|
|
111
119
|
getImageAutoResize(): boolean;
|
|
@@ -154,23 +162,28 @@ export type ToolName = keyof typeof BUILTIN_TOOLS;
|
|
|
154
162
|
*/
|
|
155
163
|
export async function createTools(session: ToolSession, toolNames?: string[]): Promise<Tool[]> {
|
|
156
164
|
const includeComplete = session.requireCompleteTool === true;
|
|
165
|
+
const enableLsp = session.enableLsp ?? true;
|
|
157
166
|
const requestedTools = toolNames && toolNames.length > 0 ? [...new Set(toolNames)] : undefined;
|
|
158
167
|
const allTools: Record<string, ToolFactory> = { ...BUILTIN_TOOLS, ...HIDDEN_TOOLS };
|
|
168
|
+
const isToolAllowed = (name: string) => (name === "lsp" ? enableLsp : true);
|
|
159
169
|
if (includeComplete && requestedTools && !requestedTools.includes("complete")) {
|
|
160
170
|
requestedTools.push("complete");
|
|
161
171
|
}
|
|
162
172
|
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
173
|
+
const filteredRequestedTools = requestedTools?.filter((name) => name in allTools && isToolAllowed(name));
|
|
174
|
+
|
|
175
|
+
const entries =
|
|
176
|
+
filteredRequestedTools !== undefined
|
|
177
|
+
? filteredRequestedTools.map((name) => [name, allTools[name]] as const)
|
|
178
|
+
: [
|
|
179
|
+
...Object.entries(BUILTIN_TOOLS).filter(([name]) => isToolAllowed(name)),
|
|
180
|
+
...(includeComplete ? ([["complete", HIDDEN_TOOLS.complete]] as const) : []),
|
|
181
|
+
];
|
|
169
182
|
const results = await Promise.all(entries.map(([, factory]) => factory(session)));
|
|
170
183
|
const tools = results.filter((t): t is Tool => t !== null);
|
|
171
184
|
|
|
172
|
-
if (
|
|
173
|
-
const allowed = new Set(
|
|
185
|
+
if (filteredRequestedTools !== undefined) {
|
|
186
|
+
const allowed = new Set(filteredRequestedTools);
|
|
174
187
|
return tools.filter((tool) => allowed.has(tool.name));
|
|
175
188
|
}
|
|
176
189
|
|
|
@@ -734,7 +734,10 @@ export function createLspWritethrough(cwd: string, options?: WritethroughOptions
|
|
|
734
734
|
}
|
|
735
735
|
|
|
736
736
|
/** Create an LSP tool */
|
|
737
|
-
export function createLspTool(session: ToolSession): AgentTool<typeof lspSchema, LspToolDetails, Theme> {
|
|
737
|
+
export function createLspTool(session: ToolSession): AgentTool<typeof lspSchema, LspToolDetails, Theme> | null {
|
|
738
|
+
if (session.enableLsp === false) {
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
738
741
|
return {
|
|
739
742
|
name: "lsp",
|
|
740
743
|
label: "LSP",
|
package/src/core/tools/ssh.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { Text } from "@oh-my-pi/pi-tui";
|
|
|
4
4
|
import { Type } from "@sinclair/typebox";
|
|
5
5
|
import type { SSHHost } from "../../capability/ssh";
|
|
6
6
|
import { sshCapability } from "../../capability/ssh";
|
|
7
|
-
import {
|
|
7
|
+
import { loadCapability } from "../../discovery/index";
|
|
8
8
|
import type { Theme } from "../../modes/interactive/theme/theme";
|
|
9
9
|
import sshDescriptionBase from "../../prompts/tools/ssh.md" with { type: "text" };
|
|
10
10
|
import type { RenderResultOptions } from "../custom-tools/types";
|
|
@@ -97,11 +97,11 @@ function buildRemoteCommand(command: string, cwd: string | undefined, info: SSHH
|
|
|
97
97
|
return `cd -- ${quoteRemotePath(cwd)} && ${command}`;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
function loadHosts(session: ToolSession): {
|
|
100
|
+
async function loadHosts(session: ToolSession): Promise<{
|
|
101
101
|
hostNames: string[];
|
|
102
102
|
hostsByName: Map<string, SSHHost>;
|
|
103
|
-
} {
|
|
104
|
-
const result =
|
|
103
|
+
}> {
|
|
104
|
+
const result = await loadCapability<SSHHost>(sshCapability.id, { cwd: session.cwd });
|
|
105
105
|
const hostsByName = new Map<string, SSHHost>();
|
|
106
106
|
for (const host of result.items) {
|
|
107
107
|
if (!hostsByName.has(host.name)) {
|
|
@@ -112,8 +112,8 @@ function loadHosts(session: ToolSession): {
|
|
|
112
112
|
return { hostNames, hostsByName };
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
export function createSshTool(session: ToolSession): AgentTool<typeof sshSchema> | null {
|
|
116
|
-
const { hostNames, hostsByName } = loadHosts(session);
|
|
115
|
+
export async function createSshTool(session: ToolSession): Promise<AgentTool<typeof sshSchema> | null> {
|
|
116
|
+
const { hostNames, hostsByName } = await loadHosts(session);
|
|
117
117
|
if (hostNames.length === 0) {
|
|
118
118
|
return null;
|
|
119
119
|
}
|
|
@@ -6,16 +6,14 @@
|
|
|
6
6
|
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import { type SlashCommand, slashCommandCapability } from "../../../capability/slash-command";
|
|
9
|
-
import {
|
|
9
|
+
import { loadCapability } from "../../../discovery";
|
|
10
10
|
|
|
11
11
|
// Embed command markdown files at build time
|
|
12
12
|
import initMd from "../../../prompts/agents/init.md" with { type: "text" };
|
|
13
|
-
import plannerMd from "../../../prompts/agents/planner.md" with { type: "text" };
|
|
14
13
|
import { renderPromptTemplate } from "../../prompt-templates";
|
|
15
14
|
|
|
16
15
|
const EMBEDDED_COMMANDS: { name: string; content: string }[] = [
|
|
17
16
|
{ name: "init.md", content: renderPromptTemplate(initMd) },
|
|
18
|
-
{ name: "planner.md", content: renderPromptTemplate(plannerMd) },
|
|
19
17
|
];
|
|
20
18
|
|
|
21
19
|
export const EMBEDDED_COMMAND_TEMPLATES: ReadonlyArray<{ name: string; content: string }> = EMBEDDED_COMMANDS;
|
|
@@ -97,11 +95,11 @@ export function loadBundledCommands(): WorkflowCommand[] {
|
|
|
97
95
|
*
|
|
98
96
|
* Precedence (highest wins): .omp > .pi > .claude (project before user), then bundled
|
|
99
97
|
*/
|
|
100
|
-
export function discoverCommands(cwd: string): WorkflowCommand[] {
|
|
98
|
+
export async function discoverCommands(cwd: string): Promise<WorkflowCommand[]> {
|
|
101
99
|
const resolvedCwd = path.resolve(cwd);
|
|
102
100
|
|
|
103
101
|
// Load slash commands from capability API
|
|
104
|
-
const result =
|
|
102
|
+
const result = await loadCapability<SlashCommand>(slashCommandCapability.id, { cwd: resolvedCwd });
|
|
105
103
|
|
|
106
104
|
const commands: WorkflowCommand[] = [];
|
|
107
105
|
const seen = new Set<string>();
|
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { AgentEvent } from "@oh-my-pi/pi-agent-core";
|
|
8
|
+
import type { AuthStorage } from "../../auth-storage";
|
|
8
9
|
import type { EventBus } from "../../event-bus";
|
|
10
|
+
import { callTool } from "../../mcp/client";
|
|
11
|
+
import type { MCPManager } from "../../mcp/manager";
|
|
12
|
+
import type { ModelRegistry } from "../../model-registry";
|
|
9
13
|
import { ensureArtifactsDir, getArtifactPaths } from "./artifacts";
|
|
10
14
|
import { resolveModelPattern } from "./model-resolver";
|
|
11
15
|
import { subprocessToolRegistry } from "./subprocess-tool-registry";
|
|
@@ -18,7 +22,12 @@ import {
|
|
|
18
22
|
TASK_SUBAGENT_EVENT_CHANNEL,
|
|
19
23
|
TASK_SUBAGENT_PROGRESS_CHANNEL,
|
|
20
24
|
} from "./types";
|
|
21
|
-
import type {
|
|
25
|
+
import type {
|
|
26
|
+
MCPToolCallRequest,
|
|
27
|
+
MCPToolMetadata,
|
|
28
|
+
SubagentWorkerRequest,
|
|
29
|
+
SubagentWorkerResponse,
|
|
30
|
+
} from "./worker-protocol";
|
|
22
31
|
|
|
23
32
|
/** Options for worker execution */
|
|
24
33
|
export interface ExecutorOptions {
|
|
@@ -31,12 +40,16 @@ export interface ExecutorOptions {
|
|
|
31
40
|
context?: string;
|
|
32
41
|
modelOverride?: string;
|
|
33
42
|
outputSchema?: unknown;
|
|
43
|
+
enableLsp?: boolean;
|
|
34
44
|
signal?: AbortSignal;
|
|
35
45
|
onProgress?: (progress: AgentProgress) => void;
|
|
36
46
|
sessionFile?: string | null;
|
|
37
47
|
persistArtifacts?: boolean;
|
|
38
48
|
artifactsDir?: string;
|
|
39
49
|
eventBus?: EventBus;
|
|
50
|
+
mcpManager?: MCPManager;
|
|
51
|
+
authStorage?: AuthStorage;
|
|
52
|
+
modelRegistry?: ModelRegistry;
|
|
40
53
|
}
|
|
41
54
|
|
|
42
55
|
/**
|
|
@@ -133,11 +146,45 @@ function getUsageTokens(usage: unknown): number {
|
|
|
133
146
|
return input + output + cacheRead + cacheWrite;
|
|
134
147
|
}
|
|
135
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Parse MCP tool name to extract server and tool names.
|
|
151
|
+
* Format: mcp_<serverName>_<toolName>
|
|
152
|
+
* Note: Uses lastIndexOf to handle server names with underscores.
|
|
153
|
+
*/
|
|
154
|
+
function parseMCPToolName(fullName: string): { serverName: string; toolName: string } | undefined {
|
|
155
|
+
if (!fullName.startsWith("mcp_")) return undefined;
|
|
156
|
+
const rest = fullName.slice(4);
|
|
157
|
+
const underscoreIndex = rest.lastIndexOf("_");
|
|
158
|
+
if (underscoreIndex === -1) return undefined;
|
|
159
|
+
return {
|
|
160
|
+
serverName: rest.slice(0, underscoreIndex),
|
|
161
|
+
toolName: rest.slice(underscoreIndex + 1),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Extract MCP tool metadata from MCPManager for passing to worker.
|
|
167
|
+
*/
|
|
168
|
+
function extractMCPToolMetadata(mcpManager: MCPManager): MCPToolMetadata[] {
|
|
169
|
+
return mcpManager.getTools().map((tool) => {
|
|
170
|
+
const parsed = parseMCPToolName(tool.name);
|
|
171
|
+
return {
|
|
172
|
+
name: tool.name,
|
|
173
|
+
label: tool.label ?? tool.name,
|
|
174
|
+
description: tool.description ?? "",
|
|
175
|
+
parameters: tool.parameters,
|
|
176
|
+
serverName: parsed?.serverName ?? "",
|
|
177
|
+
mcpToolName: parsed?.toolName ?? "",
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
136
182
|
/**
|
|
137
183
|
* Run a single agent in a worker.
|
|
138
184
|
*/
|
|
139
185
|
export async function runSubprocess(options: ExecutorOptions): Promise<SingleResult> {
|
|
140
|
-
const { cwd, agent, task, index, taskId, context, modelOverride, outputSchema, signal, onProgress } =
|
|
186
|
+
const { cwd, agent, task, index, taskId, context, modelOverride, outputSchema, enableLsp, signal, onProgress } =
|
|
187
|
+
options;
|
|
141
188
|
const startTime = Date.now();
|
|
142
189
|
|
|
143
190
|
// Initialize progress
|
|
@@ -208,7 +255,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
208
255
|
}
|
|
209
256
|
|
|
210
257
|
// Resolve and add model
|
|
211
|
-
const resolvedModel = resolveModelPattern(modelOverride || agent.model);
|
|
258
|
+
const resolvedModel = await resolveModelPattern(modelOverride || agent.model);
|
|
212
259
|
const sessionFile = subtaskSessionFile ?? options.sessionFile ?? null;
|
|
213
260
|
const spawnsEnv = agent.spawns === undefined ? "" : agent.spawns === "*" ? "*" : agent.spawns.join(",");
|
|
214
261
|
|
|
@@ -535,6 +582,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
535
582
|
outputSchema,
|
|
536
583
|
sessionFile,
|
|
537
584
|
spawnsEnv,
|
|
585
|
+
enableLsp,
|
|
586
|
+
serializedAuth: options.authStorage?.serialize(),
|
|
587
|
+
serializedModels: options.modelRegistry?.serialize(),
|
|
588
|
+
mcpTools: options.mcpManager ? extractMCPToolMetadata(options.mcpManager) : undefined,
|
|
538
589
|
},
|
|
539
590
|
};
|
|
540
591
|
|
|
@@ -556,9 +607,43 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
556
607
|
cleanup();
|
|
557
608
|
resolve(message);
|
|
558
609
|
};
|
|
610
|
+
const handleMCPCall = async (request: MCPToolCallRequest) => {
|
|
611
|
+
const mcpManager = options.mcpManager;
|
|
612
|
+
if (!mcpManager) {
|
|
613
|
+
worker.postMessage({
|
|
614
|
+
type: "mcp_tool_result",
|
|
615
|
+
callId: request.callId,
|
|
616
|
+
error: "MCP not available",
|
|
617
|
+
});
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
try {
|
|
621
|
+
const parsed = parseMCPToolName(request.toolName);
|
|
622
|
+
if (!parsed) throw new Error(`Invalid MCP tool name: ${request.toolName}`);
|
|
623
|
+
const connection = mcpManager.getConnection(parsed.serverName);
|
|
624
|
+
if (!connection) throw new Error(`MCP server not connected: ${parsed.serverName}`);
|
|
625
|
+
const result = await callTool(connection, parsed.toolName, request.params);
|
|
626
|
+
worker.postMessage({
|
|
627
|
+
type: "mcp_tool_result",
|
|
628
|
+
callId: request.callId,
|
|
629
|
+
result: { content: result.content ?? [], isError: result.isError },
|
|
630
|
+
});
|
|
631
|
+
} catch (error) {
|
|
632
|
+
worker.postMessage({
|
|
633
|
+
type: "mcp_tool_result",
|
|
634
|
+
callId: request.callId,
|
|
635
|
+
error: error instanceof Error ? error.message : String(error),
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
|
|
559
640
|
const onMessage = (event: WorkerMessageEvent<SubagentWorkerResponse>) => {
|
|
560
641
|
const message = event.data;
|
|
561
642
|
if (!message || resolved) return;
|
|
643
|
+
if (message.type === "mcp_tool_call") {
|
|
644
|
+
handleMCPCall(message as MCPToolCallRequest);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
562
647
|
if (message.type === "event") {
|
|
563
648
|
try {
|
|
564
649
|
processEvent(message.event);
|
|
@@ -349,12 +349,16 @@ export async function createTaskTool(
|
|
|
349
349
|
sessionFile,
|
|
350
350
|
persistArtifacts: !!artifactsDir,
|
|
351
351
|
artifactsDir: effectiveArtifactsDir,
|
|
352
|
+
enableLsp: false,
|
|
352
353
|
signal,
|
|
353
354
|
eventBus: undefined,
|
|
354
355
|
onProgress: (progress) => {
|
|
355
356
|
progressMap.set(index, structuredClone(progress));
|
|
356
357
|
emitProgress();
|
|
357
358
|
},
|
|
359
|
+
authStorage: session.authStorage,
|
|
360
|
+
modelRegistry: session.modelRegistry,
|
|
361
|
+
mcpManager: session.mcpManager,
|
|
358
362
|
});
|
|
359
363
|
},
|
|
360
364
|
signal,
|