@oh-my-pi/pi-coding-agent 13.7.6 → 13.8.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 +15 -0
- package/package.json +7 -7
- package/scripts/generate-docs-index.ts +3 -3
- package/src/capability/context-file.ts +6 -3
- package/src/capability/fs.ts +18 -0
- package/src/capability/index.ts +3 -2
- package/src/capability/types.ts +2 -0
- package/src/config/model-resolver.ts +14 -2
- package/src/discovery/agents-md.ts +3 -4
- package/src/discovery/agents.ts +104 -84
- package/src/discovery/builtin.ts +28 -15
- package/src/discovery/claude.ts +27 -9
- package/src/extensibility/skills.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/main.ts +4 -0
- package/src/patch/hashline.ts +113 -0
- package/src/patch/index.ts +13 -2
- package/src/prompts/tools/hashline.md +9 -10
- package/src/sdk.ts +4 -0
- package/src/session/agent-session.ts +17 -29
- package/src/tools/fetch.ts +152 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [13.8.0] - 2026-03-04
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Added `buildCompactHashlineDiffPreview()` function to generate compact diff previews for model-visible tool responses, collapsing long unchanged runs and consecutive additions/removals to show edit shape without full file content
|
|
9
|
+
- Added project-level discovery for `.agent/` and `.agents/` directories, walking up from cwd to repo root (matching behavior of other providers like `.omp`, `.claude`, `.codex`). Applies to skills, rules, prompts, commands, context files (AGENTS.md), and system prompts (SYSTEM.md)
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Changed edit tool response to include diff summary with line counts (+added -removed) and a compact diff preview instead of warnings-only output
|
|
14
|
+
- Limited auto context promotion to models with explicit `contextPromotionTarget`; models without a configured target now compact on overflow instead of switching to arbitrary larger models ([#282](https://github.com/can1357/oh-my-pi/issues/282))
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Fixed `:thinking` suffix in `modelRoles` config values silently breaking model resolution (e.g., `slow: anthropic/claude-opus-4-6:high`) and being stripped on Ctrl+P role cycling
|
|
19
|
+
|
|
5
20
|
## [13.7.6] - 2026-03-04
|
|
6
21
|
### Added
|
|
7
22
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "13.
|
|
4
|
+
"version": "13.8.0",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@mozilla/readability": "^0.6",
|
|
44
|
-
"@oh-my-pi/omp-stats": "13.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "13.
|
|
46
|
-
"@oh-my-pi/pi-ai": "13.
|
|
47
|
-
"@oh-my-pi/pi-natives": "13.
|
|
48
|
-
"@oh-my-pi/pi-tui": "13.
|
|
49
|
-
"@oh-my-pi/pi-utils": "13.
|
|
44
|
+
"@oh-my-pi/omp-stats": "13.8.0",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "13.8.0",
|
|
46
|
+
"@oh-my-pi/pi-ai": "13.8.0",
|
|
47
|
+
"@oh-my-pi/pi-natives": "13.8.0",
|
|
48
|
+
"@oh-my-pi/pi-tui": "13.8.0",
|
|
49
|
+
"@oh-my-pi/pi-utils": "13.8.0",
|
|
50
50
|
"@sinclair/typebox": "^0.34",
|
|
51
51
|
"@xterm/headless": "^6.0",
|
|
52
52
|
"ajv": "^8.18",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import { Glob } from "bun";
|
|
4
3
|
import * as path from "node:path";
|
|
4
|
+
import { Glob } from "bun";
|
|
5
5
|
|
|
6
6
|
const docsDir = path.resolve(import.meta.dir, "../../../docs");
|
|
7
7
|
const outputPath = path.resolve(import.meta.dir, "../src/internal-urls/docs-index.generated.ts");
|
|
@@ -14,10 +14,10 @@ for await (const relativePath of glob.scan(docsDir)) {
|
|
|
14
14
|
entries.sort();
|
|
15
15
|
|
|
16
16
|
const docsWithContent = await Promise.all(
|
|
17
|
-
entries.map(async
|
|
17
|
+
entries.map(async relativePath => ({
|
|
18
18
|
relativePath,
|
|
19
19
|
content: await Bun.file(path.join(docsDir, relativePath)).text(),
|
|
20
|
-
}))
|
|
20
|
+
})),
|
|
21
21
|
);
|
|
22
22
|
|
|
23
23
|
const filenamesLiteral = JSON.stringify(entries);
|
|
@@ -27,9 +27,12 @@ export const contextFileCapability = defineCapability<ContextFile>({
|
|
|
27
27
|
id: "context-files",
|
|
28
28
|
displayName: "Context Files",
|
|
29
29
|
description: "Persistent instruction files (CLAUDE.md, AGENTS.md, etc.) that guide agent behavior",
|
|
30
|
-
// Deduplicate by
|
|
31
|
-
//
|
|
32
|
-
|
|
30
|
+
// Deduplicate by scope: one user-level file, and one project-level file per directory depth.
|
|
31
|
+
// Within each depth level, higher-priority providers shadow lower-priority ones.
|
|
32
|
+
// This supports monorepo hierarchies where AGENTS.md exists at multiple ancestor levels.
|
|
33
|
+
// Clamp depth >= 0: files inside config subdirectories of an ancestor (e.g. .claude/, .github/)
|
|
34
|
+
// are same-scope as the ancestor itself.
|
|
35
|
+
key: file => (file.level === "user" ? "user" : `project:${Math.max(0, file.depth ?? 0)}`),
|
|
33
36
|
validate: file => {
|
|
34
37
|
if (!file.path) return "Missing path";
|
|
35
38
|
if (file.content === undefined) return "Missing content";
|
package/src/capability/fs.ts
CHANGED
|
@@ -66,6 +66,24 @@ export async function walkUp(
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Walk up from startDir looking for a `.git` entry (file or directory).
|
|
71
|
+
* Returns the directory containing `.git` (the repo root), or null if not in a git repo.
|
|
72
|
+
* Results are based on the cached readDirEntries, so repeated calls are cheap.
|
|
73
|
+
*/
|
|
74
|
+
export async function findRepoRoot(startDir: string): Promise<string | null> {
|
|
75
|
+
let current = resolvePath(startDir);
|
|
76
|
+
while (true) {
|
|
77
|
+
const entries = await readDirEntries(current);
|
|
78
|
+
if (entries.some(e => e.name === ".git")) {
|
|
79
|
+
return current;
|
|
80
|
+
}
|
|
81
|
+
const parent = path.dirname(current);
|
|
82
|
+
if (parent === current) return null;
|
|
83
|
+
current = parent;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
69
87
|
export function cacheStats(): { content: number; dir: number } {
|
|
70
88
|
return {
|
|
71
89
|
content: contentCache.size,
|
package/src/capability/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ import * as path from "node:path";
|
|
|
11
11
|
import { getProjectDir, logger } from "@oh-my-pi/pi-utils";
|
|
12
12
|
|
|
13
13
|
import type { Settings } from "../config/settings";
|
|
14
|
-
import { clearCache as clearFsCache, cacheStats as fsCacheStats, invalidate as invalidateFs } from "./fs";
|
|
14
|
+
import { clearCache as clearFsCache, findRepoRoot, cacheStats as fsCacheStats, invalidate as invalidateFs } from "./fs";
|
|
15
15
|
import type {
|
|
16
16
|
Capability,
|
|
17
17
|
CapabilityInfo,
|
|
@@ -220,7 +220,8 @@ export async function loadCapability<T>(capabilityId: string, options: LoadOptio
|
|
|
220
220
|
|
|
221
221
|
const cwd = options.cwd ?? getProjectDir();
|
|
222
222
|
const home = os.homedir();
|
|
223
|
-
const
|
|
223
|
+
const repoRoot = await findRepoRoot(cwd);
|
|
224
|
+
const ctx: LoadContext = { cwd, home, repoRoot };
|
|
224
225
|
const providers = filterProviders(capability, options);
|
|
225
226
|
|
|
226
227
|
return await loadImpl(capability, providers, ctx, options);
|
package/src/capability/types.ts
CHANGED
|
@@ -23,10 +23,22 @@ export interface ScopedModel {
|
|
|
23
23
|
* Parse a model string in "provider/modelId" format.
|
|
24
24
|
* Returns undefined if the format is invalid.
|
|
25
25
|
*/
|
|
26
|
-
export function parseModelString(
|
|
26
|
+
export function parseModelString(
|
|
27
|
+
modelStr: string,
|
|
28
|
+
): { provider: string; id: string; thinkingLevel?: ThinkingLevel } | undefined {
|
|
27
29
|
const slashIdx = modelStr.indexOf("/");
|
|
28
30
|
if (slashIdx <= 0) return undefined;
|
|
29
|
-
|
|
31
|
+
const id = modelStr.slice(slashIdx + 1);
|
|
32
|
+
const provider = modelStr.slice(0, slashIdx);
|
|
33
|
+
// Strip valid thinking level suffix (e.g., "claude-sonnet-4-6:high" -> id "claude-sonnet-4-6", thinkingLevel "high")
|
|
34
|
+
const colonIdx = id.lastIndexOf(":");
|
|
35
|
+
if (colonIdx !== -1) {
|
|
36
|
+
const suffix = id.slice(colonIdx + 1);
|
|
37
|
+
if (isValidThinkingLevel(suffix)) {
|
|
38
|
+
return { provider, id: id.slice(0, colonIdx), thinkingLevel: suffix };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { provider, id };
|
|
30
42
|
}
|
|
31
43
|
|
|
32
44
|
/**
|
|
@@ -14,7 +14,6 @@ import { calculateDepth, createSourceMeta } from "./helpers";
|
|
|
14
14
|
|
|
15
15
|
const PROVIDER_ID = "agents-md";
|
|
16
16
|
const DISPLAY_NAME = "AGENTS.md";
|
|
17
|
-
const MAX_DEPTH = 20; // Prevent walking up excessively far from cwd
|
|
18
17
|
|
|
19
18
|
/**
|
|
20
19
|
* Load standalone AGENTS.md files.
|
|
@@ -25,9 +24,8 @@ async function loadAgentsMd(ctx: LoadContext): Promise<LoadResult<ContextFile>>
|
|
|
25
24
|
|
|
26
25
|
// Walk up from cwd looking for AGENTS.md files
|
|
27
26
|
let current = ctx.cwd;
|
|
28
|
-
let depth = 0;
|
|
29
27
|
|
|
30
|
-
while (
|
|
28
|
+
while (true) {
|
|
31
29
|
const candidate = path.join(current, "AGENTS.md");
|
|
32
30
|
const content = await readFile(candidate);
|
|
33
31
|
|
|
@@ -49,11 +47,12 @@ async function loadAgentsMd(ctx: LoadContext): Promise<LoadResult<ContextFile>>
|
|
|
49
47
|
}
|
|
50
48
|
}
|
|
51
49
|
|
|
50
|
+
if (current === (ctx.repoRoot ?? ctx.home)) break; // scanned repo root or home, stop
|
|
51
|
+
|
|
52
52
|
// Move to parent directory
|
|
53
53
|
const parent = path.dirname(current);
|
|
54
54
|
if (parent === current) break; // Reached filesystem root
|
|
55
55
|
current = parent;
|
|
56
|
-
depth++;
|
|
57
56
|
}
|
|
58
57
|
|
|
59
58
|
return { items, warnings };
|
package/src/discovery/agents.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agents (standard) Provider
|
|
3
3
|
*
|
|
4
|
-
* Loads
|
|
4
|
+
* Loads skills, rules, prompts, commands, context files, and system prompts
|
|
5
|
+
* from .agent/ and .agents/ directories at both user (~/) and project levels.
|
|
6
|
+
* Project-level discovery walks up from cwd to repoRoot.
|
|
5
7
|
*/
|
|
6
8
|
import * as path from "node:path";
|
|
7
9
|
import { registerProvider } from "../capability";
|
|
@@ -13,76 +15,97 @@ import { type Skill, skillCapability } from "../capability/skill";
|
|
|
13
15
|
import { type SlashCommand, slashCommandCapability } from "../capability/slash-command";
|
|
14
16
|
import { type SystemPrompt, systemPromptCapability } from "../capability/system-prompt";
|
|
15
17
|
import type { LoadContext, LoadResult } from "../capability/types";
|
|
16
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
buildRuleFromMarkdown,
|
|
20
|
+
calculateDepth,
|
|
21
|
+
createSourceMeta,
|
|
22
|
+
loadFilesFromDir,
|
|
23
|
+
scanSkillsFromDir,
|
|
24
|
+
} from "./helpers";
|
|
17
25
|
|
|
18
26
|
const PROVIDER_ID = "agents";
|
|
19
27
|
const DISPLAY_NAME = "Agents (standard)";
|
|
20
28
|
const PRIORITY = 70;
|
|
21
|
-
const
|
|
29
|
+
const AGENT_DIR_CANDIDATES = [".agent", ".agents"] as const;
|
|
22
30
|
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
/** User-level paths: ~/.agent/<segments> and ~/.agents/<segments>. */
|
|
32
|
+
function getUserPathCandidates(ctx: LoadContext, ...segments: string[]): string[] {
|
|
33
|
+
return AGENT_DIR_CANDIDATES.map(baseDir => path.join(ctx.home, baseDir, ...segments));
|
|
25
34
|
}
|
|
26
35
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
/** Project-level paths: walk up from cwd to repoRoot, returning .agent/<segments> and .agents/<segments> at each level. */
|
|
37
|
+
function getProjectPathCandidates(ctx: LoadContext, ...segments: string[]): string[] {
|
|
38
|
+
const paths: string[] = [];
|
|
39
|
+
let current = ctx.cwd;
|
|
40
|
+
while (true) {
|
|
41
|
+
for (const baseDir of AGENT_DIR_CANDIDATES) {
|
|
42
|
+
paths.push(path.join(current, baseDir, ...segments));
|
|
43
|
+
}
|
|
44
|
+
if (current === (ctx.repoRoot ?? ctx.home)) break;
|
|
45
|
+
const parent = path.dirname(current);
|
|
46
|
+
if (parent === current) break;
|
|
47
|
+
current = parent;
|
|
38
48
|
}
|
|
49
|
+
return paths;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Skills
|
|
53
|
+
async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
|
|
54
|
+
const projectScans = getProjectPathCandidates(ctx, "skills").map(dir =>
|
|
55
|
+
scanSkillsFromDir(ctx, { dir, providerId: PROVIDER_ID, level: "project" }),
|
|
56
|
+
);
|
|
57
|
+
const userScans = getUserPathCandidates(ctx, "skills").map(dir =>
|
|
58
|
+
scanSkillsFromDir(ctx, { dir, providerId: PROVIDER_ID, level: "user" }),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const results = await Promise.all([...projectScans, ...userScans]);
|
|
62
|
+
|
|
39
63
|
return {
|
|
40
|
-
items,
|
|
41
|
-
warnings,
|
|
64
|
+
items: results.flatMap(r => r.items),
|
|
65
|
+
warnings: results.flatMap(r => r.warnings ?? []),
|
|
42
66
|
};
|
|
43
67
|
}
|
|
44
68
|
|
|
45
69
|
registerProvider<Skill>(skillCapability.id, {
|
|
46
70
|
id: PROVIDER_ID,
|
|
47
71
|
displayName: DISPLAY_NAME,
|
|
48
|
-
description: "Load skills from
|
|
72
|
+
description: "Load skills from .agent/skills and .agents/skills (project walk-up + user home)",
|
|
49
73
|
priority: PRIORITY,
|
|
50
74
|
load: loadSkills,
|
|
51
75
|
});
|
|
52
76
|
|
|
53
77
|
// Rules
|
|
54
78
|
async function loadRules(ctx: LoadContext): Promise<LoadResult<Rule>> {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
for (const userRulesDir of getUserAgentPathCandidates(ctx, "rules")) {
|
|
58
|
-
const result = await loadFilesFromDir<Rule>(ctx, userRulesDir, PROVIDER_ID, "user", {
|
|
79
|
+
const load = (dir: string, level: "user" | "project") =>
|
|
80
|
+
loadFilesFromDir<Rule>(ctx, dir, PROVIDER_ID, level, {
|
|
59
81
|
extensions: ["md", "mdc"],
|
|
60
82
|
transform: (name, content, filePath, source) =>
|
|
61
83
|
buildRuleFromMarkdown(name, content, filePath, source, { stripNamePattern: /\.(md|mdc)$/ }),
|
|
62
84
|
});
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
85
|
+
|
|
86
|
+
const results = await Promise.all([
|
|
87
|
+
...getProjectPathCandidates(ctx, "rules").map(dir => load(dir, "project")),
|
|
88
|
+
...getUserPathCandidates(ctx, "rules").map(dir => load(dir, "user")),
|
|
89
|
+
]);
|
|
90
|
+
|
|
66
91
|
return {
|
|
67
|
-
items,
|
|
68
|
-
warnings,
|
|
92
|
+
items: results.flatMap(r => r.items),
|
|
93
|
+
warnings: results.flatMap(r => r.warnings ?? []),
|
|
69
94
|
};
|
|
70
95
|
}
|
|
71
96
|
|
|
72
97
|
registerProvider<Rule>(ruleCapability.id, {
|
|
73
98
|
id: PROVIDER_ID,
|
|
74
99
|
displayName: DISPLAY_NAME,
|
|
75
|
-
description: "Load rules from
|
|
100
|
+
description: "Load rules from .agent/rules and .agents/rules (project walk-up + user home)",
|
|
76
101
|
priority: PRIORITY,
|
|
77
102
|
load: loadRules,
|
|
78
103
|
});
|
|
79
104
|
|
|
80
105
|
// Prompts
|
|
81
106
|
async function loadPrompts(ctx: LoadContext): Promise<LoadResult<Prompt>> {
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
for (const userPromptsDir of getUserAgentPathCandidates(ctx, "prompts")) {
|
|
85
|
-
const result = await loadFilesFromDir<Prompt>(ctx, userPromptsDir, PROVIDER_ID, "user", {
|
|
107
|
+
const load = (dir: string, level: "user" | "project") =>
|
|
108
|
+
loadFilesFromDir<Prompt>(ctx, dir, PROVIDER_ID, level, {
|
|
86
109
|
extensions: ["md"],
|
|
87
110
|
transform: (name, content, filePath, source) => ({
|
|
88
111
|
name: name.replace(/\.md$/, ""),
|
|
@@ -91,109 +114,106 @@ async function loadPrompts(ctx: LoadContext): Promise<LoadResult<Prompt>> {
|
|
|
91
114
|
_source: source,
|
|
92
115
|
}),
|
|
93
116
|
});
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
117
|
+
|
|
118
|
+
const results = await Promise.all([
|
|
119
|
+
...getProjectPathCandidates(ctx, "prompts").map(dir => load(dir, "project")),
|
|
120
|
+
...getUserPathCandidates(ctx, "prompts").map(dir => load(dir, "user")),
|
|
121
|
+
]);
|
|
122
|
+
|
|
97
123
|
return {
|
|
98
|
-
items,
|
|
99
|
-
warnings,
|
|
124
|
+
items: results.flatMap(r => r.items),
|
|
125
|
+
warnings: results.flatMap(r => r.warnings ?? []),
|
|
100
126
|
};
|
|
101
127
|
}
|
|
102
128
|
|
|
103
129
|
registerProvider<Prompt>(promptCapability.id, {
|
|
104
130
|
id: PROVIDER_ID,
|
|
105
131
|
displayName: DISPLAY_NAME,
|
|
106
|
-
description: "Load prompts from
|
|
132
|
+
description: "Load prompts from .agent/prompts and .agents/prompts (project walk-up + user home)",
|
|
107
133
|
priority: PRIORITY,
|
|
108
134
|
load: loadPrompts,
|
|
109
135
|
});
|
|
110
136
|
|
|
111
137
|
// Slash Commands
|
|
112
138
|
async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashCommand>> {
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
for (const userCommandsDir of getUserAgentPathCandidates(ctx, "commands")) {
|
|
116
|
-
const result = await loadFilesFromDir<SlashCommand>(ctx, userCommandsDir, PROVIDER_ID, "user", {
|
|
139
|
+
const load = (dir: string, level: "user" | "project") =>
|
|
140
|
+
loadFilesFromDir<SlashCommand>(ctx, dir, PROVIDER_ID, level, {
|
|
117
141
|
extensions: ["md"],
|
|
118
142
|
transform: (name, content, filePath, source) => ({
|
|
119
143
|
name: name.replace(/\.md$/, ""),
|
|
120
144
|
path: filePath,
|
|
121
145
|
content,
|
|
122
|
-
level
|
|
146
|
+
level,
|
|
123
147
|
_source: source,
|
|
124
148
|
}),
|
|
125
149
|
});
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
150
|
+
|
|
151
|
+
const results = await Promise.all([
|
|
152
|
+
...getProjectPathCandidates(ctx, "commands").map(dir => load(dir, "project")),
|
|
153
|
+
...getUserPathCandidates(ctx, "commands").map(dir => load(dir, "user")),
|
|
154
|
+
]);
|
|
155
|
+
|
|
129
156
|
return {
|
|
130
|
-
items,
|
|
131
|
-
warnings,
|
|
157
|
+
items: results.flatMap(r => r.items),
|
|
158
|
+
warnings: results.flatMap(r => r.warnings ?? []),
|
|
132
159
|
};
|
|
133
160
|
}
|
|
134
161
|
|
|
135
162
|
registerProvider<SlashCommand>(slashCommandCapability.id, {
|
|
136
163
|
id: PROVIDER_ID,
|
|
137
164
|
displayName: DISPLAY_NAME,
|
|
138
|
-
description: "Load commands from
|
|
165
|
+
description: "Load commands from .agent/commands and .agents/commands (project walk-up + user home)",
|
|
139
166
|
priority: PRIORITY,
|
|
140
167
|
load: loadSlashCommands,
|
|
141
168
|
});
|
|
142
169
|
|
|
143
170
|
// Context Files (AGENTS.md)
|
|
144
171
|
async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFile>> {
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
path: agentsPath,
|
|
153
|
-
content,
|
|
154
|
-
level: "user",
|
|
155
|
-
_source: createSourceMeta(PROVIDER_ID, agentsPath, "user"),
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
return {
|
|
159
|
-
items,
|
|
160
|
-
warnings: [],
|
|
172
|
+
const load = async (filePath: string, level: "user" | "project"): Promise<ContextFile | null> => {
|
|
173
|
+
const content = await readFile(filePath);
|
|
174
|
+
if (!content) return null;
|
|
175
|
+
// filePath is <ancestor>/.agent(s)/AGENTS.md — go up past the config dir to the ancestor
|
|
176
|
+
const ancestorDir = path.dirname(path.dirname(filePath));
|
|
177
|
+
const depth = level === "project" ? calculateDepth(ctx.cwd, ancestorDir, path.sep) : undefined;
|
|
178
|
+
return { path: filePath, content, level, depth, _source: createSourceMeta(PROVIDER_ID, filePath, level) };
|
|
161
179
|
};
|
|
180
|
+
|
|
181
|
+
const results = await Promise.all([
|
|
182
|
+
...getProjectPathCandidates(ctx, "AGENTS.md").map(p => load(p, "project")),
|
|
183
|
+
...getUserPathCandidates(ctx, "AGENTS.md").map(p => load(p, "user")),
|
|
184
|
+
]);
|
|
185
|
+
|
|
186
|
+
return { items: results.filter((r): r is ContextFile => r !== null), warnings: [] };
|
|
162
187
|
}
|
|
163
188
|
|
|
164
189
|
registerProvider<ContextFile>(contextFileCapability.id, {
|
|
165
190
|
id: PROVIDER_ID,
|
|
166
191
|
displayName: DISPLAY_NAME,
|
|
167
|
-
description: "Load AGENTS.md from
|
|
192
|
+
description: "Load AGENTS.md from .agent and .agents (project walk-up + user home)",
|
|
168
193
|
priority: PRIORITY,
|
|
169
194
|
load: loadContextFiles,
|
|
170
195
|
});
|
|
171
196
|
|
|
172
197
|
// System Prompt (SYSTEM.md)
|
|
173
198
|
async function loadSystemPrompt(ctx: LoadContext): Promise<LoadResult<SystemPrompt>> {
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
items.push({
|
|
181
|
-
path: systemPath,
|
|
182
|
-
content,
|
|
183
|
-
level: "user",
|
|
184
|
-
_source: createSourceMeta(PROVIDER_ID, systemPath, "user"),
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
return {
|
|
188
|
-
items,
|
|
189
|
-
warnings: [],
|
|
199
|
+
const load = async (filePath: string, level: "user" | "project"): Promise<SystemPrompt | null> => {
|
|
200
|
+
const content = await readFile(filePath);
|
|
201
|
+
if (!content) return null;
|
|
202
|
+
return { path: filePath, content, level, _source: createSourceMeta(PROVIDER_ID, filePath, level) };
|
|
190
203
|
};
|
|
204
|
+
|
|
205
|
+
const results = await Promise.all([
|
|
206
|
+
...getProjectPathCandidates(ctx, "SYSTEM.md").map(p => load(p, "project")),
|
|
207
|
+
...getUserPathCandidates(ctx, "SYSTEM.md").map(p => load(p, "user")),
|
|
208
|
+
]);
|
|
209
|
+
|
|
210
|
+
return { items: results.filter((r): r is SystemPrompt => r !== null), warnings: [] };
|
|
191
211
|
}
|
|
192
212
|
|
|
193
213
|
registerProvider<SystemPrompt>(systemPromptCapability.id, {
|
|
194
214
|
id: PROVIDER_ID,
|
|
195
215
|
displayName: DISPLAY_NAME,
|
|
196
|
-
description: "Load SYSTEM.md from
|
|
216
|
+
description: "Load SYSTEM.md from .agent and .agents (project walk-up + user home)",
|
|
197
217
|
priority: PRIORITY,
|
|
198
218
|
load: loadSystemPrompt,
|
|
199
219
|
});
|
package/src/discovery/builtin.ts
CHANGED
|
@@ -68,12 +68,13 @@ async function getConfigDirs(ctx: LoadContext): Promise<Array<{ dir: string; lev
|
|
|
68
68
|
return result;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
function getAncestorDirs(cwd: string): Array<{ dir: string; depth: number }> {
|
|
71
|
+
function getAncestorDirs(cwd: string, stopAt?: string | null): Array<{ dir: string; depth: number }> {
|
|
72
72
|
const ancestors: Array<{ dir: string; depth: number }> = [];
|
|
73
73
|
let current = cwd;
|
|
74
74
|
let depth = 0;
|
|
75
75
|
while (true) {
|
|
76
76
|
ancestors.push({ dir: current, depth });
|
|
77
|
+
if (stopAt && current === stopAt) break;
|
|
77
78
|
const parent = path.dirname(current);
|
|
78
79
|
if (parent === current) break;
|
|
79
80
|
current = parent;
|
|
@@ -82,8 +83,11 @@ function getAncestorDirs(cwd: string): Array<{ dir: string; depth: number }> {
|
|
|
82
83
|
return ancestors;
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
async function findNearestProjectConfigDir(
|
|
86
|
-
|
|
86
|
+
async function findNearestProjectConfigDir(
|
|
87
|
+
cwd: string,
|
|
88
|
+
repoRoot?: string | null,
|
|
89
|
+
): Promise<{ dir: string; depth: number } | null> {
|
|
90
|
+
for (const ancestor of getAncestorDirs(cwd, repoRoot)) {
|
|
87
91
|
const configDir = await ifNonEmptyDir(ancestor.dir, PATHS.projectDir);
|
|
88
92
|
if (configDir) return { dir: configDir, depth: ancestor.depth };
|
|
89
93
|
}
|
|
@@ -215,7 +219,7 @@ async function loadSystemPrompt(ctx: LoadContext): Promise<LoadResult<SystemProm
|
|
|
215
219
|
});
|
|
216
220
|
}
|
|
217
221
|
|
|
218
|
-
const nearestProjectConfigDir = await findNearestProjectConfigDir(ctx.cwd);
|
|
222
|
+
const nearestProjectConfigDir = await findNearestProjectConfigDir(ctx.cwd, ctx.repoRoot);
|
|
219
223
|
if (nearestProjectConfigDir) {
|
|
220
224
|
const projectPath = path.join(nearestProjectConfigDir.dir, "SYSTEM.md");
|
|
221
225
|
const projectContent = await readFile(projectPath);
|
|
@@ -242,18 +246,27 @@ registerProvider<SystemPrompt>(systemPromptCapability.id, {
|
|
|
242
246
|
|
|
243
247
|
// Skills
|
|
244
248
|
async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
),
|
|
249
|
+
// Walk up from cwd finding .omp/skills/ in ancestors (closest first)
|
|
250
|
+
const ancestors = getAncestorDirs(ctx.cwd, ctx.repoRoot ?? ctx.home);
|
|
251
|
+
const projectScans = ancestors.map(({ dir }) =>
|
|
252
|
+
scanSkillsFromDir(ctx, {
|
|
253
|
+
dir: path.join(dir, PATHS.projectDir, "skills"),
|
|
254
|
+
providerId: PROVIDER_ID,
|
|
255
|
+
level: "project",
|
|
256
|
+
requireDescription: true,
|
|
257
|
+
}),
|
|
255
258
|
);
|
|
256
259
|
|
|
260
|
+
// User-level scan from ~/.omp/agent/skills/
|
|
261
|
+
const userScan = scanSkillsFromDir(ctx, {
|
|
262
|
+
dir: path.join(ctx.home, PATHS.userAgent, "skills"),
|
|
263
|
+
providerId: PROVIDER_ID,
|
|
264
|
+
level: "user",
|
|
265
|
+
requireDescription: true,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const results = await Promise.all([...projectScans, userScan]);
|
|
269
|
+
|
|
257
270
|
return {
|
|
258
271
|
items: results.flatMap(r => r.items),
|
|
259
272
|
warnings: results.flatMap(r => r.warnings ?? []),
|
|
@@ -795,7 +808,7 @@ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFil
|
|
|
795
808
|
});
|
|
796
809
|
}
|
|
797
810
|
|
|
798
|
-
const nearestProjectConfigDir = await findNearestProjectConfigDir(ctx.cwd);
|
|
811
|
+
const nearestProjectConfigDir = await findNearestProjectConfigDir(ctx.cwd, ctx.repoRoot);
|
|
799
812
|
if (nearestProjectConfigDir) {
|
|
800
813
|
const projectPath = path.join(nearestProjectConfigDir.dir, "AGENTS.md");
|
|
801
814
|
const projectContent = await readFile(projectPath);
|
package/src/discovery/claude.ts
CHANGED
|
@@ -145,7 +145,7 @@ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFil
|
|
|
145
145
|
const projectClaudeMd = path.join(projectBase, "CLAUDE.md");
|
|
146
146
|
const projectContent = await readFile(projectClaudeMd);
|
|
147
147
|
if (projectContent !== null) {
|
|
148
|
-
const depth = calculateDepth(ctx.cwd, projectBase, path.sep);
|
|
148
|
+
const depth = calculateDepth(ctx.cwd, path.dirname(projectBase), path.sep);
|
|
149
149
|
items.push({
|
|
150
150
|
path: projectClaudeMd,
|
|
151
151
|
content: projectContent,
|
|
@@ -164,11 +164,27 @@ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFil
|
|
|
164
164
|
|
|
165
165
|
async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
|
|
166
166
|
const userSkillsDir = path.join(getUserClaude(ctx), "skills");
|
|
167
|
-
const projectSkillsDir = path.join(getProjectClaude(ctx), "skills");
|
|
168
167
|
|
|
169
|
-
|
|
168
|
+
// Walk up from cwd finding .claude/skills/ in ancestors
|
|
169
|
+
const projectScans: Promise<LoadResult<Skill>>[] = [];
|
|
170
|
+
let current = ctx.cwd;
|
|
171
|
+
while (true) {
|
|
172
|
+
projectScans.push(
|
|
173
|
+
scanSkillsFromDir(ctx, {
|
|
174
|
+
dir: path.join(current, CONFIG_DIR, "skills"),
|
|
175
|
+
providerId: PROVIDER_ID,
|
|
176
|
+
level: "project",
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
if (current === (ctx.repoRoot ?? ctx.home)) break;
|
|
180
|
+
const parent = path.dirname(current);
|
|
181
|
+
if (parent === current) break; // filesystem root
|
|
182
|
+
current = parent;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const [userResult, ...projectResults] = await Promise.allSettled([
|
|
170
186
|
scanSkillsFromDir(ctx, { dir: userSkillsDir, providerId: PROVIDER_ID, level: "user" }),
|
|
171
|
-
|
|
187
|
+
...projectScans,
|
|
172
188
|
]);
|
|
173
189
|
|
|
174
190
|
const items: Skill[] = [];
|
|
@@ -181,11 +197,13 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
|
|
|
181
197
|
warnings.push(`Failed to scan Claude user skills in ${userSkillsDir}: ${String(userResult.reason)}`);
|
|
182
198
|
}
|
|
183
199
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
200
|
+
for (const projectResult of projectResults) {
|
|
201
|
+
if (projectResult.status === "fulfilled") {
|
|
202
|
+
items.push(...projectResult.value.items);
|
|
203
|
+
warnings.push(...(projectResult.value.warnings ?? []));
|
|
204
|
+
} else if (!isMissingDirectoryError(projectResult.reason)) {
|
|
205
|
+
warnings.push(`Failed to scan Claude project skills: ${String(projectResult.reason)}`);
|
|
206
|
+
}
|
|
189
207
|
}
|
|
190
208
|
|
|
191
209
|
return { items, warnings };
|