@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/helpers.ts
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* Shared helpers for discovery providers.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { join, resolve } from "path";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
6
|
import { parse as parseYAML } from "yaml";
|
|
7
|
+
import { readDirEntries, readFile } from "../capability/fs";
|
|
8
|
+
import type { Skill, SkillFrontmatter } from "../capability/skill";
|
|
7
9
|
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
|
|
8
10
|
|
|
9
11
|
/**
|
|
@@ -70,16 +72,13 @@ export function getUserPath(ctx: LoadContext, source: SourceId, subpath: string)
|
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
/**
|
|
73
|
-
* Get project-level path for a source (
|
|
75
|
+
* Get project-level path for a source (cwd only).
|
|
74
76
|
*/
|
|
75
77
|
export function getProjectPath(ctx: LoadContext, source: SourceId, subpath: string): string | null {
|
|
76
78
|
const paths = SOURCE_PATHS[source];
|
|
77
79
|
if (!paths.projectDir) return null;
|
|
78
80
|
|
|
79
|
-
|
|
80
|
-
if (!found) return null;
|
|
81
|
-
|
|
82
|
-
return join(found, subpath);
|
|
81
|
+
return join(ctx.cwd, paths.projectDir, subpath);
|
|
83
82
|
}
|
|
84
83
|
|
|
85
84
|
/**
|
|
@@ -126,6 +125,59 @@ export function parseFrontmatter(content: string): {
|
|
|
126
125
|
}
|
|
127
126
|
}
|
|
128
127
|
|
|
128
|
+
export async function loadSkillsFromDir(
|
|
129
|
+
_ctx: LoadContext,
|
|
130
|
+
options: {
|
|
131
|
+
dir: string;
|
|
132
|
+
providerId: string;
|
|
133
|
+
level: "user" | "project";
|
|
134
|
+
requireDescription?: boolean;
|
|
135
|
+
},
|
|
136
|
+
): Promise<LoadResult<Skill>> {
|
|
137
|
+
const items: Skill[] = [];
|
|
138
|
+
const warnings: string[] = [];
|
|
139
|
+
const { dir, level, providerId, requireDescription = false } = options;
|
|
140
|
+
|
|
141
|
+
const entries = await readDirEntries(dir);
|
|
142
|
+
const skillDirs = entries.filter(
|
|
143
|
+
(entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules",
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const results = await Promise.all(
|
|
147
|
+
skillDirs.map(async (entry) => {
|
|
148
|
+
const skillFile = join(dir, entry.name, "SKILL.md");
|
|
149
|
+
const content = await readFile(skillFile);
|
|
150
|
+
if (!content) {
|
|
151
|
+
return { item: null as Skill | null, warning: null as string | null };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
155
|
+
if (requireDescription && !frontmatter.description) {
|
|
156
|
+
return { item: null as Skill | null, warning: null as string | null };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
item: {
|
|
161
|
+
name: (frontmatter.name as string) || entry.name,
|
|
162
|
+
path: skillFile,
|
|
163
|
+
content: body,
|
|
164
|
+
frontmatter: frontmatter as SkillFrontmatter,
|
|
165
|
+
level,
|
|
166
|
+
_source: createSourceMeta(providerId, skillFile, level),
|
|
167
|
+
},
|
|
168
|
+
warning: null as string | null,
|
|
169
|
+
};
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
for (const result of results) {
|
|
174
|
+
if (result.warning) warnings.push(result.warning);
|
|
175
|
+
if (result.item) items.push(result.item);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { items, warnings };
|
|
179
|
+
}
|
|
180
|
+
|
|
129
181
|
/**
|
|
130
182
|
* Expand environment variables in a string.
|
|
131
183
|
* Supports ${VAR} and ${VAR:-default} syntax.
|
|
@@ -162,8 +214,8 @@ export function expandEnvVarsDeep<T>(obj: T, extraEnv?: Record<string, string>):
|
|
|
162
214
|
/**
|
|
163
215
|
* Load files from a directory matching a pattern.
|
|
164
216
|
*/
|
|
165
|
-
export function loadFilesFromDir<T>(
|
|
166
|
-
|
|
217
|
+
export async function loadFilesFromDir<T>(
|
|
218
|
+
_ctx: LoadContext,
|
|
167
219
|
dir: string,
|
|
168
220
|
provider: string,
|
|
169
221
|
level: "user" | "project",
|
|
@@ -175,37 +227,40 @@ export function loadFilesFromDir<T>(
|
|
|
175
227
|
/** Whether to recurse into subdirectories */
|
|
176
228
|
recursive?: boolean;
|
|
177
229
|
},
|
|
178
|
-
): LoadResult<T
|
|
230
|
+
): Promise<LoadResult<T>> {
|
|
231
|
+
const entries = await readDirEntries(dir);
|
|
232
|
+
|
|
233
|
+
const visibleEntries = entries.filter((entry) => !entry.name.startsWith("."));
|
|
234
|
+
|
|
235
|
+
const directories = options.recursive ? visibleEntries.filter((entry) => entry.isDirectory()) : [];
|
|
236
|
+
|
|
237
|
+
const files = visibleEntries
|
|
238
|
+
.filter((entry) => entry.isFile())
|
|
239
|
+
.filter((entry) => {
|
|
240
|
+
if (!options.extensions) return true;
|
|
241
|
+
return options.extensions.some((ext) => entry.name.endsWith(`.${ext}`));
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const [subResults, fileResults] = await Promise.all([
|
|
245
|
+
Promise.all(directories.map((entry) => loadFilesFromDir(_ctx, join(dir, entry.name), provider, level, options))),
|
|
246
|
+
Promise.all(
|
|
247
|
+
files.map(async (entry) => {
|
|
248
|
+
const path = join(dir, entry.name);
|
|
249
|
+
const content = await readFile(path);
|
|
250
|
+
return { entry, path, content };
|
|
251
|
+
}),
|
|
252
|
+
),
|
|
253
|
+
]);
|
|
254
|
+
|
|
179
255
|
const items: T[] = [];
|
|
180
256
|
const warnings: string[] = [];
|
|
181
257
|
|
|
182
|
-
|
|
183
|
-
|
|
258
|
+
for (const subResult of subResults) {
|
|
259
|
+
items.push(...subResult.items);
|
|
260
|
+
if (subResult.warnings) warnings.push(...subResult.warnings);
|
|
184
261
|
}
|
|
185
262
|
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
for (const name of files) {
|
|
189
|
-
if (name.startsWith(".")) continue;
|
|
190
|
-
|
|
191
|
-
const path = join(dir, name);
|
|
192
|
-
|
|
193
|
-
if (options.recursive && ctx.fs.isDir(path)) {
|
|
194
|
-
const subResult = loadFilesFromDir(ctx, path, provider, level, options);
|
|
195
|
-
items.push(...subResult.items);
|
|
196
|
-
if (subResult.warnings) warnings.push(...subResult.warnings);
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if (!ctx.fs.isFile(path)) continue;
|
|
201
|
-
|
|
202
|
-
// Check extension
|
|
203
|
-
if (options.extensions) {
|
|
204
|
-
const hasMatch = options.extensions.some((ext) => name.endsWith(`.${ext}`));
|
|
205
|
-
if (!hasMatch) continue;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const content = ctx.fs.readFile(path);
|
|
263
|
+
for (const { entry, path, content } of fileResults) {
|
|
209
264
|
if (content === null) {
|
|
210
265
|
warnings.push(`Failed to read file: ${path}`);
|
|
211
266
|
continue;
|
|
@@ -214,7 +269,7 @@ export function loadFilesFromDir<T>(
|
|
|
214
269
|
const source = createSourceMeta(provider, path, level);
|
|
215
270
|
|
|
216
271
|
try {
|
|
217
|
-
const item = options.transform(name, content, path, source);
|
|
272
|
+
const item = options.transform(entry.name, content, path, source);
|
|
218
273
|
if (item !== null) {
|
|
219
274
|
items.push(item);
|
|
220
275
|
}
|
|
@@ -252,8 +307,11 @@ interface ExtensionModuleManifest {
|
|
|
252
307
|
extensions?: string[];
|
|
253
308
|
}
|
|
254
309
|
|
|
255
|
-
function readExtensionModuleManifest(
|
|
256
|
-
|
|
310
|
+
async function readExtensionModuleManifest(
|
|
311
|
+
_ctx: LoadContext,
|
|
312
|
+
packageJsonPath: string,
|
|
313
|
+
): Promise<ExtensionModuleManifest | null> {
|
|
314
|
+
const content = await readFile(packageJsonPath);
|
|
257
315
|
if (!content) return null;
|
|
258
316
|
|
|
259
317
|
const pkg = parseJSON<{ omp?: ExtensionModuleManifest; pi?: ExtensionModuleManifest }>(content);
|
|
@@ -278,34 +336,35 @@ function isExtensionModuleFile(name: string): boolean {
|
|
|
278
336
|
*
|
|
279
337
|
* No recursion beyond one level. Complex packages must use package.json manifest.
|
|
280
338
|
*/
|
|
281
|
-
export function discoverExtensionModulePaths(ctx: LoadContext, dir: string): string[] {
|
|
282
|
-
if (!ctx.fs.isDir(dir)) {
|
|
283
|
-
return [];
|
|
284
|
-
}
|
|
285
|
-
|
|
339
|
+
export async function discoverExtensionModulePaths(ctx: LoadContext, dir: string): Promise<string[]> {
|
|
286
340
|
const discovered: string[] = [];
|
|
341
|
+
const entries = await readDirEntries(dir);
|
|
287
342
|
|
|
288
|
-
for (const
|
|
289
|
-
if (name.startsWith(".")) continue;
|
|
343
|
+
for (const entry of entries) {
|
|
344
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
290
345
|
|
|
291
|
-
const entryPath = join(dir, name);
|
|
346
|
+
const entryPath = join(dir, entry.name);
|
|
292
347
|
|
|
293
348
|
// 1. Direct files: *.ts or *.js
|
|
294
|
-
if (
|
|
349
|
+
if (entry.isFile() && isExtensionModuleFile(entry.name)) {
|
|
295
350
|
discovered.push(entryPath);
|
|
296
351
|
continue;
|
|
297
352
|
}
|
|
298
353
|
|
|
299
354
|
// 2 & 3. Subdirectories
|
|
300
|
-
if (
|
|
355
|
+
if (entry.isDirectory()) {
|
|
356
|
+
const subEntries = await readDirEntries(entryPath);
|
|
357
|
+
const subFileNames = new Set(subEntries.filter((e) => e.isFile()).map((e) => e.name));
|
|
358
|
+
|
|
301
359
|
// Check for package.json with "omp"/"pi" field first
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const manifest = readExtensionModuleManifest(ctx, packageJsonPath);
|
|
360
|
+
if (subFileNames.has("package.json")) {
|
|
361
|
+
const packageJsonPath = join(entryPath, "package.json");
|
|
362
|
+
const manifest = await readExtensionModuleManifest(ctx, packageJsonPath);
|
|
305
363
|
if (manifest?.extensions && Array.isArray(manifest.extensions)) {
|
|
306
364
|
for (const extPath of manifest.extensions) {
|
|
307
365
|
const resolvedExtPath = resolve(entryPath, extPath);
|
|
308
|
-
|
|
366
|
+
const content = await readFile(resolvedExtPath);
|
|
367
|
+
if (content !== null) {
|
|
309
368
|
discovered.push(resolvedExtPath);
|
|
310
369
|
}
|
|
311
370
|
}
|
|
@@ -314,12 +373,10 @@ export function discoverExtensionModulePaths(ctx: LoadContext, dir: string): str
|
|
|
314
373
|
}
|
|
315
374
|
|
|
316
375
|
// Check for index.ts or index.js
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
if (
|
|
320
|
-
discovered.push(
|
|
321
|
-
} else if (ctx.fs.isFile(indexJs)) {
|
|
322
|
-
discovered.push(indexJs);
|
|
376
|
+
if (subFileNames.has("index.ts")) {
|
|
377
|
+
discovered.push(join(entryPath, "index.ts"));
|
|
378
|
+
} else if (subFileNames.has("index.js")) {
|
|
379
|
+
discovered.push(join(entryPath, "index.js"));
|
|
323
380
|
}
|
|
324
381
|
}
|
|
325
382
|
}
|
package/src/discovery/index.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { join } from "node:path";
|
|
11
|
+
import { readFile } from "../capability/fs";
|
|
11
12
|
import { registerProvider } from "../capability/index";
|
|
12
13
|
import { type MCPServer, mcpCapability } from "../capability/mcp";
|
|
13
14
|
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
|
|
@@ -69,17 +70,16 @@ function transformMCPConfig(config: MCPConfigFile, source: SourceMeta): MCPServe
|
|
|
69
70
|
/**
|
|
70
71
|
* Load MCP servers from a JSON file.
|
|
71
72
|
*/
|
|
72
|
-
function loadMCPJsonFile(
|
|
73
|
+
async function loadMCPJsonFile(
|
|
74
|
+
_ctx: LoadContext,
|
|
75
|
+
path: string,
|
|
76
|
+
level: "user" | "project",
|
|
77
|
+
): Promise<LoadResult<MCPServer>> {
|
|
73
78
|
const warnings: string[] = [];
|
|
74
79
|
const items: MCPServer[] = [];
|
|
75
80
|
|
|
76
|
-
|
|
77
|
-
return { items, warnings };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const content = ctx.fs.readFile(path);
|
|
81
|
+
const content = await readFile(path);
|
|
81
82
|
if (content === null) {
|
|
82
|
-
warnings.push(`Failed to read ${path}`);
|
|
83
83
|
return { items, warnings };
|
|
84
84
|
}
|
|
85
85
|
|
|
@@ -99,17 +99,14 @@ function loadMCPJsonFile(ctx: LoadContext, path: string, level: "user" | "projec
|
|
|
99
99
|
/**
|
|
100
100
|
* MCP JSON Provider loader.
|
|
101
101
|
*/
|
|
102
|
-
function load(ctx: LoadContext): LoadResult<MCPServer
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
allItems.push(...result.items);
|
|
111
|
-
if (result.warnings) allWarnings.push(...result.warnings);
|
|
112
|
-
}
|
|
102
|
+
async function load(ctx: LoadContext): Promise<LoadResult<MCPServer>> {
|
|
103
|
+
const filenames = ["mcp.json", ".mcp.json"];
|
|
104
|
+
const results = await Promise.all(
|
|
105
|
+
filenames.map((filename) => loadMCPJsonFile(ctx, join(ctx.cwd, filename), "project")),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const allItems = results.flatMap((r) => r.items);
|
|
109
|
+
const allWarnings = results.flatMap((r) => r.warnings ?? []);
|
|
113
110
|
|
|
114
111
|
return {
|
|
115
112
|
items: allItems,
|
package/src/discovery/ssh.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
+
import { readFile } from "../capability/fs";
|
|
9
10
|
import { registerProvider } from "../capability/index";
|
|
10
11
|
import { type SSHHost, sshCapability } from "../capability/ssh";
|
|
11
12
|
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
|
|
@@ -90,17 +91,12 @@ function normalizeHost(
|
|
|
90
91
|
};
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
function loadSshJsonFile(
|
|
94
|
+
async function loadSshJsonFile(_ctx: LoadContext, path: string): Promise<LoadResult<SSHHost>> {
|
|
94
95
|
const items: SSHHost[] = [];
|
|
95
96
|
const warnings: string[] = [];
|
|
96
97
|
|
|
97
|
-
|
|
98
|
-
return { items, warnings };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const content = ctx.fs.readFile(path);
|
|
98
|
+
const content = await readFile(path);
|
|
102
99
|
if (content === null) {
|
|
103
|
-
warnings.push(`Failed to read ${path}`);
|
|
104
100
|
return { items, warnings };
|
|
105
101
|
}
|
|
106
102
|
|
|
@@ -126,7 +122,7 @@ function loadSshJsonFile(ctx: LoadContext, path: string): LoadResult<SSHHost> {
|
|
|
126
122
|
warnings.push(`Invalid host entry in ${path}: ${name}`);
|
|
127
123
|
continue;
|
|
128
124
|
}
|
|
129
|
-
const host = normalizeHost(name, rawHost, source,
|
|
125
|
+
const host = normalizeHost(name, rawHost, source, _ctx.home, warnings);
|
|
130
126
|
if (host) items.push(host);
|
|
131
127
|
}
|
|
132
128
|
|
|
@@ -136,16 +132,12 @@ function loadSshJsonFile(ctx: LoadContext, path: string): LoadResult<SSHHost> {
|
|
|
136
132
|
};
|
|
137
133
|
}
|
|
138
134
|
|
|
139
|
-
function load(ctx: LoadContext): LoadResult<SSHHost
|
|
140
|
-
const
|
|
141
|
-
const
|
|
135
|
+
async function load(ctx: LoadContext): Promise<LoadResult<SSHHost>> {
|
|
136
|
+
const filenames = ["ssh.json", ".ssh.json"];
|
|
137
|
+
const results = await Promise.all(filenames.map((filename) => loadSshJsonFile(ctx, join(ctx.cwd, filename))));
|
|
142
138
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const result = loadSshJsonFile(ctx, path);
|
|
146
|
-
allItems.push(...result.items);
|
|
147
|
-
if (result.warnings) allWarnings.push(...result.warnings);
|
|
148
|
-
}
|
|
139
|
+
const allItems = results.flatMap((r) => r.items);
|
|
140
|
+
const allWarnings = results.flatMap((r) => r.warnings ?? []);
|
|
149
141
|
|
|
150
142
|
return {
|
|
151
143
|
items: allItems,
|
package/src/discovery/vscode.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Supports MCP server discovery from `mcp.json` with nested `mcp.servers` structure.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { readFile } from "../capability/fs";
|
|
8
9
|
import { registerProvider } from "../capability/index";
|
|
9
10
|
import { type MCPServer, mcpCapability } from "../capability/mcp";
|
|
10
11
|
import type { LoadContext, LoadResult } from "../capability/types";
|
|
@@ -23,14 +24,14 @@ registerProvider<MCPServer>(mcpCapability.id, {
|
|
|
23
24
|
displayName: DISPLAY_NAME,
|
|
24
25
|
description: "Load MCP servers from .vscode/mcp.json",
|
|
25
26
|
priority: PRIORITY,
|
|
26
|
-
load(ctx: LoadContext): LoadResult<MCPServer
|
|
27
|
+
async load(ctx: LoadContext): Promise<LoadResult<MCPServer>> {
|
|
27
28
|
const items: MCPServer[] = [];
|
|
28
29
|
const warnings: string[] = [];
|
|
29
30
|
|
|
30
31
|
// Project-only (VS Code doesn't support user-level MCP config)
|
|
31
32
|
const projectPath = getProjectPath(ctx, "vscode", "mcp.json");
|
|
32
|
-
if (projectPath
|
|
33
|
-
const result = loadMCPConfig(ctx, projectPath, "project");
|
|
33
|
+
if (projectPath) {
|
|
34
|
+
const result = await loadMCPConfig(ctx, projectPath, "project");
|
|
34
35
|
items.push(...result.items);
|
|
35
36
|
if (result.warnings) warnings.push(...result.warnings);
|
|
36
37
|
}
|
|
@@ -43,11 +44,15 @@ registerProvider<MCPServer>(mcpCapability.id, {
|
|
|
43
44
|
* Load MCP servers from a mcp.json file.
|
|
44
45
|
* VS Code uses nested structure: { "mcp": { "servers": { ... } } }
|
|
45
46
|
*/
|
|
46
|
-
function loadMCPConfig(
|
|
47
|
+
async function loadMCPConfig(
|
|
48
|
+
_ctx: LoadContext,
|
|
49
|
+
path: string,
|
|
50
|
+
level: "user" | "project",
|
|
51
|
+
): Promise<LoadResult<MCPServer>> {
|
|
47
52
|
const items: MCPServer[] = [];
|
|
48
53
|
const warnings: string[] = [];
|
|
49
54
|
|
|
50
|
-
const content =
|
|
55
|
+
const content = await readFile(path);
|
|
51
56
|
if (!content) {
|
|
52
57
|
warnings.push(`Failed to read ${path}`);
|
|
53
58
|
return { items, warnings };
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* - Legacy .windsurfrules file
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { readFile } from "../capability/fs";
|
|
14
15
|
import { registerProvider } from "../capability/index";
|
|
15
16
|
import { type MCPServer, mcpCapability } from "../capability/mcp";
|
|
16
17
|
import { type Rule, ruleCapability } from "../capability/rule";
|
|
@@ -33,65 +34,58 @@ const PRIORITY = 50;
|
|
|
33
34
|
// MCP Servers
|
|
34
35
|
// =============================================================================
|
|
35
36
|
|
|
36
|
-
function
|
|
37
|
+
function parseServerConfig(
|
|
38
|
+
name: string,
|
|
39
|
+
serverConfig: unknown,
|
|
40
|
+
path: string,
|
|
41
|
+
scope: "user" | "project",
|
|
42
|
+
): { server?: MCPServer; warning?: string } {
|
|
43
|
+
if (typeof serverConfig !== "object" || serverConfig === null) {
|
|
44
|
+
return { warning: `Invalid server config for "${name}" in ${path}` };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const server = expandEnvVarsDeep(serverConfig as Record<string, unknown>);
|
|
48
|
+
return {
|
|
49
|
+
server: {
|
|
50
|
+
name,
|
|
51
|
+
command: server.command as string | undefined,
|
|
52
|
+
args: server.args as string[] | undefined,
|
|
53
|
+
env: server.env as Record<string, string> | undefined,
|
|
54
|
+
url: server.url as string | undefined,
|
|
55
|
+
headers: server.headers as Record<string, string> | undefined,
|
|
56
|
+
transport: server.type as "stdio" | "sse" | "http" | undefined,
|
|
57
|
+
_source: createSourceMeta(PROVIDER_ID, path, scope),
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>> {
|
|
37
63
|
const items: MCPServer[] = [];
|
|
38
64
|
const warnings: string[] = [];
|
|
39
65
|
|
|
40
|
-
// User-level: ~/.codeium/windsurf/mcp_config.json
|
|
41
66
|
const userPath = getUserPath(ctx, "windsurf", "mcp_config.json");
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (config?.mcpServers) {
|
|
47
|
-
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
|
|
48
|
-
if (typeof serverConfig !== "object" || serverConfig === null) {
|
|
49
|
-
warnings.push(`Invalid server config for "${name}" in ${userPath}`);
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const server = expandEnvVarsDeep(serverConfig as Record<string, unknown>);
|
|
54
|
-
items.push({
|
|
55
|
-
name,
|
|
56
|
-
command: server.command as string | undefined,
|
|
57
|
-
args: server.args as string[] | undefined,
|
|
58
|
-
env: server.env as Record<string, string> | undefined,
|
|
59
|
-
url: server.url as string | undefined,
|
|
60
|
-
headers: server.headers as Record<string, string> | undefined,
|
|
61
|
-
transport: server.type as "stdio" | "sse" | "http" | undefined,
|
|
62
|
-
_source: createSourceMeta(PROVIDER_ID, userPath, "user"),
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
67
|
+
const [userContent, projectPath] = await Promise.all([
|
|
68
|
+
userPath ? readFile(userPath) : Promise.resolve(null),
|
|
69
|
+
getProjectPath(ctx, "windsurf", "mcp_config.json"),
|
|
70
|
+
]);
|
|
68
71
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
args: server.args as string[] | undefined,
|
|
87
|
-
env: server.env as Record<string, string> | undefined,
|
|
88
|
-
url: server.url as string | undefined,
|
|
89
|
-
headers: server.headers as Record<string, string> | undefined,
|
|
90
|
-
transport: server.type as "stdio" | "sse" | "http" | undefined,
|
|
91
|
-
_source: createSourceMeta(PROVIDER_ID, projectPath, "project"),
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
}
|
|
72
|
+
const projectContent = projectPath ? await readFile(projectPath) : null;
|
|
73
|
+
|
|
74
|
+
const configs: Array<{ content: string | null; path: string | null; scope: "user" | "project" }> = [
|
|
75
|
+
{ content: userContent, path: userPath, scope: "user" },
|
|
76
|
+
{ content: projectContent, path: projectPath, scope: "project" },
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
for (const { content, path, scope } of configs) {
|
|
80
|
+
if (!content || !path) continue;
|
|
81
|
+
|
|
82
|
+
const config = parseJSON<{ mcpServers?: Record<string, unknown> }>(content);
|
|
83
|
+
if (!config?.mcpServers) continue;
|
|
84
|
+
|
|
85
|
+
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
|
|
86
|
+
const result = parseServerConfig(name, serverConfig, path, scope);
|
|
87
|
+
if (result.warning) warnings.push(result.warning);
|
|
88
|
+
if (result.server) items.push(result.server);
|
|
95
89
|
}
|
|
96
90
|
}
|
|
97
91
|
|
|
@@ -102,14 +96,14 @@ function loadMCPServers(ctx: LoadContext): LoadResult<MCPServer> {
|
|
|
102
96
|
// Rules
|
|
103
97
|
// =============================================================================
|
|
104
98
|
|
|
105
|
-
function loadRules(ctx: LoadContext): LoadResult<Rule
|
|
99
|
+
async function loadRules(ctx: LoadContext): Promise<LoadResult<Rule>> {
|
|
106
100
|
const items: Rule[] = [];
|
|
107
101
|
const warnings: string[] = [];
|
|
108
102
|
|
|
109
103
|
// User-level: ~/.codeium/windsurf/memories/global_rules.md
|
|
110
104
|
const userPath = getUserPath(ctx, "windsurf", "memories/global_rules.md");
|
|
111
|
-
if (userPath
|
|
112
|
-
const content =
|
|
105
|
+
if (userPath) {
|
|
106
|
+
const content = await readFile(userPath);
|
|
113
107
|
if (content) {
|
|
114
108
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
115
109
|
|
|
@@ -137,7 +131,7 @@ function loadRules(ctx: LoadContext): LoadResult<Rule> {
|
|
|
137
131
|
// Project-level: .windsurf/rules/*.md
|
|
138
132
|
const projectRulesDir = getProjectPath(ctx, "windsurf", "rules");
|
|
139
133
|
if (projectRulesDir) {
|
|
140
|
-
const result = loadFilesFromDir<Rule>(ctx, projectRulesDir, PROVIDER_ID, "project", {
|
|
134
|
+
const result = await loadFilesFromDir<Rule>(ctx, projectRulesDir, PROVIDER_ID, "project", {
|
|
141
135
|
extensions: ["md"],
|
|
142
136
|
transform: (name, content, path, source) => {
|
|
143
137
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
@@ -167,34 +161,6 @@ function loadRules(ctx: LoadContext): LoadResult<Rule> {
|
|
|
167
161
|
if (result.warnings) warnings.push(...result.warnings);
|
|
168
162
|
}
|
|
169
163
|
|
|
170
|
-
// Legacy: .windsurfrules in project root
|
|
171
|
-
const legacyPath = ctx.fs.walkUp(".windsurfrules", { file: true });
|
|
172
|
-
if (legacyPath) {
|
|
173
|
-
const content = ctx.fs.readFile(legacyPath);
|
|
174
|
-
if (content) {
|
|
175
|
-
const { frontmatter, body } = parseFrontmatter(content);
|
|
176
|
-
|
|
177
|
-
// Validate and normalize globs
|
|
178
|
-
let globs: string[] | undefined;
|
|
179
|
-
if (Array.isArray(frontmatter.globs)) {
|
|
180
|
-
globs = frontmatter.globs.filter((g): g is string => typeof g === "string");
|
|
181
|
-
} else if (typeof frontmatter.globs === "string") {
|
|
182
|
-
globs = [frontmatter.globs];
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
items.push({
|
|
186
|
-
name: "windsurfrules",
|
|
187
|
-
path: legacyPath,
|
|
188
|
-
content: body,
|
|
189
|
-
globs,
|
|
190
|
-
alwaysApply: frontmatter.alwaysApply as boolean | undefined,
|
|
191
|
-
description: frontmatter.description as string | undefined,
|
|
192
|
-
ttsrTrigger: typeof frontmatter.ttsr_trigger === "string" ? frontmatter.ttsr_trigger : undefined,
|
|
193
|
-
_source: createSourceMeta(PROVIDER_ID, legacyPath, "project"),
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
164
|
return { items, warnings };
|
|
199
165
|
}
|
|
200
166
|
|
package/src/main.ts
CHANGED
|
@@ -359,6 +359,10 @@ async function buildSessionOptions(
|
|
|
359
359
|
options.toolNames = parsed.tools;
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
+
if (parsed.noLsp) {
|
|
363
|
+
options.enableLsp = false;
|
|
364
|
+
}
|
|
365
|
+
|
|
362
366
|
// Skills
|
|
363
367
|
if (parsed.noSkills) {
|
|
364
368
|
options.skills = [];
|
|
@@ -461,7 +465,7 @@ export async function main(args: string[]) {
|
|
|
461
465
|
}
|
|
462
466
|
|
|
463
467
|
const cwd = process.cwd();
|
|
464
|
-
const settingsManager = SettingsManager.create(cwd);
|
|
468
|
+
const settingsManager = await SettingsManager.create(cwd);
|
|
465
469
|
settingsManager.applyEnvironmentVariables();
|
|
466
470
|
time("SettingsManager.create");
|
|
467
471
|
const { initialMessage, initialImages } = await prepareInitialMessage(parsed, settingsManager.getImageAutoResize());
|