@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/helpers.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
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";
|
|
7
8
|
import type { Skill, SkillFrontmatter } from "../capability/skill";
|
|
8
9
|
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
|
|
9
10
|
|
|
@@ -71,16 +72,13 @@ export function getUserPath(ctx: LoadContext, source: SourceId, subpath: string)
|
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
/**
|
|
74
|
-
* Get project-level path for a source (
|
|
75
|
+
* Get project-level path for a source (cwd only).
|
|
75
76
|
*/
|
|
76
77
|
export function getProjectPath(ctx: LoadContext, source: SourceId, subpath: string): string | null {
|
|
77
78
|
const paths = SOURCE_PATHS[source];
|
|
78
79
|
if (!paths.projectDir) return null;
|
|
79
80
|
|
|
80
|
-
|
|
81
|
-
if (!found) return null;
|
|
82
|
-
|
|
83
|
-
return join(found, subpath);
|
|
81
|
+
return join(ctx.cwd, paths.projectDir, subpath);
|
|
84
82
|
}
|
|
85
83
|
|
|
86
84
|
/**
|
|
@@ -127,51 +125,54 @@ export function parseFrontmatter(content: string): {
|
|
|
127
125
|
}
|
|
128
126
|
}
|
|
129
127
|
|
|
130
|
-
export function loadSkillsFromDir(
|
|
131
|
-
|
|
128
|
+
export async function loadSkillsFromDir(
|
|
129
|
+
_ctx: LoadContext,
|
|
132
130
|
options: {
|
|
133
131
|
dir: string;
|
|
134
132
|
providerId: string;
|
|
135
133
|
level: "user" | "project";
|
|
136
134
|
requireDescription?: boolean;
|
|
137
135
|
},
|
|
138
|
-
): LoadResult<Skill
|
|
136
|
+
): Promise<LoadResult<Skill>> {
|
|
139
137
|
const items: Skill[] = [];
|
|
140
138
|
const warnings: string[] = [];
|
|
141
139
|
const { dir, level, providerId, requireDescription = false } = options;
|
|
142
140
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const content = ctx.fs.readFile(skillFile);
|
|
157
|
-
if (!content) {
|
|
158
|
-
warnings.push(`Failed to read ${skillFile}`);
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
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
|
+
}
|
|
161
153
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
154
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
155
|
+
if (requireDescription && !frontmatter.description) {
|
|
156
|
+
return { item: null as Skill | null, warning: null as string | null };
|
|
157
|
+
}
|
|
166
158
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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);
|
|
175
176
|
}
|
|
176
177
|
|
|
177
178
|
return { items, warnings };
|
|
@@ -213,8 +214,8 @@ export function expandEnvVarsDeep<T>(obj: T, extraEnv?: Record<string, string>):
|
|
|
213
214
|
/**
|
|
214
215
|
* Load files from a directory matching a pattern.
|
|
215
216
|
*/
|
|
216
|
-
export function loadFilesFromDir<T>(
|
|
217
|
-
|
|
217
|
+
export async function loadFilesFromDir<T>(
|
|
218
|
+
_ctx: LoadContext,
|
|
218
219
|
dir: string,
|
|
219
220
|
provider: string,
|
|
220
221
|
level: "user" | "project",
|
|
@@ -226,37 +227,40 @@ export function loadFilesFromDir<T>(
|
|
|
226
227
|
/** Whether to recurse into subdirectories */
|
|
227
228
|
recursive?: boolean;
|
|
228
229
|
},
|
|
229
|
-
): LoadResult<T
|
|
230
|
-
const
|
|
231
|
-
const warnings: string[] = [];
|
|
230
|
+
): Promise<LoadResult<T>> {
|
|
231
|
+
const entries = await readDirEntries(dir);
|
|
232
232
|
|
|
233
|
-
|
|
234
|
-
return { items, warnings };
|
|
235
|
-
}
|
|
233
|
+
const visibleEntries = entries.filter((entry) => !entry.name.startsWith("."));
|
|
236
234
|
|
|
237
|
-
const
|
|
235
|
+
const directories = options.recursive ? visibleEntries.filter((entry) => entry.isDirectory()) : [];
|
|
238
236
|
|
|
239
|
-
|
|
240
|
-
|
|
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
|
+
});
|
|
241
243
|
|
|
242
|
-
|
|
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
|
+
]);
|
|
243
254
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
items.push(...subResult.items);
|
|
247
|
-
if (subResult.warnings) warnings.push(...subResult.warnings);
|
|
248
|
-
continue;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (!ctx.fs.isFile(path)) continue;
|
|
255
|
+
const items: T[] = [];
|
|
256
|
+
const warnings: string[] = [];
|
|
252
257
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
+
for (const subResult of subResults) {
|
|
259
|
+
items.push(...subResult.items);
|
|
260
|
+
if (subResult.warnings) warnings.push(...subResult.warnings);
|
|
261
|
+
}
|
|
258
262
|
|
|
259
|
-
|
|
263
|
+
for (const { entry, path, content } of fileResults) {
|
|
260
264
|
if (content === null) {
|
|
261
265
|
warnings.push(`Failed to read file: ${path}`);
|
|
262
266
|
continue;
|
|
@@ -265,7 +269,7 @@ export function loadFilesFromDir<T>(
|
|
|
265
269
|
const source = createSourceMeta(provider, path, level);
|
|
266
270
|
|
|
267
271
|
try {
|
|
268
|
-
const item = options.transform(name, content, path, source);
|
|
272
|
+
const item = options.transform(entry.name, content, path, source);
|
|
269
273
|
if (item !== null) {
|
|
270
274
|
items.push(item);
|
|
271
275
|
}
|
|
@@ -303,8 +307,11 @@ interface ExtensionModuleManifest {
|
|
|
303
307
|
extensions?: string[];
|
|
304
308
|
}
|
|
305
309
|
|
|
306
|
-
function readExtensionModuleManifest(
|
|
307
|
-
|
|
310
|
+
async function readExtensionModuleManifest(
|
|
311
|
+
_ctx: LoadContext,
|
|
312
|
+
packageJsonPath: string,
|
|
313
|
+
): Promise<ExtensionModuleManifest | null> {
|
|
314
|
+
const content = await readFile(packageJsonPath);
|
|
308
315
|
if (!content) return null;
|
|
309
316
|
|
|
310
317
|
const pkg = parseJSON<{ omp?: ExtensionModuleManifest; pi?: ExtensionModuleManifest }>(content);
|
|
@@ -329,34 +336,35 @@ function isExtensionModuleFile(name: string): boolean {
|
|
|
329
336
|
*
|
|
330
337
|
* No recursion beyond one level. Complex packages must use package.json manifest.
|
|
331
338
|
*/
|
|
332
|
-
export function discoverExtensionModulePaths(ctx: LoadContext, dir: string): string[] {
|
|
333
|
-
if (!ctx.fs.isDir(dir)) {
|
|
334
|
-
return [];
|
|
335
|
-
}
|
|
336
|
-
|
|
339
|
+
export async function discoverExtensionModulePaths(ctx: LoadContext, dir: string): Promise<string[]> {
|
|
337
340
|
const discovered: string[] = [];
|
|
341
|
+
const entries = await readDirEntries(dir);
|
|
338
342
|
|
|
339
|
-
for (const
|
|
340
|
-
if (name.startsWith(".") || name === "node_modules") continue;
|
|
343
|
+
for (const entry of entries) {
|
|
344
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
341
345
|
|
|
342
|
-
const entryPath = join(dir, name);
|
|
346
|
+
const entryPath = join(dir, entry.name);
|
|
343
347
|
|
|
344
348
|
// 1. Direct files: *.ts or *.js
|
|
345
|
-
if (
|
|
349
|
+
if (entry.isFile() && isExtensionModuleFile(entry.name)) {
|
|
346
350
|
discovered.push(entryPath);
|
|
347
351
|
continue;
|
|
348
352
|
}
|
|
349
353
|
|
|
350
354
|
// 2 & 3. Subdirectories
|
|
351
|
-
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
|
+
|
|
352
359
|
// Check for package.json with "omp"/"pi" field first
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
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);
|
|
356
363
|
if (manifest?.extensions && Array.isArray(manifest.extensions)) {
|
|
357
364
|
for (const extPath of manifest.extensions) {
|
|
358
365
|
const resolvedExtPath = resolve(entryPath, extPath);
|
|
359
|
-
|
|
366
|
+
const content = await readFile(resolvedExtPath);
|
|
367
|
+
if (content !== null) {
|
|
360
368
|
discovered.push(resolvedExtPath);
|
|
361
369
|
}
|
|
362
370
|
}
|
|
@@ -365,12 +373,10 @@ export function discoverExtensionModulePaths(ctx: LoadContext, dir: string): str
|
|
|
365
373
|
}
|
|
366
374
|
|
|
367
375
|
// Check for index.ts or index.js
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
if (
|
|
371
|
-
discovered.push(
|
|
372
|
-
} else if (ctx.fs.isFile(indexJs)) {
|
|
373
|
-
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"));
|
|
374
380
|
}
|
|
375
381
|
}
|
|
376
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());
|