@oh-my-pi/pi-coding-agent 4.2.0 → 4.2.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 +46 -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 +7 -7
- package/src/core/agent-storage.ts +50 -0
- package/src/core/auth-storage.ts +102 -3
- package/src/core/bash-executor.ts +1 -1
- package/src/core/custom-tools/loader.ts +2 -2
- package/src/core/export-html/index.ts +1 -33
- 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 +52 -10
- package/src/core/tools/complete.ts +5 -2
- package/src/core/tools/edit.ts +7 -4
- 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 -9
- 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 +368 -293
- package/src/discovery/claude.ts +183 -345
- package/src/discovery/cline.ts +30 -10
- package/src/discovery/codex.ts +188 -272
- 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 +114 -57
- 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 +9 -5
- package/src/modes/interactive/interactive-mode.ts +22 -15
- package/src/prompts/agents/plan.md +107 -30
- package/src/prompts/agents/task.md +5 -4
- package/src/prompts/system/system-prompt.md +5 -0
- package/src/prompts/tools/task.md +25 -19
- package/src/utils/shell.ts +2 -2
- package/src/prompts/agents/architect-plan.md +0 -10
- package/src/prompts/agents/implement-with-critic.md +0 -11
- package/src/prompts/agents/implement.md +0 -11
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";
|
|
@@ -148,6 +148,45 @@ function stripQuotes(value: string): string {
|
|
|
148
148
|
return value.replace(/^"|"$/g, "");
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
const AGENTS_MD_PATTERN = "**/AGENTS.md";
|
|
152
|
+
const AGENTS_MD_LIMIT = 200;
|
|
153
|
+
|
|
154
|
+
interface AgentsMdSearch {
|
|
155
|
+
scopePath: string;
|
|
156
|
+
limit: number;
|
|
157
|
+
pattern: string;
|
|
158
|
+
files: string[];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function normalizePath(value: string): string {
|
|
162
|
+
return value.replace(/\\/g, "/");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function listAgentsMdFiles(root: string, limit: number): string[] {
|
|
166
|
+
try {
|
|
167
|
+
const entries = Array.from(
|
|
168
|
+
new Bun.Glob(AGENTS_MD_PATTERN).scanSync({ cwd: root, onlyFiles: true, dot: false, absolute: false }),
|
|
169
|
+
);
|
|
170
|
+
const normalized = entries
|
|
171
|
+
.map((entry) => normalizePath(entry))
|
|
172
|
+
.filter((entry) => entry.length > 0 && !entry.includes("node_modules"))
|
|
173
|
+
.sort();
|
|
174
|
+
return normalized.length > limit ? normalized.slice(0, limit) : normalized;
|
|
175
|
+
} catch {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function buildAgentsMdSearch(cwd: string): AgentsMdSearch {
|
|
181
|
+
const files = listAgentsMdFiles(cwd, AGENTS_MD_LIMIT);
|
|
182
|
+
return {
|
|
183
|
+
scopePath: ".",
|
|
184
|
+
limit: AGENTS_MD_LIMIT,
|
|
185
|
+
pattern: AGENTS_MD_PATTERN,
|
|
186
|
+
files,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
151
190
|
function getOsName(): string {
|
|
152
191
|
switch (process.platform) {
|
|
153
192
|
case "win32":
|
|
@@ -519,12 +558,12 @@ export interface LoadContextFilesOptions {
|
|
|
519
558
|
* Returns {path, content, depth} entries for all discovered context files.
|
|
520
559
|
* Files are sorted by depth (descending) so files closer to cwd appear last/more prominent.
|
|
521
560
|
*/
|
|
522
|
-
export function loadProjectContextFiles(
|
|
561
|
+
export async function loadProjectContextFiles(
|
|
523
562
|
options: LoadContextFilesOptions = {},
|
|
524
|
-
): Array<{ path: string; content: string; depth?: number }
|
|
563
|
+
): Promise<Array<{ path: string; content: string; depth?: number }>> {
|
|
525
564
|
const resolvedCwd = options.cwd ?? process.cwd();
|
|
526
565
|
|
|
527
|
-
const result =
|
|
566
|
+
const result = await loadCapability(contextFileCapability.id, { cwd: resolvedCwd });
|
|
528
567
|
|
|
529
568
|
// Convert ContextFile items and preserve depth info
|
|
530
569
|
const files = result.items.map((item) => {
|
|
@@ -551,10 +590,10 @@ export function loadProjectContextFiles(
|
|
|
551
590
|
* Load system prompt customization files (SYSTEM.md).
|
|
552
591
|
* Returns combined content from all discovered SYSTEM.md files.
|
|
553
592
|
*/
|
|
554
|
-
export function loadSystemPromptFiles(options: LoadContextFilesOptions = {}): string | null {
|
|
593
|
+
export async function loadSystemPromptFiles(options: LoadContextFilesOptions = {}): Promise<string | null> {
|
|
555
594
|
const resolvedCwd = options.cwd ?? process.cwd();
|
|
556
595
|
|
|
557
|
-
const result =
|
|
596
|
+
const result = await loadCapability<SystemPromptFile>(systemPromptCapability.id, { cwd: resolvedCwd });
|
|
558
597
|
|
|
559
598
|
if (result.items.length === 0) return null;
|
|
560
599
|
|
|
@@ -592,7 +631,7 @@ export interface BuildSystemPromptOptions {
|
|
|
592
631
|
}
|
|
593
632
|
|
|
594
633
|
/** Build the system prompt with tools, guidelines, and context */
|
|
595
|
-
export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): string {
|
|
634
|
+
export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): Promise<string> {
|
|
596
635
|
const {
|
|
597
636
|
customPrompt,
|
|
598
637
|
tools,
|
|
@@ -609,7 +648,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|
|
609
648
|
const resolvedAppendPrompt = resolvePromptInput(appendSystemPrompt, "append system prompt");
|
|
610
649
|
|
|
611
650
|
// Load SYSTEM.md customization (prepended to prompt)
|
|
612
|
-
const systemPromptCustomization = loadSystemPromptFiles({ cwd: resolvedCwd });
|
|
651
|
+
const systemPromptCustomization = await loadSystemPromptFiles({ cwd: resolvedCwd });
|
|
613
652
|
|
|
614
653
|
const now = new Date();
|
|
615
654
|
const dateTime = now.toLocaleString("en-US", {
|
|
@@ -624,7 +663,8 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|
|
624
663
|
});
|
|
625
664
|
|
|
626
665
|
// Resolve context files: use provided or discover
|
|
627
|
-
const contextFiles = providedContextFiles ?? loadProjectContextFiles({ cwd: resolvedCwd });
|
|
666
|
+
const contextFiles = providedContextFiles ?? (await loadProjectContextFiles({ cwd: resolvedCwd }));
|
|
667
|
+
const agentsMdSearch = buildAgentsMdSearch(resolvedCwd);
|
|
628
668
|
|
|
629
669
|
// Build tool descriptions array
|
|
630
670
|
// Priority: toolNames (explicit list) > tools (Map) > defaults
|
|
@@ -648,7 +688,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|
|
648
688
|
// Resolve skills: use provided or discover
|
|
649
689
|
const skills =
|
|
650
690
|
providedSkills ??
|
|
651
|
-
(skillsSettings?.enabled !== false ? loadSkills({ ...skillsSettings, cwd: resolvedCwd }).skills : []);
|
|
691
|
+
(skillsSettings?.enabled !== false ? (await loadSkills({ ...skillsSettings, cwd: resolvedCwd })).skills : []);
|
|
652
692
|
|
|
653
693
|
// Get git context
|
|
654
694
|
const git = loadGitContext(resolvedCwd);
|
|
@@ -663,6 +703,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|
|
663
703
|
customPrompt: resolvedCustomPrompt,
|
|
664
704
|
appendPrompt: resolvedAppendPrompt ?? "",
|
|
665
705
|
contextFiles,
|
|
706
|
+
agentsMdSearch,
|
|
666
707
|
toolDescriptions: toolDescriptionsArray,
|
|
667
708
|
git,
|
|
668
709
|
skills: filteredSkills,
|
|
@@ -678,6 +719,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|
|
678
719
|
environment: getEnvironmentInfo(),
|
|
679
720
|
systemPromptCustomization: systemPromptCustomization ?? "",
|
|
680
721
|
contextFiles,
|
|
722
|
+
agentsMdSearch,
|
|
681
723
|
git,
|
|
682
724
|
skills: filteredSkills,
|
|
683
725
|
rules: rules ?? [],
|
|
@@ -79,7 +79,7 @@ export function createCompleteTool(session: ToolSession) {
|
|
|
79
79
|
: Type.Any({ description: "Structured JSON output (no schema specified)" });
|
|
80
80
|
|
|
81
81
|
const completeParams = Type.Object({
|
|
82
|
-
data: dataSchema,
|
|
82
|
+
data: Type.Optional(dataSchema),
|
|
83
83
|
status: Type.Optional(
|
|
84
84
|
Type.Union([Type.Literal("success"), Type.Literal("aborted")], {
|
|
85
85
|
default: "success",
|
|
@@ -99,8 +99,11 @@ export function createCompleteTool(session: ToolSession) {
|
|
|
99
99
|
execute: async (_toolCallId, params) => {
|
|
100
100
|
const status = params.status ?? "success";
|
|
101
101
|
|
|
102
|
-
// Skip
|
|
102
|
+
// Skip validation when aborting - data is optional for aborts
|
|
103
103
|
if (status === "success") {
|
|
104
|
+
if (params.data === undefined) {
|
|
105
|
+
throw new Error("data is required when status is 'success'");
|
|
106
|
+
}
|
|
104
107
|
if (schemaError) {
|
|
105
108
|
throw new Error(`Invalid output schema: ${schemaError}`);
|
|
106
109
|
}
|
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",
|
|
@@ -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,19 +6,13 @@
|
|
|
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
|
-
import architectPlanMd from "../../../prompts/agents/architect-plan.md" with { type: "text" };
|
|
13
|
-
import implementMd from "../../../prompts/agents/implement.md" with { type: "text" };
|
|
14
|
-
import implementWithCriticMd from "../../../prompts/agents/implement-with-critic.md" with { type: "text" };
|
|
15
12
|
import initMd from "../../../prompts/agents/init.md" with { type: "text" };
|
|
16
13
|
import { renderPromptTemplate } from "../../prompt-templates";
|
|
17
14
|
|
|
18
15
|
const EMBEDDED_COMMANDS: { name: string; content: string }[] = [
|
|
19
|
-
{ name: "architect-plan.md", content: renderPromptTemplate(architectPlanMd) },
|
|
20
|
-
{ name: "implement-with-critic.md", content: renderPromptTemplate(implementWithCriticMd) },
|
|
21
|
-
{ name: "implement.md", content: renderPromptTemplate(implementMd) },
|
|
22
16
|
{ name: "init.md", content: renderPromptTemplate(initMd) },
|
|
23
17
|
];
|
|
24
18
|
|
|
@@ -101,11 +95,11 @@ export function loadBundledCommands(): WorkflowCommand[] {
|
|
|
101
95
|
*
|
|
102
96
|
* Precedence (highest wins): .omp > .pi > .claude (project before user), then bundled
|
|
103
97
|
*/
|
|
104
|
-
export function discoverCommands(cwd: string): WorkflowCommand[] {
|
|
98
|
+
export async function discoverCommands(cwd: string): Promise<WorkflowCommand[]> {
|
|
105
99
|
const resolvedCwd = path.resolve(cwd);
|
|
106
100
|
|
|
107
101
|
// Load slash commands from capability API
|
|
108
|
-
const result =
|
|
102
|
+
const result = await loadCapability<SlashCommand>(slashCommandCapability.id, { cwd: resolvedCwd });
|
|
109
103
|
|
|
110
104
|
const commands: WorkflowCommand[] = [];
|
|
111
105
|
const seen = new Set<string>();
|