@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.
Files changed (64) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/docs/sdk.md +5 -5
  3. package/examples/sdk/10-settings.ts +2 -2
  4. package/package.json +5 -5
  5. package/src/capability/fs.ts +90 -0
  6. package/src/capability/index.ts +41 -227
  7. package/src/capability/types.ts +1 -11
  8. package/src/cli/args.ts +4 -0
  9. package/src/core/agent-session.ts +7 -7
  10. package/src/core/agent-storage.ts +50 -0
  11. package/src/core/auth-storage.ts +102 -3
  12. package/src/core/bash-executor.ts +1 -1
  13. package/src/core/custom-tools/loader.ts +2 -2
  14. package/src/core/export-html/index.ts +1 -33
  15. package/src/core/extensions/loader.ts +2 -2
  16. package/src/core/extensions/types.ts +1 -1
  17. package/src/core/hooks/loader.ts +2 -2
  18. package/src/core/mcp/config.ts +2 -2
  19. package/src/core/model-registry.ts +46 -0
  20. package/src/core/sdk.ts +37 -29
  21. package/src/core/settings-manager.ts +152 -135
  22. package/src/core/skills.ts +72 -51
  23. package/src/core/slash-commands.ts +3 -3
  24. package/src/core/system-prompt.ts +52 -10
  25. package/src/core/tools/complete.ts +5 -2
  26. package/src/core/tools/edit.ts +7 -4
  27. package/src/core/tools/index.test.ts +16 -0
  28. package/src/core/tools/index.ts +21 -8
  29. package/src/core/tools/lsp/index.ts +4 -1
  30. package/src/core/tools/ssh.ts +6 -6
  31. package/src/core/tools/task/commands.ts +3 -9
  32. package/src/core/tools/task/executor.ts +88 -3
  33. package/src/core/tools/task/index.ts +4 -0
  34. package/src/core/tools/task/model-resolver.ts +10 -7
  35. package/src/core/tools/task/worker-protocol.ts +48 -2
  36. package/src/core/tools/task/worker.ts +152 -7
  37. package/src/core/tools/write.ts +7 -4
  38. package/src/discovery/agents-md.ts +13 -19
  39. package/src/discovery/builtin.ts +368 -293
  40. package/src/discovery/claude.ts +183 -345
  41. package/src/discovery/cline.ts +30 -10
  42. package/src/discovery/codex.ts +188 -272
  43. package/src/discovery/cursor.ts +106 -121
  44. package/src/discovery/gemini.ts +72 -97
  45. package/src/discovery/github.ts +7 -10
  46. package/src/discovery/helpers.ts +114 -57
  47. package/src/discovery/index.ts +1 -2
  48. package/src/discovery/mcp-json.ts +15 -18
  49. package/src/discovery/ssh.ts +9 -17
  50. package/src/discovery/vscode.ts +10 -5
  51. package/src/discovery/windsurf.ts +52 -86
  52. package/src/main.ts +5 -1
  53. package/src/modes/interactive/components/extensions/extension-dashboard.ts +24 -11
  54. package/src/modes/interactive/components/extensions/state-manager.ts +19 -15
  55. package/src/modes/interactive/controllers/selector-controller.ts +9 -5
  56. package/src/modes/interactive/interactive-mode.ts +22 -15
  57. package/src/prompts/agents/plan.md +107 -30
  58. package/src/prompts/agents/task.md +5 -4
  59. package/src/prompts/system/system-prompt.md +5 -0
  60. package/src/prompts/tools/task.md +25 -19
  61. package/src/utils/shell.ts +2 -2
  62. package/src/prompts/agents/architect-plan.md +0 -10
  63. package/src/prompts/agents/implement-with-critic.md +0 -11
  64. package/src/prompts/agents/implement.md +0 -11
@@ -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 (walks up from cwd).
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
- const found = ctx.fs.walkUp(paths.projectDir, { dir: true });
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
- ctx: LoadContext,
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
- if (!ctx.fs.isDir(dir)) {
183
- return { items, warnings };
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 files = ctx.fs.readDir(dir);
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(ctx: LoadContext, packageJsonPath: string): ExtensionModuleManifest | null {
256
- const content = ctx.fs.readFile(packageJsonPath);
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 name of ctx.fs.readDir(dir)) {
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 (ctx.fs.isFile(entryPath) && isExtensionModuleFile(name)) {
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 (ctx.fs.isDir(entryPath)) {
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
- const packageJsonPath = join(entryPath, "package.json");
303
- if (ctx.fs.isFile(packageJsonPath)) {
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
- if (ctx.fs.isFile(resolvedExtPath)) {
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
- const indexTs = join(entryPath, "index.ts");
318
- const indexJs = join(entryPath, "index.js");
319
- if (ctx.fs.isFile(indexTs)) {
320
- discovered.push(indexTs);
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
  }
@@ -58,8 +58,7 @@ export {
58
58
  isProviderEnabled,
59
59
  listCapabilities,
60
60
  // Loading API
61
- load,
62
- loadSync,
61
+ loadCapability,
63
62
  // Cache management
64
63
  reset,
65
64
  setDisabledProviders,
@@ -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(ctx: LoadContext, path: string, level: "user" | "project"): LoadResult<MCPServer> {
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
- if (!ctx.fs.isFile(path)) {
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 allItems: MCPServer[] = [];
104
- const allWarnings: string[] = [];
105
-
106
- // Check for mcp.json or .mcp.json in project root (cwd)
107
- for (const filename of ["mcp.json", ".mcp.json"]) {
108
- const path = join(ctx.cwd, filename);
109
- const result = loadMCPJsonFile(ctx, path, "project");
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,
@@ -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(ctx: LoadContext, path: string): LoadResult<SSHHost> {
94
+ async function loadSshJsonFile(_ctx: LoadContext, path: string): Promise<LoadResult<SSHHost>> {
94
95
  const items: SSHHost[] = [];
95
96
  const warnings: string[] = [];
96
97
 
97
- if (!ctx.fs.isFile(path)) {
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, ctx.home, warnings);
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 allItems: SSHHost[] = [];
141
- const allWarnings: string[] = [];
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
- for (const filename of ["ssh.json", ".ssh.json"]) {
144
- const path = join(ctx.cwd, filename);
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,
@@ -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 && ctx.fs.isFile(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(ctx: LoadContext, path: string, level: "user" | "project"): LoadResult<MCPServer> {
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 = ctx.fs.readFile(path);
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 loadMCPServers(ctx: LoadContext): LoadResult<MCPServer> {
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
- if (userPath && ctx.fs.isFile(userPath)) {
43
- const content = ctx.fs.readFile(userPath);
44
- if (content) {
45
- const config = parseJSON<{ mcpServers?: Record<string, unknown> }>(content);
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
- // Project-level: .windsurf/mcp_config.json
70
- const projectPath = getProjectPath(ctx, "windsurf", "mcp_config.json");
71
- if (projectPath && ctx.fs.isFile(projectPath)) {
72
- const content = ctx.fs.readFile(projectPath);
73
- if (content) {
74
- const config = parseJSON<{ mcpServers?: Record<string, unknown> }>(content);
75
- if (config?.mcpServers) {
76
- for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
77
- if (typeof serverConfig !== "object" || serverConfig === null) {
78
- warnings.push(`Invalid server config for "${name}" in ${projectPath}`);
79
- continue;
80
- }
81
-
82
- const server = expandEnvVarsDeep(serverConfig as Record<string, unknown>);
83
- items.push({
84
- name,
85
- command: server.command as string | undefined,
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 && ctx.fs.isFile(userPath)) {
112
- const content = ctx.fs.readFile(userPath);
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());