@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/discovery/builtin.ts
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
* .pi is an alias for backwards compatibility.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { dirname, isAbsolute, join, resolve } from "path";
|
|
9
9
|
import { type ContextFile, contextFileCapability } from "../capability/context-file";
|
|
10
10
|
import { type Extension, type ExtensionManifest, extensionCapability } from "../capability/extension";
|
|
11
11
|
import { type ExtensionModule, extensionModuleCapability } from "../capability/extension-module";
|
|
12
|
+
import { readDirEntries, readFile } from "../capability/fs";
|
|
12
13
|
import { type Hook, hookCapability } from "../capability/hook";
|
|
13
14
|
import { registerProvider } from "../capability/index";
|
|
14
15
|
import { type Instruction, instructionCapability } from "../capability/instruction";
|
|
@@ -16,7 +17,7 @@ import { type MCPServer, mcpCapability } from "../capability/mcp";
|
|
|
16
17
|
import { type Prompt, promptCapability } from "../capability/prompt";
|
|
17
18
|
import { type Rule, ruleCapability } from "../capability/rule";
|
|
18
19
|
import { type Settings, settingsCapability } from "../capability/settings";
|
|
19
|
-
import { type Skill,
|
|
20
|
+
import { type Skill, skillCapability } from "../capability/skill";
|
|
20
21
|
import { type SlashCommand, slashCommandCapability } from "../capability/slash-command";
|
|
21
22
|
import { type SystemPrompt, systemPromptCapability } from "../capability/system-prompt";
|
|
22
23
|
import { type CustomTool, toolCapability } from "../capability/tool";
|
|
@@ -27,6 +28,7 @@ import {
|
|
|
27
28
|
expandEnvVarsDeep,
|
|
28
29
|
getExtensionNameFromPath,
|
|
29
30
|
loadFilesFromDir,
|
|
31
|
+
loadSkillsFromDir,
|
|
30
32
|
parseFrontmatter,
|
|
31
33
|
parseJSON,
|
|
32
34
|
SOURCE_PATHS,
|
|
@@ -41,12 +43,13 @@ const PATHS = SOURCE_PATHS.native;
|
|
|
41
43
|
const PROJECT_DIRS = [PATHS.projectDir, ...PATHS.aliases];
|
|
42
44
|
const USER_DIRS = [PATHS.userBase, ...PATHS.aliases];
|
|
43
45
|
|
|
44
|
-
function getConfigDirs(ctx: LoadContext): Array<{ dir: string; level: "user" | "project" }
|
|
46
|
+
async function getConfigDirs(ctx: LoadContext): Promise<Array<{ dir: string; level: "user" | "project" }>> {
|
|
45
47
|
const result: Array<{ dir: string; level: "user" | "project" }> = [];
|
|
46
48
|
|
|
47
49
|
for (const name of PROJECT_DIRS) {
|
|
48
|
-
const projectDir = ctx.
|
|
49
|
-
|
|
50
|
+
const projectDir = join(ctx.cwd, name);
|
|
51
|
+
const entries = await readDirEntries(projectDir);
|
|
52
|
+
if (entries.length > 0) {
|
|
50
53
|
result.push({ dir: projectDir, level: "project" });
|
|
51
54
|
break;
|
|
52
55
|
}
|
|
@@ -54,7 +57,8 @@ function getConfigDirs(ctx: LoadContext): Array<{ dir: string; level: "user" | "
|
|
|
54
57
|
|
|
55
58
|
for (const name of USER_DIRS) {
|
|
56
59
|
const userDir = join(ctx.home, name, PATHS.userAgent.replace(`${PATHS.userBase}/`, ""));
|
|
57
|
-
|
|
60
|
+
const entries = await readDirEntries(userDir);
|
|
61
|
+
if (entries.length > 0) {
|
|
58
62
|
result.push({ dir: userDir, level: "user" });
|
|
59
63
|
break;
|
|
60
64
|
}
|
|
@@ -64,53 +68,19 @@ function getConfigDirs(ctx: LoadContext): Array<{ dir: string; level: "user" | "
|
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
// MCP
|
|
67
|
-
function loadMCPServers(ctx: LoadContext): LoadResult<MCPServer
|
|
71
|
+
async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>> {
|
|
68
72
|
const items: MCPServer[] = [];
|
|
69
73
|
const warnings: string[] = [];
|
|
70
74
|
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
if (!projectDir) continue;
|
|
74
|
-
|
|
75
|
-
for (const filename of ["mcp.json", ".mcp.json"]) {
|
|
76
|
-
const path = join(projectDir, filename);
|
|
77
|
-
const content = ctx.fs.readFile(path);
|
|
78
|
-
if (!content) continue;
|
|
79
|
-
|
|
80
|
-
const data = parseJSON<{ mcpServers?: Record<string, unknown> }>(content);
|
|
81
|
-
if (!data?.mcpServers) continue;
|
|
82
|
-
|
|
83
|
-
const expanded = expandEnvVarsDeep(data.mcpServers);
|
|
84
|
-
for (const [serverName, config] of Object.entries(expanded)) {
|
|
85
|
-
const serverConfig = config as Record<string, unknown>;
|
|
86
|
-
items.push({
|
|
87
|
-
name: serverName,
|
|
88
|
-
command: serverConfig.command as string | undefined,
|
|
89
|
-
args: serverConfig.args as string[] | undefined,
|
|
90
|
-
env: serverConfig.env as Record<string, string> | undefined,
|
|
91
|
-
url: serverConfig.url as string | undefined,
|
|
92
|
-
headers: serverConfig.headers as Record<string, string> | undefined,
|
|
93
|
-
transport: serverConfig.type as "stdio" | "sse" | "http" | undefined,
|
|
94
|
-
_source: createSourceMeta(PROVIDER_ID, path, "project"),
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
break;
|
|
98
|
-
}
|
|
99
|
-
break;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
for (const name of USER_DIRS) {
|
|
103
|
-
const userPath = join(ctx.home, name, "mcp.json");
|
|
104
|
-
const content = ctx.fs.readFile(userPath);
|
|
105
|
-
if (!content) continue;
|
|
106
|
-
|
|
75
|
+
const parseMcpServers = (content: string, path: string, level: "user" | "project"): MCPServer[] => {
|
|
76
|
+
const result: MCPServer[] = [];
|
|
107
77
|
const data = parseJSON<{ mcpServers?: Record<string, unknown> }>(content);
|
|
108
|
-
if (!data?.mcpServers)
|
|
78
|
+
if (!data?.mcpServers) return result;
|
|
109
79
|
|
|
110
80
|
const expanded = expandEnvVarsDeep(data.mcpServers);
|
|
111
81
|
for (const [serverName, config] of Object.entries(expanded)) {
|
|
112
82
|
const serverConfig = config as Record<string, unknown>;
|
|
113
|
-
|
|
83
|
+
result.push({
|
|
114
84
|
name: serverName,
|
|
115
85
|
command: serverConfig.command as string | undefined,
|
|
116
86
|
args: serverConfig.args as string[] | undefined,
|
|
@@ -118,10 +88,41 @@ function loadMCPServers(ctx: LoadContext): LoadResult<MCPServer> {
|
|
|
118
88
|
url: serverConfig.url as string | undefined,
|
|
119
89
|
headers: serverConfig.headers as Record<string, string> | undefined,
|
|
120
90
|
transport: serverConfig.type as "stdio" | "sse" | "http" | undefined,
|
|
121
|
-
_source: createSourceMeta(PROVIDER_ID,
|
|
91
|
+
_source: createSourceMeta(PROVIDER_ID, path, level),
|
|
122
92
|
});
|
|
123
93
|
}
|
|
124
|
-
|
|
94
|
+
return result;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const projectDirs = await Promise.all(
|
|
98
|
+
PROJECT_DIRS.map(async (name) => {
|
|
99
|
+
const dir = join(ctx.cwd, name);
|
|
100
|
+
const entries = await readDirEntries(dir);
|
|
101
|
+
return entries.length > 0 ? dir : null;
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
const userPaths = USER_DIRS.map((name) => join(ctx.home, name, "mcp.json"));
|
|
105
|
+
|
|
106
|
+
const projectDir = projectDirs.find((dir) => dir !== null);
|
|
107
|
+
if (projectDir) {
|
|
108
|
+
const projectCandidates = ["mcp.json", ".mcp.json"].map((filename) => join(projectDir, filename));
|
|
109
|
+
const projectContents = await Promise.all(projectCandidates.map((path) => readFile(path)));
|
|
110
|
+
for (let i = 0; i < projectCandidates.length; i++) {
|
|
111
|
+
const content = projectContents[i];
|
|
112
|
+
if (content) {
|
|
113
|
+
items.push(...parseMcpServers(content, projectCandidates[i], "project"));
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const userContents = await Promise.all(userPaths.map((path) => readFile(path)));
|
|
120
|
+
for (let i = 0; i < userPaths.length; i++) {
|
|
121
|
+
const content = userContents[i];
|
|
122
|
+
if (content) {
|
|
123
|
+
items.push(...parseMcpServers(content, userPaths[i], "user"));
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
125
126
|
}
|
|
126
127
|
|
|
127
128
|
return { items, warnings };
|
|
@@ -136,48 +137,54 @@ registerProvider<MCPServer>(mcpCapability.id, {
|
|
|
136
137
|
});
|
|
137
138
|
|
|
138
139
|
// System Prompt (SYSTEM.md)
|
|
139
|
-
function loadSystemPrompt(ctx: LoadContext): LoadResult<SystemPrompt
|
|
140
|
+
async function loadSystemPrompt(ctx: LoadContext): Promise<LoadResult<SystemPrompt>> {
|
|
140
141
|
const items: SystemPrompt[] = [];
|
|
141
142
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
143
|
+
const userPaths = USER_DIRS.map((name) =>
|
|
144
|
+
join(ctx.home, name, PATHS.userAgent.replace(`${PATHS.userBase}/`, ""), "SYSTEM.md"),
|
|
145
|
+
);
|
|
146
|
+
const userContents = await Promise.all(userPaths.map((p) => readFile(p)));
|
|
147
|
+
for (let i = 0; i < userPaths.length; i++) {
|
|
148
|
+
const content = userContents[i];
|
|
149
|
+
if (content) {
|
|
147
150
|
items.push({
|
|
148
|
-
path:
|
|
149
|
-
content
|
|
151
|
+
path: userPaths[i],
|
|
152
|
+
content,
|
|
150
153
|
level: "user",
|
|
151
|
-
_source: createSourceMeta(PROVIDER_ID,
|
|
154
|
+
_source: createSourceMeta(PROVIDER_ID, userPaths[i], "user"),
|
|
152
155
|
});
|
|
153
|
-
break;
|
|
156
|
+
break;
|
|
154
157
|
}
|
|
155
158
|
}
|
|
156
159
|
|
|
157
|
-
|
|
160
|
+
const ancestors: string[] = [];
|
|
158
161
|
let current = ctx.cwd;
|
|
159
162
|
while (true) {
|
|
160
|
-
|
|
161
|
-
const configDir = join(current, name);
|
|
162
|
-
if (ctx.fs.isDir(configDir)) {
|
|
163
|
-
const projectPath = join(configDir, "SYSTEM.md");
|
|
164
|
-
const content = ctx.fs.readFile(projectPath);
|
|
165
|
-
if (content) {
|
|
166
|
-
items.push({
|
|
167
|
-
path: projectPath,
|
|
168
|
-
content,
|
|
169
|
-
level: "project",
|
|
170
|
-
_source: createSourceMeta(PROVIDER_ID, projectPath, "project"),
|
|
171
|
-
});
|
|
172
|
-
break; // First config dir in this directory wins
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
163
|
+
ancestors.push(current);
|
|
176
164
|
const parent = dirname(current);
|
|
177
165
|
if (parent === current) break;
|
|
178
166
|
current = parent;
|
|
179
167
|
}
|
|
180
168
|
|
|
169
|
+
for (const dir of ancestors) {
|
|
170
|
+
const configDirs = PROJECT_DIRS.map((name) => join(dir, name));
|
|
171
|
+
const entriesResults = await Promise.all(configDirs.map((d) => readDirEntries(d)));
|
|
172
|
+
const validConfigDir = configDirs.find((_, i) => entriesResults[i].length > 0);
|
|
173
|
+
if (!validConfigDir) continue;
|
|
174
|
+
|
|
175
|
+
const projectPath = join(validConfigDir, "SYSTEM.md");
|
|
176
|
+
const content = await readFile(projectPath);
|
|
177
|
+
if (content) {
|
|
178
|
+
items.push({
|
|
179
|
+
path: projectPath,
|
|
180
|
+
content,
|
|
181
|
+
level: "project",
|
|
182
|
+
_source: createSourceMeta(PROVIDER_ID, projectPath, "project"),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
|
|
181
188
|
return { items, warnings: [] };
|
|
182
189
|
}
|
|
183
190
|
|
|
@@ -190,71 +197,25 @@ registerProvider<SystemPrompt>(systemPromptCapability.id, {
|
|
|
190
197
|
});
|
|
191
198
|
|
|
192
199
|
// Skills
|
|
193
|
-
function
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
200
|
+
async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
|
|
201
|
+
const configDirs = await getConfigDirs(ctx);
|
|
202
|
+
const results = await Promise.all(
|
|
203
|
+
configDirs.map(({ dir, level }) =>
|
|
204
|
+
loadSkillsFromDir(ctx, {
|
|
205
|
+
dir: join(dir, "skills"),
|
|
206
|
+
providerId: PROVIDER_ID,
|
|
207
|
+
level,
|
|
208
|
+
requireDescription: true,
|
|
209
|
+
}),
|
|
210
|
+
),
|
|
211
|
+
);
|
|
203
212
|
|
|
204
213
|
return {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
content: body,
|
|
208
|
-
frontmatter: frontmatter as SkillFrontmatter,
|
|
209
|
-
level,
|
|
210
|
-
_source: createSourceMeta(PROVIDER_ID, path, level),
|
|
214
|
+
items: results.flatMap((r) => r.items),
|
|
215
|
+
warnings: results.flatMap((r) => r.warnings ?? []),
|
|
211
216
|
};
|
|
212
217
|
}
|
|
213
218
|
|
|
214
|
-
function loadSkillsRecursive(ctx: LoadContext, dir: string, level: "user" | "project"): LoadResult<Skill> {
|
|
215
|
-
const items: Skill[] = [];
|
|
216
|
-
const warnings: string[] = [];
|
|
217
|
-
|
|
218
|
-
if (!ctx.fs.isDir(dir)) return { items, warnings };
|
|
219
|
-
|
|
220
|
-
for (const name of ctx.fs.readDir(dir)) {
|
|
221
|
-
if (name.startsWith(".") || name === "node_modules") continue;
|
|
222
|
-
|
|
223
|
-
const path = join(dir, name);
|
|
224
|
-
|
|
225
|
-
if (ctx.fs.isDir(path)) {
|
|
226
|
-
const skillFile = join(path, "SKILL.md");
|
|
227
|
-
if (ctx.fs.isFile(skillFile)) {
|
|
228
|
-
const skill = loadSkillFromFile(ctx, skillFile, level);
|
|
229
|
-
if (skill) items.push(skill);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const sub = loadSkillsRecursive(ctx, path, level);
|
|
233
|
-
items.push(...sub.items);
|
|
234
|
-
if (sub.warnings) warnings.push(...sub.warnings);
|
|
235
|
-
} else if (name === "SKILL.md") {
|
|
236
|
-
const skill = loadSkillFromFile(ctx, path, level);
|
|
237
|
-
if (skill) items.push(skill);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
return { items, warnings };
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function loadSkills(ctx: LoadContext): LoadResult<Skill> {
|
|
245
|
-
const items: Skill[] = [];
|
|
246
|
-
const warnings: string[] = [];
|
|
247
|
-
|
|
248
|
-
for (const { dir, level } of getConfigDirs(ctx)) {
|
|
249
|
-
const skillsDir = join(dir, "skills");
|
|
250
|
-
const result = loadSkillsRecursive(ctx, skillsDir, level);
|
|
251
|
-
items.push(...result.items);
|
|
252
|
-
if (result.warnings) warnings.push(...result.warnings);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return { items, warnings };
|
|
256
|
-
}
|
|
257
|
-
|
|
258
219
|
registerProvider<Skill>(skillCapability.id, {
|
|
259
220
|
id: PROVIDER_ID,
|
|
260
221
|
displayName: DISPLAY_NAME,
|
|
@@ -264,13 +225,13 @@ registerProvider<Skill>(skillCapability.id, {
|
|
|
264
225
|
});
|
|
265
226
|
|
|
266
227
|
// Slash Commands
|
|
267
|
-
function loadSlashCommands(ctx: LoadContext): LoadResult<SlashCommand
|
|
228
|
+
async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashCommand>> {
|
|
268
229
|
const items: SlashCommand[] = [];
|
|
269
230
|
const warnings: string[] = [];
|
|
270
231
|
|
|
271
|
-
for (const { dir, level } of getConfigDirs(ctx)) {
|
|
232
|
+
for (const { dir, level } of await getConfigDirs(ctx)) {
|
|
272
233
|
const commandsDir = join(dir, "commands");
|
|
273
|
-
const result = loadFilesFromDir<SlashCommand>(ctx, commandsDir, PROVIDER_ID, level, {
|
|
234
|
+
const result = await loadFilesFromDir<SlashCommand>(ctx, commandsDir, PROVIDER_ID, level, {
|
|
274
235
|
extensions: ["md"],
|
|
275
236
|
transform: (name, content, path, source) => ({
|
|
276
237
|
name: name.replace(/\.md$/, ""),
|
|
@@ -296,13 +257,13 @@ registerProvider<SlashCommand>(slashCommandCapability.id, {
|
|
|
296
257
|
});
|
|
297
258
|
|
|
298
259
|
// Rules
|
|
299
|
-
function loadRules(ctx: LoadContext): LoadResult<Rule
|
|
260
|
+
async function loadRules(ctx: LoadContext): Promise<LoadResult<Rule>> {
|
|
300
261
|
const items: Rule[] = [];
|
|
301
262
|
const warnings: string[] = [];
|
|
302
263
|
|
|
303
|
-
for (const { dir, level } of getConfigDirs(ctx)) {
|
|
264
|
+
for (const { dir, level } of await getConfigDirs(ctx)) {
|
|
304
265
|
const rulesDir = join(dir, "rules");
|
|
305
|
-
const result = loadFilesFromDir<Rule>(ctx, rulesDir, PROVIDER_ID, level, {
|
|
266
|
+
const result = await loadFilesFromDir<Rule>(ctx, rulesDir, PROVIDER_ID, level, {
|
|
306
267
|
extensions: ["md", "mdc"],
|
|
307
268
|
transform: (name, content, path, source) => {
|
|
308
269
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
@@ -334,13 +295,13 @@ registerProvider<Rule>(ruleCapability.id, {
|
|
|
334
295
|
});
|
|
335
296
|
|
|
336
297
|
// Prompts
|
|
337
|
-
function loadPrompts(ctx: LoadContext): LoadResult<Prompt
|
|
298
|
+
async function loadPrompts(ctx: LoadContext): Promise<LoadResult<Prompt>> {
|
|
338
299
|
const items: Prompt[] = [];
|
|
339
300
|
const warnings: string[] = [];
|
|
340
301
|
|
|
341
|
-
for (const { dir, level } of getConfigDirs(ctx)) {
|
|
302
|
+
for (const { dir, level } of await getConfigDirs(ctx)) {
|
|
342
303
|
const promptsDir = join(dir, "prompts");
|
|
343
|
-
const result = loadFilesFromDir<Prompt>(ctx, promptsDir, PROVIDER_ID, level, {
|
|
304
|
+
const result = await loadFilesFromDir<Prompt>(ctx, promptsDir, PROVIDER_ID, level, {
|
|
344
305
|
extensions: ["md"],
|
|
345
306
|
transform: (name, content, path, source) => ({
|
|
346
307
|
name: name.replace(/\.md$/, ""),
|
|
@@ -365,7 +326,7 @@ registerProvider<Prompt>(promptCapability.id, {
|
|
|
365
326
|
});
|
|
366
327
|
|
|
367
328
|
// Extension Modules
|
|
368
|
-
function loadExtensionModules(ctx: LoadContext): LoadResult<ExtensionModule
|
|
329
|
+
async function loadExtensionModules(ctx: LoadContext): Promise<LoadResult<ExtensionModule>> {
|
|
369
330
|
const items: ExtensionModule[] = [];
|
|
370
331
|
const warnings: string[] = [];
|
|
371
332
|
|
|
@@ -382,45 +343,88 @@ function loadExtensionModules(ctx: LoadContext): LoadResult<ExtensionModule> {
|
|
|
382
343
|
return resolve(ctx.cwd, rawPath);
|
|
383
344
|
};
|
|
384
345
|
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
});
|
|
392
|
-
};
|
|
346
|
+
const createExtensionModule = (extPath: string, level: "user" | "project"): ExtensionModule => ({
|
|
347
|
+
name: getExtensionNameFromPath(extPath),
|
|
348
|
+
path: extPath,
|
|
349
|
+
level,
|
|
350
|
+
_source: createSourceMeta(PROVIDER_ID, extPath, level),
|
|
351
|
+
});
|
|
393
352
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
353
|
+
const configDirs = await getConfigDirs(ctx);
|
|
354
|
+
|
|
355
|
+
const [discoveredResults, settingsResults] = await Promise.all([
|
|
356
|
+
Promise.all(configDirs.map(({ dir }) => discoverExtensionModulePaths(ctx, join(dir, "extensions")))),
|
|
357
|
+
Promise.all(configDirs.map(({ dir }) => readFile(join(dir, "settings.json")))),
|
|
358
|
+
]);
|
|
359
|
+
|
|
360
|
+
for (let i = 0; i < configDirs.length; i++) {
|
|
361
|
+
const { level } = configDirs[i];
|
|
362
|
+
for (const extPath of discoveredResults[i]) {
|
|
363
|
+
items.push(createExtensionModule(extPath, level));
|
|
399
364
|
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const settingsExtensions: Array<{
|
|
368
|
+
resolvedPath: string;
|
|
369
|
+
settingsPath: string;
|
|
370
|
+
level: "user" | "project";
|
|
371
|
+
}> = [];
|
|
372
|
+
|
|
373
|
+
for (let i = 0; i < configDirs.length; i++) {
|
|
374
|
+
const { dir, level } = configDirs[i];
|
|
375
|
+
const settingsContent = settingsResults[i];
|
|
376
|
+
if (!settingsContent) continue;
|
|
400
377
|
|
|
401
378
|
const settingsPath = join(dir, "settings.json");
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
continue;
|
|
411
|
-
}
|
|
412
|
-
const resolvedPath = resolveExtensionPath(entry);
|
|
413
|
-
if (ctx.fs.isDir(resolvedPath)) {
|
|
414
|
-
for (const extPath of discoverExtensionModulePaths(ctx, resolvedPath)) {
|
|
415
|
-
addExtensionPath(extPath, level);
|
|
416
|
-
}
|
|
417
|
-
} else if (ctx.fs.isFile(resolvedPath)) {
|
|
418
|
-
addExtensionPath(resolvedPath, level);
|
|
419
|
-
} else {
|
|
420
|
-
warnings.push(`Extension path not found: ${resolvedPath}`);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
379
|
+
const settingsData = parseJSON<{ extensions?: unknown }>(settingsContent);
|
|
380
|
+
const extensions = settingsData?.extensions;
|
|
381
|
+
if (!Array.isArray(extensions)) continue;
|
|
382
|
+
|
|
383
|
+
for (const entry of extensions) {
|
|
384
|
+
if (typeof entry !== "string") {
|
|
385
|
+
warnings.push(`Invalid extension path in ${settingsPath}: ${String(entry)}`);
|
|
386
|
+
continue;
|
|
423
387
|
}
|
|
388
|
+
settingsExtensions.push({
|
|
389
|
+
resolvedPath: resolveExtensionPath(entry),
|
|
390
|
+
settingsPath,
|
|
391
|
+
level,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const [entriesResults, fileContents] = await Promise.all([
|
|
397
|
+
Promise.all(settingsExtensions.map(({ resolvedPath }) => readDirEntries(resolvedPath))),
|
|
398
|
+
Promise.all(settingsExtensions.map(({ resolvedPath }) => readFile(resolvedPath))),
|
|
399
|
+
]);
|
|
400
|
+
|
|
401
|
+
const dirDiscoveryPromises: Array<{
|
|
402
|
+
promise: Promise<string[]>;
|
|
403
|
+
level: "user" | "project";
|
|
404
|
+
}> = [];
|
|
405
|
+
|
|
406
|
+
for (let i = 0; i < settingsExtensions.length; i++) {
|
|
407
|
+
const { resolvedPath, level } = settingsExtensions[i];
|
|
408
|
+
const entries = entriesResults[i];
|
|
409
|
+
const content = fileContents[i];
|
|
410
|
+
|
|
411
|
+
if (entries.length > 0) {
|
|
412
|
+
dirDiscoveryPromises.push({
|
|
413
|
+
promise: discoverExtensionModulePaths(ctx, resolvedPath),
|
|
414
|
+
level,
|
|
415
|
+
});
|
|
416
|
+
} else if (content !== null) {
|
|
417
|
+
items.push(createExtensionModule(resolvedPath, level));
|
|
418
|
+
} else {
|
|
419
|
+
warnings.push(`Extension path not found: ${resolvedPath}`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const dirDiscoveryResults = await Promise.all(dirDiscoveryPromises.map((d) => d.promise));
|
|
424
|
+
for (let i = 0; i < dirDiscoveryPromises.length; i++) {
|
|
425
|
+
const { level } = dirDiscoveryPromises[i];
|
|
426
|
+
for (const extPath of dirDiscoveryResults[i]) {
|
|
427
|
+
items.push(createExtensionModule(extPath, level));
|
|
424
428
|
}
|
|
425
429
|
}
|
|
426
430
|
|
|
@@ -436,40 +440,61 @@ registerProvider<ExtensionModule>(extensionModuleCapability.id, {
|
|
|
436
440
|
});
|
|
437
441
|
|
|
438
442
|
// Extensions
|
|
439
|
-
function loadExtensions(ctx: LoadContext): LoadResult<Extension
|
|
443
|
+
async function loadExtensions(ctx: LoadContext): Promise<LoadResult<Extension>> {
|
|
440
444
|
const items: Extension[] = [];
|
|
441
445
|
const warnings: string[] = [];
|
|
442
446
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
if (!ctx.fs.isDir(extensionsDir)) continue;
|
|
446
|
-
|
|
447
|
-
for (const name of ctx.fs.readDir(extensionsDir)) {
|
|
448
|
-
if (name.startsWith(".")) continue;
|
|
447
|
+
const configDirs = await getConfigDirs(ctx);
|
|
448
|
+
const entriesResults = await Promise.all(configDirs.map(({ dir }) => readDirEntries(join(dir, "extensions"))));
|
|
449
449
|
|
|
450
|
-
|
|
451
|
-
|
|
450
|
+
const manifestCandidates: Array<{
|
|
451
|
+
extDir: string;
|
|
452
|
+
manifestPath: string;
|
|
453
|
+
entryName: string;
|
|
454
|
+
level: "user" | "project";
|
|
455
|
+
}> = [];
|
|
452
456
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
457
|
+
for (let i = 0; i < configDirs.length; i++) {
|
|
458
|
+
const { dir, level } = configDirs[i];
|
|
459
|
+
const entries = entriesResults[i];
|
|
460
|
+
const extensionsDir = join(dir, "extensions");
|
|
456
461
|
|
|
457
|
-
|
|
458
|
-
if (
|
|
459
|
-
|
|
460
|
-
continue;
|
|
461
|
-
}
|
|
462
|
+
for (const entry of entries) {
|
|
463
|
+
if (entry.name.startsWith(".")) continue;
|
|
464
|
+
if (!entry.isDirectory()) continue;
|
|
462
465
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
466
|
+
const extDir = join(extensionsDir, entry.name);
|
|
467
|
+
manifestCandidates.push({
|
|
468
|
+
extDir,
|
|
469
|
+
manifestPath: join(extDir, "gemini-extension.json"),
|
|
470
|
+
entryName: entry.name,
|
|
467
471
|
level,
|
|
468
|
-
_source: createSourceMeta(PROVIDER_ID, manifestPath, level),
|
|
469
472
|
});
|
|
470
473
|
}
|
|
471
474
|
}
|
|
472
475
|
|
|
476
|
+
const manifestContents = await Promise.all(manifestCandidates.map(({ manifestPath }) => readFile(manifestPath)));
|
|
477
|
+
|
|
478
|
+
for (let i = 0; i < manifestCandidates.length; i++) {
|
|
479
|
+
const content = manifestContents[i];
|
|
480
|
+
if (!content) continue;
|
|
481
|
+
|
|
482
|
+
const { extDir, manifestPath, entryName, level } = manifestCandidates[i];
|
|
483
|
+
const manifest = parseJSON<ExtensionManifest>(content);
|
|
484
|
+
if (!manifest) {
|
|
485
|
+
warnings.push(`Failed to parse ${manifestPath}`);
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
items.push({
|
|
490
|
+
name: manifest.name || entryName,
|
|
491
|
+
path: extDir,
|
|
492
|
+
manifest,
|
|
493
|
+
level,
|
|
494
|
+
_source: createSourceMeta(PROVIDER_ID, manifestPath, level),
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
473
498
|
return { items, warnings };
|
|
474
499
|
}
|
|
475
500
|
|
|
@@ -482,13 +507,13 @@ registerProvider<Extension>(extensionCapability.id, {
|
|
|
482
507
|
});
|
|
483
508
|
|
|
484
509
|
// Instructions
|
|
485
|
-
function loadInstructions(ctx: LoadContext): LoadResult<Instruction
|
|
510
|
+
async function loadInstructions(ctx: LoadContext): Promise<LoadResult<Instruction>> {
|
|
486
511
|
const items: Instruction[] = [];
|
|
487
512
|
const warnings: string[] = [];
|
|
488
513
|
|
|
489
|
-
for (const { dir, level } of getConfigDirs(ctx)) {
|
|
514
|
+
for (const { dir, level } of await getConfigDirs(ctx)) {
|
|
490
515
|
const instructionsDir = join(dir, "instructions");
|
|
491
|
-
const result = loadFilesFromDir<Instruction>(ctx, instructionsDir, PROVIDER_ID, level, {
|
|
516
|
+
const result = await loadFilesFromDir<Instruction>(ctx, instructionsDir, PROVIDER_ID, level, {
|
|
492
517
|
extensions: ["md"],
|
|
493
518
|
transform: (name, content, path, source) => {
|
|
494
519
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
@@ -517,35 +542,50 @@ registerProvider<Instruction>(instructionCapability.id, {
|
|
|
517
542
|
});
|
|
518
543
|
|
|
519
544
|
// Hooks
|
|
520
|
-
function loadHooks(ctx: LoadContext): LoadResult<Hook
|
|
545
|
+
async function loadHooks(ctx: LoadContext): Promise<LoadResult<Hook>> {
|
|
521
546
|
const items: Hook[] = [];
|
|
522
547
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
548
|
+
const configDirs = await getConfigDirs(ctx);
|
|
549
|
+
const hookTypes = ["pre", "post"] as const;
|
|
550
|
+
|
|
551
|
+
const typeDirRequests: Array<{
|
|
552
|
+
typeDir: string;
|
|
553
|
+
hookType: (typeof hookTypes)[number];
|
|
554
|
+
level: "user" | "project";
|
|
555
|
+
}> = [];
|
|
556
|
+
|
|
557
|
+
for (const { dir, level } of configDirs) {
|
|
558
|
+
for (const hookType of hookTypes) {
|
|
559
|
+
typeDirRequests.push({
|
|
560
|
+
typeDir: join(dir, "hooks", hookType),
|
|
561
|
+
hookType,
|
|
562
|
+
level,
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
}
|
|
526
566
|
|
|
527
|
-
|
|
528
|
-
const typeDir = join(hooksDir, hookType);
|
|
529
|
-
if (!ctx.fs.isDir(typeDir)) continue;
|
|
567
|
+
const typeEntriesResults = await Promise.all(typeDirRequests.map(({ typeDir }) => readDirEntries(typeDir)));
|
|
530
568
|
|
|
531
|
-
|
|
532
|
-
|
|
569
|
+
for (let i = 0; i < typeDirRequests.length; i++) {
|
|
570
|
+
const { typeDir, hookType, level } = typeDirRequests[i];
|
|
571
|
+
const typeEntries = typeEntriesResults[i];
|
|
533
572
|
|
|
534
|
-
|
|
535
|
-
|
|
573
|
+
for (const entry of typeEntries) {
|
|
574
|
+
if (entry.name.startsWith(".")) continue;
|
|
575
|
+
if (!entry.isFile()) continue;
|
|
536
576
|
|
|
537
|
-
|
|
538
|
-
|
|
577
|
+
const path = join(typeDir, entry.name);
|
|
578
|
+
const baseName = entry.name.includes(".") ? entry.name.slice(0, entry.name.lastIndexOf(".")) : entry.name;
|
|
579
|
+
const tool = baseName === "*" ? "*" : baseName;
|
|
539
580
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
}
|
|
581
|
+
items.push({
|
|
582
|
+
name: entry.name,
|
|
583
|
+
path,
|
|
584
|
+
type: hookType,
|
|
585
|
+
tool,
|
|
586
|
+
level,
|
|
587
|
+
_source: createSourceMeta(PROVIDER_ID, path, level),
|
|
588
|
+
});
|
|
549
589
|
}
|
|
550
590
|
}
|
|
551
591
|
|
|
@@ -561,58 +601,86 @@ registerProvider<Hook>(hookCapability.id, {
|
|
|
561
601
|
});
|
|
562
602
|
|
|
563
603
|
// Custom Tools
|
|
564
|
-
function loadTools(ctx: LoadContext): LoadResult<CustomTool
|
|
604
|
+
async function loadTools(ctx: LoadContext): Promise<LoadResult<CustomTool>> {
|
|
565
605
|
const items: CustomTool[] = [];
|
|
566
606
|
const warnings: string[] = [];
|
|
567
607
|
|
|
568
|
-
|
|
608
|
+
const configDirs = await getConfigDirs(ctx);
|
|
609
|
+
const entriesResults = await Promise.all(configDirs.map(({ dir }) => readDirEntries(join(dir, "tools"))));
|
|
610
|
+
|
|
611
|
+
const fileLoadPromises: Array<Promise<{ items: CustomTool[]; warnings?: string[] }>> = [];
|
|
612
|
+
const subDirCandidates: Array<{
|
|
613
|
+
indexPath: string;
|
|
614
|
+
entryName: string;
|
|
615
|
+
level: "user" | "project";
|
|
616
|
+
}> = [];
|
|
617
|
+
|
|
618
|
+
for (let i = 0; i < configDirs.length; i++) {
|
|
619
|
+
const { dir, level } = configDirs[i];
|
|
620
|
+
const toolEntries = entriesResults[i];
|
|
621
|
+
if (toolEntries.length === 0) continue;
|
|
622
|
+
|
|
569
623
|
const toolsDir = join(dir, "tools");
|
|
570
|
-
if (!ctx.fs.isDir(toolsDir)) continue;
|
|
571
624
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
625
|
+
fileLoadPromises.push(
|
|
626
|
+
loadFilesFromDir<CustomTool>(ctx, toolsDir, PROVIDER_ID, level, {
|
|
627
|
+
extensions: ["json", "md"],
|
|
628
|
+
transform: (name, content, path, source) => {
|
|
629
|
+
if (name.endsWith(".json")) {
|
|
630
|
+
const data = parseJSON<{ name?: string; description?: string }>(content);
|
|
631
|
+
return {
|
|
632
|
+
name: data?.name || name.replace(/\.json$/, ""),
|
|
633
|
+
path,
|
|
634
|
+
description: data?.description,
|
|
635
|
+
level,
|
|
636
|
+
_source: source,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
const { frontmatter } = parseFrontmatter(content);
|
|
578
640
|
return {
|
|
579
|
-
name:
|
|
641
|
+
name: (frontmatter.name as string) || name.replace(/\.md$/, ""),
|
|
580
642
|
path,
|
|
581
|
-
description:
|
|
643
|
+
description: frontmatter.description as string | undefined,
|
|
582
644
|
level,
|
|
583
645
|
_source: source,
|
|
584
646
|
};
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
647
|
+
},
|
|
648
|
+
}),
|
|
649
|
+
);
|
|
650
|
+
|
|
651
|
+
for (const entry of toolEntries) {
|
|
652
|
+
if (entry.name.startsWith(".")) continue;
|
|
653
|
+
if (!entry.isDirectory()) continue;
|
|
654
|
+
|
|
655
|
+
subDirCandidates.push({
|
|
656
|
+
indexPath: join(toolsDir, entry.name, "index.ts"),
|
|
657
|
+
entryName: entry.name,
|
|
658
|
+
level,
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const [fileResults, indexContents] = await Promise.all([
|
|
664
|
+
Promise.all(fileLoadPromises),
|
|
665
|
+
Promise.all(subDirCandidates.map(({ indexPath }) => readFile(indexPath))),
|
|
666
|
+
]);
|
|
667
|
+
|
|
668
|
+
for (const result of fileResults) {
|
|
596
669
|
items.push(...result.items);
|
|
597
670
|
if (result.warnings) warnings.push(...result.warnings);
|
|
671
|
+
}
|
|
598
672
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
path: indexPath,
|
|
611
|
-
description: undefined,
|
|
612
|
-
level,
|
|
613
|
-
_source: createSourceMeta(PROVIDER_ID, indexPath, level),
|
|
614
|
-
});
|
|
615
|
-
}
|
|
673
|
+
for (let i = 0; i < subDirCandidates.length; i++) {
|
|
674
|
+
const indexContent = indexContents[i];
|
|
675
|
+
if (indexContent !== null) {
|
|
676
|
+
const { indexPath, entryName, level } = subDirCandidates[i];
|
|
677
|
+
items.push({
|
|
678
|
+
name: entryName,
|
|
679
|
+
path: indexPath,
|
|
680
|
+
description: undefined,
|
|
681
|
+
level,
|
|
682
|
+
_source: createSourceMeta(PROVIDER_ID, indexPath, level),
|
|
683
|
+
});
|
|
616
684
|
}
|
|
617
685
|
}
|
|
618
686
|
|
|
@@ -628,13 +696,13 @@ registerProvider<CustomTool>(toolCapability.id, {
|
|
|
628
696
|
});
|
|
629
697
|
|
|
630
698
|
// Settings
|
|
631
|
-
function loadSettings(ctx: LoadContext): LoadResult<Settings
|
|
699
|
+
async function loadSettings(ctx: LoadContext): Promise<LoadResult<Settings>> {
|
|
632
700
|
const items: Settings[] = [];
|
|
633
701
|
const warnings: string[] = [];
|
|
634
702
|
|
|
635
|
-
for (const { dir, level } of getConfigDirs(ctx)) {
|
|
703
|
+
for (const { dir, level } of await getConfigDirs(ctx)) {
|
|
636
704
|
const settingsPath = join(dir, "settings.json");
|
|
637
|
-
const content =
|
|
705
|
+
const content = await readFile(settingsPath);
|
|
638
706
|
if (!content) continue;
|
|
639
707
|
|
|
640
708
|
const data = parseJSON<Record<string, unknown>>(content);
|
|
@@ -663,52 +731,59 @@ registerProvider<Settings>(settingsCapability.id, {
|
|
|
663
731
|
});
|
|
664
732
|
|
|
665
733
|
// Context Files (AGENTS.md)
|
|
666
|
-
function loadContextFiles(ctx: LoadContext): LoadResult<ContextFile
|
|
734
|
+
async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFile>> {
|
|
667
735
|
const items: ContextFile[] = [];
|
|
668
736
|
const warnings: string[] = [];
|
|
669
737
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
738
|
+
const userPaths = USER_DIRS.map((name) =>
|
|
739
|
+
join(ctx.home, name, PATHS.userAgent.replace(`${PATHS.userBase}/`, ""), "AGENTS.md"),
|
|
740
|
+
);
|
|
741
|
+
const userContents = await Promise.all(userPaths.map((p) => readFile(p)));
|
|
742
|
+
for (let i = 0; i < userPaths.length; i++) {
|
|
743
|
+
const content = userContents[i];
|
|
674
744
|
if (content) {
|
|
675
745
|
items.push({
|
|
676
|
-
path:
|
|
746
|
+
path: userPaths[i],
|
|
677
747
|
content,
|
|
678
748
|
level: "user",
|
|
679
|
-
_source: createSourceMeta(PROVIDER_ID,
|
|
749
|
+
_source: createSourceMeta(PROVIDER_ID, userPaths[i], "user"),
|
|
680
750
|
});
|
|
681
|
-
break;
|
|
751
|
+
break;
|
|
682
752
|
}
|
|
683
753
|
}
|
|
684
754
|
|
|
685
|
-
|
|
755
|
+
const ancestors: Array<{ dir: string; depth: number }> = [];
|
|
686
756
|
let current = ctx.cwd;
|
|
687
757
|
let depth = 0;
|
|
688
758
|
while (true) {
|
|
689
|
-
|
|
690
|
-
const configDir = join(current, name);
|
|
691
|
-
if (ctx.fs.isDir(configDir)) {
|
|
692
|
-
const projectPath = join(configDir, "AGENTS.md");
|
|
693
|
-
const content = ctx.fs.readFile(projectPath);
|
|
694
|
-
if (content) {
|
|
695
|
-
items.push({
|
|
696
|
-
path: projectPath,
|
|
697
|
-
content,
|
|
698
|
-
level: "project",
|
|
699
|
-
depth,
|
|
700
|
-
_source: createSourceMeta(PROVIDER_ID, projectPath, "project"),
|
|
701
|
-
});
|
|
702
|
-
return { items, warnings }; // First config dir wins
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
}
|
|
759
|
+
ancestors.push({ dir: current, depth });
|
|
706
760
|
const parent = dirname(current);
|
|
707
761
|
if (parent === current) break;
|
|
708
762
|
current = parent;
|
|
709
763
|
depth++;
|
|
710
764
|
}
|
|
711
765
|
|
|
766
|
+
for (const { dir, depth: ancestorDepth } of ancestors) {
|
|
767
|
+
const configDirs = PROJECT_DIRS.map((name) => join(dir, name));
|
|
768
|
+
const entriesResults = await Promise.all(configDirs.map((d) => readDirEntries(d)));
|
|
769
|
+
const validConfigDir = configDirs.find((_, i) => entriesResults[i].length > 0);
|
|
770
|
+
if (!validConfigDir) continue;
|
|
771
|
+
|
|
772
|
+
const projectPath = join(validConfigDir, "AGENTS.md");
|
|
773
|
+
const content = await readFile(projectPath);
|
|
774
|
+
if (content) {
|
|
775
|
+
items.push({
|
|
776
|
+
path: projectPath,
|
|
777
|
+
content,
|
|
778
|
+
level: "project",
|
|
779
|
+
depth: ancestorDepth,
|
|
780
|
+
_source: createSourceMeta(PROVIDER_ID, projectPath, "project"),
|
|
781
|
+
});
|
|
782
|
+
return { items, warnings };
|
|
783
|
+
}
|
|
784
|
+
break;
|
|
785
|
+
}
|
|
786
|
+
|
|
712
787
|
return { items, warnings };
|
|
713
788
|
}
|
|
714
789
|
|