@oh-my-pi/pi-coding-agent 4.2.1 → 4.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/docs/sdk.md +5 -5
- package/examples/sdk/10-settings.ts +2 -2
- package/package.json +5 -5
- package/src/capability/fs.ts +90 -0
- package/src/capability/index.ts +41 -227
- package/src/capability/types.ts +1 -11
- package/src/cli/args.ts +4 -0
- package/src/core/agent-session.ts +4 -4
- package/src/core/agent-storage.ts +50 -0
- package/src/core/auth-storage.ts +112 -4
- package/src/core/bash-executor.ts +1 -1
- package/src/core/custom-tools/loader.ts +2 -2
- package/src/core/extensions/loader.ts +2 -2
- package/src/core/extensions/types.ts +1 -1
- package/src/core/hooks/loader.ts +2 -2
- package/src/core/mcp/config.ts +2 -2
- package/src/core/model-registry.ts +46 -0
- package/src/core/sdk.ts +37 -29
- package/src/core/settings-manager.ts +152 -135
- package/src/core/skills.ts +72 -51
- package/src/core/slash-commands.ts +3 -3
- package/src/core/system-prompt.ts +10 -10
- package/src/core/tools/edit.ts +7 -4
- package/src/core/tools/find.ts +2 -2
- package/src/core/tools/index.test.ts +16 -0
- package/src/core/tools/index.ts +21 -8
- package/src/core/tools/lsp/index.ts +4 -1
- package/src/core/tools/ssh.ts +6 -6
- package/src/core/tools/task/commands.ts +3 -5
- package/src/core/tools/task/executor.ts +88 -3
- package/src/core/tools/task/index.ts +4 -0
- package/src/core/tools/task/model-resolver.ts +10 -7
- package/src/core/tools/task/worker-protocol.ts +48 -2
- package/src/core/tools/task/worker.ts +152 -7
- package/src/core/tools/write.ts +7 -4
- package/src/discovery/agents-md.ts +13 -19
- package/src/discovery/builtin.ts +367 -247
- package/src/discovery/claude.ts +181 -290
- package/src/discovery/cline.ts +30 -10
- package/src/discovery/codex.ts +185 -244
- package/src/discovery/cursor.ts +106 -121
- package/src/discovery/gemini.ts +72 -97
- package/src/discovery/github.ts +7 -10
- package/src/discovery/helpers.ts +94 -88
- package/src/discovery/index.ts +1 -2
- package/src/discovery/mcp-json.ts +15 -18
- package/src/discovery/ssh.ts +9 -17
- package/src/discovery/vscode.ts +10 -5
- package/src/discovery/windsurf.ts +52 -86
- package/src/main.ts +5 -1
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +24 -11
- package/src/modes/interactive/components/extensions/state-manager.ts +19 -15
- package/src/modes/interactive/controllers/selector-controller.ts +6 -2
- package/src/modes/interactive/interactive-mode.ts +19 -15
- package/src/prompts/agents/plan.md +107 -30
- package/src/utils/shell.ts +2 -2
- package/src/prompts/agents/planner.md +0 -112
package/src/discovery/builtin.ts
CHANGED
|
@@ -9,6 +9,7 @@ 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";
|
|
@@ -42,12 +43,13 @@ const PATHS = SOURCE_PATHS.native;
|
|
|
42
43
|
const PROJECT_DIRS = [PATHS.projectDir, ...PATHS.aliases];
|
|
43
44
|
const USER_DIRS = [PATHS.userBase, ...PATHS.aliases];
|
|
44
45
|
|
|
45
|
-
function getConfigDirs(ctx: LoadContext): Array<{ dir: string; level: "user" | "project" }
|
|
46
|
+
async function getConfigDirs(ctx: LoadContext): Promise<Array<{ dir: string; level: "user" | "project" }>> {
|
|
46
47
|
const result: Array<{ dir: string; level: "user" | "project" }> = [];
|
|
47
48
|
|
|
48
49
|
for (const name of PROJECT_DIRS) {
|
|
49
|
-
const projectDir = ctx.
|
|
50
|
-
|
|
50
|
+
const projectDir = join(ctx.cwd, name);
|
|
51
|
+
const entries = await readDirEntries(projectDir);
|
|
52
|
+
if (entries.length > 0) {
|
|
51
53
|
result.push({ dir: projectDir, level: "project" });
|
|
52
54
|
break;
|
|
53
55
|
}
|
|
@@ -55,7 +57,8 @@ function getConfigDirs(ctx: LoadContext): Array<{ dir: string; level: "user" | "
|
|
|
55
57
|
|
|
56
58
|
for (const name of USER_DIRS) {
|
|
57
59
|
const userDir = join(ctx.home, name, PATHS.userAgent.replace(`${PATHS.userBase}/`, ""));
|
|
58
|
-
|
|
60
|
+
const entries = await readDirEntries(userDir);
|
|
61
|
+
if (entries.length > 0) {
|
|
59
62
|
result.push({ dir: userDir, level: "user" });
|
|
60
63
|
break;
|
|
61
64
|
}
|
|
@@ -65,53 +68,19 @@ function getConfigDirs(ctx: LoadContext): Array<{ dir: string; level: "user" | "
|
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
// MCP
|
|
68
|
-
function loadMCPServers(ctx: LoadContext): LoadResult<MCPServer
|
|
71
|
+
async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>> {
|
|
69
72
|
const items: MCPServer[] = [];
|
|
70
73
|
const warnings: string[] = [];
|
|
71
74
|
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
if (!projectDir) continue;
|
|
75
|
-
|
|
76
|
-
for (const filename of ["mcp.json", ".mcp.json"]) {
|
|
77
|
-
const path = join(projectDir, filename);
|
|
78
|
-
const content = ctx.fs.readFile(path);
|
|
79
|
-
if (!content) continue;
|
|
80
|
-
|
|
81
|
-
const data = parseJSON<{ mcpServers?: Record<string, unknown> }>(content);
|
|
82
|
-
if (!data?.mcpServers) continue;
|
|
83
|
-
|
|
84
|
-
const expanded = expandEnvVarsDeep(data.mcpServers);
|
|
85
|
-
for (const [serverName, config] of Object.entries(expanded)) {
|
|
86
|
-
const serverConfig = config as Record<string, unknown>;
|
|
87
|
-
items.push({
|
|
88
|
-
name: serverName,
|
|
89
|
-
command: serverConfig.command as string | undefined,
|
|
90
|
-
args: serverConfig.args as string[] | undefined,
|
|
91
|
-
env: serverConfig.env as Record<string, string> | undefined,
|
|
92
|
-
url: serverConfig.url as string | undefined,
|
|
93
|
-
headers: serverConfig.headers as Record<string, string> | undefined,
|
|
94
|
-
transport: serverConfig.type as "stdio" | "sse" | "http" | undefined,
|
|
95
|
-
_source: createSourceMeta(PROVIDER_ID, path, "project"),
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
break;
|
|
99
|
-
}
|
|
100
|
-
break;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
for (const name of USER_DIRS) {
|
|
104
|
-
const userPath = join(ctx.home, name, "mcp.json");
|
|
105
|
-
const content = ctx.fs.readFile(userPath);
|
|
106
|
-
if (!content) continue;
|
|
107
|
-
|
|
75
|
+
const parseMcpServers = (content: string, path: string, level: "user" | "project"): MCPServer[] => {
|
|
76
|
+
const result: MCPServer[] = [];
|
|
108
77
|
const data = parseJSON<{ mcpServers?: Record<string, unknown> }>(content);
|
|
109
|
-
if (!data?.mcpServers)
|
|
78
|
+
if (!data?.mcpServers) return result;
|
|
110
79
|
|
|
111
80
|
const expanded = expandEnvVarsDeep(data.mcpServers);
|
|
112
81
|
for (const [serverName, config] of Object.entries(expanded)) {
|
|
113
82
|
const serverConfig = config as Record<string, unknown>;
|
|
114
|
-
|
|
83
|
+
result.push({
|
|
115
84
|
name: serverName,
|
|
116
85
|
command: serverConfig.command as string | undefined,
|
|
117
86
|
args: serverConfig.args as string[] | undefined,
|
|
@@ -119,10 +88,41 @@ function loadMCPServers(ctx: LoadContext): LoadResult<MCPServer> {
|
|
|
119
88
|
url: serverConfig.url as string | undefined,
|
|
120
89
|
headers: serverConfig.headers as Record<string, string> | undefined,
|
|
121
90
|
transport: serverConfig.type as "stdio" | "sse" | "http" | undefined,
|
|
122
|
-
_source: createSourceMeta(PROVIDER_ID,
|
|
91
|
+
_source: createSourceMeta(PROVIDER_ID, path, level),
|
|
123
92
|
});
|
|
124
93
|
}
|
|
125
|
-
|
|
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
|
+
}
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
return { items, warnings };
|
|
@@ -137,48 +137,54 @@ registerProvider<MCPServer>(mcpCapability.id, {
|
|
|
137
137
|
});
|
|
138
138
|
|
|
139
139
|
// System Prompt (SYSTEM.md)
|
|
140
|
-
function loadSystemPrompt(ctx: LoadContext): LoadResult<SystemPrompt
|
|
140
|
+
async function loadSystemPrompt(ctx: LoadContext): Promise<LoadResult<SystemPrompt>> {
|
|
141
141
|
const items: SystemPrompt[] = [];
|
|
142
142
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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) {
|
|
148
150
|
items.push({
|
|
149
|
-
path:
|
|
150
|
-
content
|
|
151
|
+
path: userPaths[i],
|
|
152
|
+
content,
|
|
151
153
|
level: "user",
|
|
152
|
-
_source: createSourceMeta(PROVIDER_ID,
|
|
154
|
+
_source: createSourceMeta(PROVIDER_ID, userPaths[i], "user"),
|
|
153
155
|
});
|
|
154
|
-
break;
|
|
156
|
+
break;
|
|
155
157
|
}
|
|
156
158
|
}
|
|
157
159
|
|
|
158
|
-
|
|
160
|
+
const ancestors: string[] = [];
|
|
159
161
|
let current = ctx.cwd;
|
|
160
162
|
while (true) {
|
|
161
|
-
|
|
162
|
-
const configDir = join(current, name);
|
|
163
|
-
if (ctx.fs.isDir(configDir)) {
|
|
164
|
-
const projectPath = join(configDir, "SYSTEM.md");
|
|
165
|
-
const content = ctx.fs.readFile(projectPath);
|
|
166
|
-
if (content) {
|
|
167
|
-
items.push({
|
|
168
|
-
path: projectPath,
|
|
169
|
-
content,
|
|
170
|
-
level: "project",
|
|
171
|
-
_source: createSourceMeta(PROVIDER_ID, projectPath, "project"),
|
|
172
|
-
});
|
|
173
|
-
break; // First config dir in this directory wins
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
163
|
+
ancestors.push(current);
|
|
177
164
|
const parent = dirname(current);
|
|
178
165
|
if (parent === current) break;
|
|
179
166
|
current = parent;
|
|
180
167
|
}
|
|
181
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
|
+
|
|
182
188
|
return { items, warnings: [] };
|
|
183
189
|
}
|
|
184
190
|
|
|
@@ -191,23 +197,23 @@ registerProvider<SystemPrompt>(systemPromptCapability.id, {
|
|
|
191
197
|
});
|
|
192
198
|
|
|
193
199
|
// Skills
|
|
194
|
-
function loadSkills(ctx: LoadContext): LoadResult<Skill
|
|
195
|
-
const
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
items.push(...result.items);
|
|
207
|
-
if (result.warnings) warnings.push(...result.warnings);
|
|
208
|
-
}
|
|
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
|
+
);
|
|
209
212
|
|
|
210
|
-
return {
|
|
213
|
+
return {
|
|
214
|
+
items: results.flatMap((r) => r.items),
|
|
215
|
+
warnings: results.flatMap((r) => r.warnings ?? []),
|
|
216
|
+
};
|
|
211
217
|
}
|
|
212
218
|
|
|
213
219
|
registerProvider<Skill>(skillCapability.id, {
|
|
@@ -219,13 +225,13 @@ registerProvider<Skill>(skillCapability.id, {
|
|
|
219
225
|
});
|
|
220
226
|
|
|
221
227
|
// Slash Commands
|
|
222
|
-
function loadSlashCommands(ctx: LoadContext): LoadResult<SlashCommand
|
|
228
|
+
async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashCommand>> {
|
|
223
229
|
const items: SlashCommand[] = [];
|
|
224
230
|
const warnings: string[] = [];
|
|
225
231
|
|
|
226
|
-
for (const { dir, level } of getConfigDirs(ctx)) {
|
|
232
|
+
for (const { dir, level } of await getConfigDirs(ctx)) {
|
|
227
233
|
const commandsDir = join(dir, "commands");
|
|
228
|
-
const result = loadFilesFromDir<SlashCommand>(ctx, commandsDir, PROVIDER_ID, level, {
|
|
234
|
+
const result = await loadFilesFromDir<SlashCommand>(ctx, commandsDir, PROVIDER_ID, level, {
|
|
229
235
|
extensions: ["md"],
|
|
230
236
|
transform: (name, content, path, source) => ({
|
|
231
237
|
name: name.replace(/\.md$/, ""),
|
|
@@ -251,13 +257,13 @@ registerProvider<SlashCommand>(slashCommandCapability.id, {
|
|
|
251
257
|
});
|
|
252
258
|
|
|
253
259
|
// Rules
|
|
254
|
-
function loadRules(ctx: LoadContext): LoadResult<Rule
|
|
260
|
+
async function loadRules(ctx: LoadContext): Promise<LoadResult<Rule>> {
|
|
255
261
|
const items: Rule[] = [];
|
|
256
262
|
const warnings: string[] = [];
|
|
257
263
|
|
|
258
|
-
for (const { dir, level } of getConfigDirs(ctx)) {
|
|
264
|
+
for (const { dir, level } of await getConfigDirs(ctx)) {
|
|
259
265
|
const rulesDir = join(dir, "rules");
|
|
260
|
-
const result = loadFilesFromDir<Rule>(ctx, rulesDir, PROVIDER_ID, level, {
|
|
266
|
+
const result = await loadFilesFromDir<Rule>(ctx, rulesDir, PROVIDER_ID, level, {
|
|
261
267
|
extensions: ["md", "mdc"],
|
|
262
268
|
transform: (name, content, path, source) => {
|
|
263
269
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
@@ -289,13 +295,13 @@ registerProvider<Rule>(ruleCapability.id, {
|
|
|
289
295
|
});
|
|
290
296
|
|
|
291
297
|
// Prompts
|
|
292
|
-
function loadPrompts(ctx: LoadContext): LoadResult<Prompt
|
|
298
|
+
async function loadPrompts(ctx: LoadContext): Promise<LoadResult<Prompt>> {
|
|
293
299
|
const items: Prompt[] = [];
|
|
294
300
|
const warnings: string[] = [];
|
|
295
301
|
|
|
296
|
-
for (const { dir, level } of getConfigDirs(ctx)) {
|
|
302
|
+
for (const { dir, level } of await getConfigDirs(ctx)) {
|
|
297
303
|
const promptsDir = join(dir, "prompts");
|
|
298
|
-
const result = loadFilesFromDir<Prompt>(ctx, promptsDir, PROVIDER_ID, level, {
|
|
304
|
+
const result = await loadFilesFromDir<Prompt>(ctx, promptsDir, PROVIDER_ID, level, {
|
|
299
305
|
extensions: ["md"],
|
|
300
306
|
transform: (name, content, path, source) => ({
|
|
301
307
|
name: name.replace(/\.md$/, ""),
|
|
@@ -320,7 +326,7 @@ registerProvider<Prompt>(promptCapability.id, {
|
|
|
320
326
|
});
|
|
321
327
|
|
|
322
328
|
// Extension Modules
|
|
323
|
-
function loadExtensionModules(ctx: LoadContext): LoadResult<ExtensionModule
|
|
329
|
+
async function loadExtensionModules(ctx: LoadContext): Promise<LoadResult<ExtensionModule>> {
|
|
324
330
|
const items: ExtensionModule[] = [];
|
|
325
331
|
const warnings: string[] = [];
|
|
326
332
|
|
|
@@ -337,45 +343,88 @@ function loadExtensionModules(ctx: LoadContext): LoadResult<ExtensionModule> {
|
|
|
337
343
|
return resolve(ctx.cwd, rawPath);
|
|
338
344
|
};
|
|
339
345
|
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
});
|
|
347
|
-
};
|
|
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
|
+
});
|
|
348
352
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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));
|
|
354
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;
|
|
355
377
|
|
|
356
378
|
const settingsPath = join(dir, "settings.json");
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
continue;
|
|
366
|
-
}
|
|
367
|
-
const resolvedPath = resolveExtensionPath(entry);
|
|
368
|
-
if (ctx.fs.isDir(resolvedPath)) {
|
|
369
|
-
for (const extPath of discoverExtensionModulePaths(ctx, resolvedPath)) {
|
|
370
|
-
addExtensionPath(extPath, level);
|
|
371
|
-
}
|
|
372
|
-
} else if (ctx.fs.isFile(resolvedPath)) {
|
|
373
|
-
addExtensionPath(resolvedPath, level);
|
|
374
|
-
} else {
|
|
375
|
-
warnings.push(`Extension path not found: ${resolvedPath}`);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
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;
|
|
378
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));
|
|
379
428
|
}
|
|
380
429
|
}
|
|
381
430
|
|
|
@@ -391,40 +440,61 @@ registerProvider<ExtensionModule>(extensionModuleCapability.id, {
|
|
|
391
440
|
});
|
|
392
441
|
|
|
393
442
|
// Extensions
|
|
394
|
-
function loadExtensions(ctx: LoadContext): LoadResult<Extension
|
|
443
|
+
async function loadExtensions(ctx: LoadContext): Promise<LoadResult<Extension>> {
|
|
395
444
|
const items: Extension[] = [];
|
|
396
445
|
const warnings: string[] = [];
|
|
397
446
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
if (!ctx.fs.isDir(extensionsDir)) continue;
|
|
401
|
-
|
|
402
|
-
for (const name of ctx.fs.readDir(extensionsDir)) {
|
|
403
|
-
if (name.startsWith(".")) continue;
|
|
447
|
+
const configDirs = await getConfigDirs(ctx);
|
|
448
|
+
const entriesResults = await Promise.all(configDirs.map(({ dir }) => readDirEntries(join(dir, "extensions"))));
|
|
404
449
|
|
|
405
|
-
|
|
406
|
-
|
|
450
|
+
const manifestCandidates: Array<{
|
|
451
|
+
extDir: string;
|
|
452
|
+
manifestPath: string;
|
|
453
|
+
entryName: string;
|
|
454
|
+
level: "user" | "project";
|
|
455
|
+
}> = [];
|
|
407
456
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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");
|
|
411
461
|
|
|
412
|
-
|
|
413
|
-
if (
|
|
414
|
-
|
|
415
|
-
continue;
|
|
416
|
-
}
|
|
462
|
+
for (const entry of entries) {
|
|
463
|
+
if (entry.name.startsWith(".")) continue;
|
|
464
|
+
if (!entry.isDirectory()) continue;
|
|
417
465
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
466
|
+
const extDir = join(extensionsDir, entry.name);
|
|
467
|
+
manifestCandidates.push({
|
|
468
|
+
extDir,
|
|
469
|
+
manifestPath: join(extDir, "gemini-extension.json"),
|
|
470
|
+
entryName: entry.name,
|
|
422
471
|
level,
|
|
423
|
-
_source: createSourceMeta(PROVIDER_ID, manifestPath, level),
|
|
424
472
|
});
|
|
425
473
|
}
|
|
426
474
|
}
|
|
427
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
|
+
|
|
428
498
|
return { items, warnings };
|
|
429
499
|
}
|
|
430
500
|
|
|
@@ -437,13 +507,13 @@ registerProvider<Extension>(extensionCapability.id, {
|
|
|
437
507
|
});
|
|
438
508
|
|
|
439
509
|
// Instructions
|
|
440
|
-
function loadInstructions(ctx: LoadContext): LoadResult<Instruction
|
|
510
|
+
async function loadInstructions(ctx: LoadContext): Promise<LoadResult<Instruction>> {
|
|
441
511
|
const items: Instruction[] = [];
|
|
442
512
|
const warnings: string[] = [];
|
|
443
513
|
|
|
444
|
-
for (const { dir, level } of getConfigDirs(ctx)) {
|
|
514
|
+
for (const { dir, level } of await getConfigDirs(ctx)) {
|
|
445
515
|
const instructionsDir = join(dir, "instructions");
|
|
446
|
-
const result = loadFilesFromDir<Instruction>(ctx, instructionsDir, PROVIDER_ID, level, {
|
|
516
|
+
const result = await loadFilesFromDir<Instruction>(ctx, instructionsDir, PROVIDER_ID, level, {
|
|
447
517
|
extensions: ["md"],
|
|
448
518
|
transform: (name, content, path, source) => {
|
|
449
519
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
@@ -472,35 +542,50 @@ registerProvider<Instruction>(instructionCapability.id, {
|
|
|
472
542
|
});
|
|
473
543
|
|
|
474
544
|
// Hooks
|
|
475
|
-
function loadHooks(ctx: LoadContext): LoadResult<Hook
|
|
545
|
+
async function loadHooks(ctx: LoadContext): Promise<LoadResult<Hook>> {
|
|
476
546
|
const items: Hook[] = [];
|
|
477
547
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
if (!ctx.fs.isDir(hooksDir)) continue;
|
|
548
|
+
const configDirs = await getConfigDirs(ctx);
|
|
549
|
+
const hookTypes = ["pre", "post"] as const;
|
|
481
550
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
551
|
+
const typeDirRequests: Array<{
|
|
552
|
+
typeDir: string;
|
|
553
|
+
hookType: (typeof hookTypes)[number];
|
|
554
|
+
level: "user" | "project";
|
|
555
|
+
}> = [];
|
|
485
556
|
|
|
486
|
-
|
|
487
|
-
|
|
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
|
+
}
|
|
488
566
|
|
|
489
|
-
|
|
490
|
-
if (!ctx.fs.isFile(path)) continue;
|
|
567
|
+
const typeEntriesResults = await Promise.all(typeDirRequests.map(({ typeDir }) => readDirEntries(typeDir)));
|
|
491
568
|
|
|
492
|
-
|
|
493
|
-
|
|
569
|
+
for (let i = 0; i < typeDirRequests.length; i++) {
|
|
570
|
+
const { typeDir, hookType, level } = typeDirRequests[i];
|
|
571
|
+
const typeEntries = typeEntriesResults[i];
|
|
494
572
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
573
|
+
for (const entry of typeEntries) {
|
|
574
|
+
if (entry.name.startsWith(".")) continue;
|
|
575
|
+
if (!entry.isFile()) continue;
|
|
576
|
+
|
|
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;
|
|
580
|
+
|
|
581
|
+
items.push({
|
|
582
|
+
name: entry.name,
|
|
583
|
+
path,
|
|
584
|
+
type: hookType,
|
|
585
|
+
tool,
|
|
586
|
+
level,
|
|
587
|
+
_source: createSourceMeta(PROVIDER_ID, path, level),
|
|
588
|
+
});
|
|
504
589
|
}
|
|
505
590
|
}
|
|
506
591
|
|
|
@@ -516,58 +601,86 @@ registerProvider<Hook>(hookCapability.id, {
|
|
|
516
601
|
});
|
|
517
602
|
|
|
518
603
|
// Custom Tools
|
|
519
|
-
function loadTools(ctx: LoadContext): LoadResult<CustomTool
|
|
604
|
+
async function loadTools(ctx: LoadContext): Promise<LoadResult<CustomTool>> {
|
|
520
605
|
const items: CustomTool[] = [];
|
|
521
606
|
const warnings: string[] = [];
|
|
522
607
|
|
|
523
|
-
|
|
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
|
+
|
|
524
623
|
const toolsDir = join(dir, "tools");
|
|
525
|
-
if (!ctx.fs.isDir(toolsDir)) continue;
|
|
526
624
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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);
|
|
533
640
|
return {
|
|
534
|
-
name:
|
|
641
|
+
name: (frontmatter.name as string) || name.replace(/\.md$/, ""),
|
|
535
642
|
path,
|
|
536
|
-
description:
|
|
643
|
+
description: frontmatter.description as string | undefined,
|
|
537
644
|
level,
|
|
538
645
|
_source: source,
|
|
539
646
|
};
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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) {
|
|
551
669
|
items.push(...result.items);
|
|
552
670
|
if (result.warnings) warnings.push(...result.warnings);
|
|
671
|
+
}
|
|
553
672
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
path: indexPath,
|
|
566
|
-
description: undefined,
|
|
567
|
-
level,
|
|
568
|
-
_source: createSourceMeta(PROVIDER_ID, indexPath, level),
|
|
569
|
-
});
|
|
570
|
-
}
|
|
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
|
+
});
|
|
571
684
|
}
|
|
572
685
|
}
|
|
573
686
|
|
|
@@ -583,13 +696,13 @@ registerProvider<CustomTool>(toolCapability.id, {
|
|
|
583
696
|
});
|
|
584
697
|
|
|
585
698
|
// Settings
|
|
586
|
-
function loadSettings(ctx: LoadContext): LoadResult<Settings
|
|
699
|
+
async function loadSettings(ctx: LoadContext): Promise<LoadResult<Settings>> {
|
|
587
700
|
const items: Settings[] = [];
|
|
588
701
|
const warnings: string[] = [];
|
|
589
702
|
|
|
590
|
-
for (const { dir, level } of getConfigDirs(ctx)) {
|
|
703
|
+
for (const { dir, level } of await getConfigDirs(ctx)) {
|
|
591
704
|
const settingsPath = join(dir, "settings.json");
|
|
592
|
-
const content =
|
|
705
|
+
const content = await readFile(settingsPath);
|
|
593
706
|
if (!content) continue;
|
|
594
707
|
|
|
595
708
|
const data = parseJSON<Record<string, unknown>>(content);
|
|
@@ -618,52 +731,59 @@ registerProvider<Settings>(settingsCapability.id, {
|
|
|
618
731
|
});
|
|
619
732
|
|
|
620
733
|
// Context Files (AGENTS.md)
|
|
621
|
-
function loadContextFiles(ctx: LoadContext): LoadResult<ContextFile
|
|
734
|
+
async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFile>> {
|
|
622
735
|
const items: ContextFile[] = [];
|
|
623
736
|
const warnings: string[] = [];
|
|
624
737
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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];
|
|
629
744
|
if (content) {
|
|
630
745
|
items.push({
|
|
631
|
-
path:
|
|
746
|
+
path: userPaths[i],
|
|
632
747
|
content,
|
|
633
748
|
level: "user",
|
|
634
|
-
_source: createSourceMeta(PROVIDER_ID,
|
|
749
|
+
_source: createSourceMeta(PROVIDER_ID, userPaths[i], "user"),
|
|
635
750
|
});
|
|
636
|
-
break;
|
|
751
|
+
break;
|
|
637
752
|
}
|
|
638
753
|
}
|
|
639
754
|
|
|
640
|
-
|
|
755
|
+
const ancestors: Array<{ dir: string; depth: number }> = [];
|
|
641
756
|
let current = ctx.cwd;
|
|
642
757
|
let depth = 0;
|
|
643
758
|
while (true) {
|
|
644
|
-
|
|
645
|
-
const configDir = join(current, name);
|
|
646
|
-
if (ctx.fs.isDir(configDir)) {
|
|
647
|
-
const projectPath = join(configDir, "AGENTS.md");
|
|
648
|
-
const content = ctx.fs.readFile(projectPath);
|
|
649
|
-
if (content) {
|
|
650
|
-
items.push({
|
|
651
|
-
path: projectPath,
|
|
652
|
-
content,
|
|
653
|
-
level: "project",
|
|
654
|
-
depth,
|
|
655
|
-
_source: createSourceMeta(PROVIDER_ID, projectPath, "project"),
|
|
656
|
-
});
|
|
657
|
-
return { items, warnings }; // First config dir wins
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
}
|
|
759
|
+
ancestors.push({ dir: current, depth });
|
|
661
760
|
const parent = dirname(current);
|
|
662
761
|
if (parent === current) break;
|
|
663
762
|
current = parent;
|
|
664
763
|
depth++;
|
|
665
764
|
}
|
|
666
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
|
+
|
|
667
787
|
return { items, warnings };
|
|
668
788
|
}
|
|
669
789
|
|