@oh-my-pi/pi-coding-agent 4.2.1 → 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 (57) hide show
  1. package/CHANGELOG.md +20 -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 +4 -4
  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/extensions/loader.ts +2 -2
  15. package/src/core/extensions/types.ts +1 -1
  16. package/src/core/hooks/loader.ts +2 -2
  17. package/src/core/mcp/config.ts +2 -2
  18. package/src/core/model-registry.ts +46 -0
  19. package/src/core/sdk.ts +37 -29
  20. package/src/core/settings-manager.ts +152 -135
  21. package/src/core/skills.ts +72 -51
  22. package/src/core/slash-commands.ts +3 -3
  23. package/src/core/system-prompt.ts +10 -10
  24. package/src/core/tools/edit.ts +7 -4
  25. package/src/core/tools/index.test.ts +16 -0
  26. package/src/core/tools/index.ts +21 -8
  27. package/src/core/tools/lsp/index.ts +4 -1
  28. package/src/core/tools/ssh.ts +6 -6
  29. package/src/core/tools/task/commands.ts +3 -5
  30. package/src/core/tools/task/executor.ts +88 -3
  31. package/src/core/tools/task/index.ts +4 -0
  32. package/src/core/tools/task/model-resolver.ts +10 -7
  33. package/src/core/tools/task/worker-protocol.ts +48 -2
  34. package/src/core/tools/task/worker.ts +152 -7
  35. package/src/core/tools/write.ts +7 -4
  36. package/src/discovery/agents-md.ts +13 -19
  37. package/src/discovery/builtin.ts +367 -247
  38. package/src/discovery/claude.ts +181 -290
  39. package/src/discovery/cline.ts +30 -10
  40. package/src/discovery/codex.ts +185 -244
  41. package/src/discovery/cursor.ts +106 -121
  42. package/src/discovery/gemini.ts +72 -97
  43. package/src/discovery/github.ts +7 -10
  44. package/src/discovery/helpers.ts +94 -88
  45. package/src/discovery/index.ts +1 -2
  46. package/src/discovery/mcp-json.ts +15 -18
  47. package/src/discovery/ssh.ts +9 -17
  48. package/src/discovery/vscode.ts +10 -5
  49. package/src/discovery/windsurf.ts +52 -86
  50. package/src/main.ts +5 -1
  51. package/src/modes/interactive/components/extensions/extension-dashboard.ts +24 -11
  52. package/src/modes/interactive/components/extensions/state-manager.ts +19 -15
  53. package/src/modes/interactive/controllers/selector-controller.ts +6 -2
  54. package/src/modes/interactive/interactive-mode.ts +19 -15
  55. package/src/prompts/agents/plan.md +107 -30
  56. package/src/utils/shell.ts +2 -2
  57. package/src/prompts/agents/planner.md +0 -112
@@ -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 (walks up from cwd).
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
- const found = ctx.fs.walkUp(paths.projectDir, { dir: true });
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
- ctx: LoadContext,
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
- if (!ctx.fs.isDir(dir)) {
144
- return { items, warnings };
145
- }
146
-
147
- for (const name of ctx.fs.readDir(dir)) {
148
- if (name.startsWith(".") || name === "node_modules") continue;
149
-
150
- const skillDir = join(dir, name);
151
- if (!ctx.fs.isDir(skillDir)) continue;
152
-
153
- const skillFile = join(skillDir, "SKILL.md");
154
- if (!ctx.fs.isFile(skillFile)) continue;
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
- const { frontmatter, body } = parseFrontmatter(content);
163
- if (requireDescription && !frontmatter.description) {
164
- continue;
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
- items.push({
168
- name: (frontmatter.name as string) || name,
169
- path: skillFile,
170
- content: body,
171
- frontmatter: frontmatter as SkillFrontmatter,
172
- level,
173
- _source: createSourceMeta(providerId, skillFile, level),
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
- ctx: LoadContext,
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 items: T[] = [];
231
- const warnings: string[] = [];
230
+ ): Promise<LoadResult<T>> {
231
+ const entries = await readDirEntries(dir);
232
232
 
233
- if (!ctx.fs.isDir(dir)) {
234
- return { items, warnings };
235
- }
233
+ const visibleEntries = entries.filter((entry) => !entry.name.startsWith("."));
236
234
 
237
- const files = ctx.fs.readDir(dir);
235
+ const directories = options.recursive ? visibleEntries.filter((entry) => entry.isDirectory()) : [];
238
236
 
239
- for (const name of files) {
240
- if (name.startsWith(".")) continue;
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
- const path = join(dir, name);
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
- if (options.recursive && ctx.fs.isDir(path)) {
245
- const subResult = loadFilesFromDir(ctx, path, provider, level, options);
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
- // Check extension
254
- if (options.extensions) {
255
- const hasMatch = options.extensions.some((ext) => name.endsWith(`.${ext}`));
256
- if (!hasMatch) continue;
257
- }
258
+ for (const subResult of subResults) {
259
+ items.push(...subResult.items);
260
+ if (subResult.warnings) warnings.push(...subResult.warnings);
261
+ }
258
262
 
259
- const content = ctx.fs.readFile(path);
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(ctx: LoadContext, packageJsonPath: string): ExtensionModuleManifest | null {
307
- 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);
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 name of ctx.fs.readDir(dir)) {
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 (ctx.fs.isFile(entryPath) && isExtensionModuleFile(name)) {
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 (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
+
352
359
  // Check for package.json with "omp"/"pi" field first
353
- const packageJsonPath = join(entryPath, "package.json");
354
- if (ctx.fs.isFile(packageJsonPath)) {
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
- if (ctx.fs.isFile(resolvedExtPath)) {
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
- const indexTs = join(entryPath, "index.ts");
369
- const indexJs = join(entryPath, "index.js");
370
- if (ctx.fs.isFile(indexTs)) {
371
- discovered.push(indexTs);
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
  }
@@ -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());