@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
@@ -118,6 +118,9 @@ export class AgentStorage {
118
118
  private listSettingsStmt: Statement;
119
119
  private insertSettingStmt: Statement;
120
120
  private deleteSettingsStmt: Statement;
121
+ private getCacheStmt: Statement;
122
+ private upsertCacheStmt: Statement;
123
+ private deleteExpiredCacheStmt: Statement;
121
124
  private listAuthStmt: Statement;
122
125
  private listAuthByProviderStmt: Statement;
123
126
  private insertAuthStmt: Statement;
@@ -139,6 +142,12 @@ export class AgentStorage {
139
142
  );
140
143
  this.deleteSettingsStmt = this.db.prepare("DELETE FROM settings");
141
144
 
145
+ this.getCacheStmt = this.db.prepare("SELECT value FROM cache WHERE key = ? AND expires_at > unixepoch()");
146
+ this.upsertCacheStmt = this.db.prepare(
147
+ "INSERT INTO cache (key, value, expires_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at",
148
+ );
149
+ this.deleteExpiredCacheStmt = this.db.prepare("DELETE FROM cache WHERE expires_at <= unixepoch()");
150
+
142
151
  this.listAuthStmt = this.db.prepare(
143
152
  "SELECT id, provider, credential_type, data FROM auth_credentials ORDER BY id ASC",
144
153
  );
@@ -175,6 +184,13 @@ CREATE TABLE IF NOT EXISTS auth_credentials (
175
184
  );
176
185
  CREATE INDEX IF NOT EXISTS idx_auth_provider ON auth_credentials(provider);
177
186
 
187
+ CREATE TABLE IF NOT EXISTS cache (
188
+ key TEXT PRIMARY KEY,
189
+ value TEXT NOT NULL,
190
+ expires_at INTEGER NOT NULL
191
+ );
192
+ CREATE INDEX IF NOT EXISTS idx_cache_expires ON cache(expires_at);
193
+
178
194
  CREATE TABLE IF NOT EXISTS schema_version (version INTEGER PRIMARY KEY);
179
195
  `);
180
196
 
@@ -302,6 +318,40 @@ CREATE TABLE settings (
302
318
  }
303
319
  }
304
320
 
321
+ /**
322
+ * Gets a cached value by key. Returns null if not found or expired.
323
+ */
324
+ getCache(key: string): string | null {
325
+ try {
326
+ const row = this.getCacheStmt.get(key) as { value?: string } | undefined;
327
+ return row?.value ?? null;
328
+ } catch {
329
+ return null;
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Sets a cached value with expiry time (unix seconds).
335
+ */
336
+ setCache(key: string, value: string, expiresAtSec: number): void {
337
+ try {
338
+ this.upsertCacheStmt.run(key, value, expiresAtSec);
339
+ } catch (error) {
340
+ logger.warn("AgentStorage failed to set cache", { key, error: String(error) });
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Deletes expired cache entries. Call periodically for cleanup.
346
+ */
347
+ cleanExpiredCache(): void {
348
+ try {
349
+ this.deleteExpiredCacheStmt.run();
350
+ } catch {
351
+ // Ignore cleanup errors
352
+ }
353
+ }
354
+
305
355
  /**
306
356
  * Checks if any auth credentials exist in storage.
307
357
  * @returns True if at least one credential is stored
@@ -35,6 +35,22 @@ export type AuthCredentialEntry = AuthCredential | AuthCredential[];
35
35
 
36
36
  export type AuthStorageData = Record<string, AuthCredentialEntry>;
37
37
 
38
+ /**
39
+ * Serialized representation of AuthStorage for passing to subagent workers.
40
+ * Contains only the essential credential data, not runtime state.
41
+ */
42
+ export interface SerializedAuthStorage {
43
+ credentials: Record<
44
+ string,
45
+ Array<{
46
+ id: number;
47
+ type: "api_key" | "oauth";
48
+ data: Record<string, unknown>;
49
+ }>
50
+ >;
51
+ runtimeOverrides?: Record<string, string>;
52
+ }
53
+
38
54
  /**
39
55
  * In-memory representation pairing DB row ID with credential.
40
56
  * The ID is required for update/delete operations against agent.db.
@@ -116,6 +132,63 @@ export class AuthStorage {
116
132
  this.storage = AgentStorage.open(this.dbPath);
117
133
  }
118
134
 
135
+ /**
136
+ * Create an in-memory AuthStorage instance from serialized data.
137
+ * Used by subagent workers to bypass discovery and use parent's credentials.
138
+ */
139
+ static fromSerialized(data: SerializedAuthStorage): AuthStorage {
140
+ const instance = Object.create(AuthStorage.prototype) as AuthStorage;
141
+ instance.data = new Map();
142
+ instance.runtimeOverrides = new Map();
143
+ instance.providerRoundRobinIndex = new Map();
144
+ instance.sessionLastCredential = new Map();
145
+ instance.credentialBackoff = new Map();
146
+ instance.codexUsageCache = new Map();
147
+
148
+ for (const [provider, creds] of Object.entries(data.credentials)) {
149
+ instance.data.set(
150
+ provider,
151
+ creds.map((c) => ({
152
+ id: c.id,
153
+ credential:
154
+ c.type === "api_key"
155
+ ? ({ type: "api_key", key: c.data.key as string } satisfies ApiKeyCredential)
156
+ : ({ type: "oauth", ...c.data } as OAuthCredential),
157
+ })),
158
+ );
159
+ }
160
+ if (data.runtimeOverrides) {
161
+ for (const [k, v] of Object.entries(data.runtimeOverrides)) {
162
+ instance.runtimeOverrides.set(k, v);
163
+ }
164
+ }
165
+
166
+ return instance;
167
+ }
168
+
169
+ /**
170
+ * Serialize AuthStorage for passing to subagent workers.
171
+ * Excludes runtime state (round-robin, backoff, usage cache).
172
+ */
173
+ serialize(): SerializedAuthStorage {
174
+ const credentials: SerializedAuthStorage["credentials"] = {};
175
+ for (const [provider, creds] of this.data.entries()) {
176
+ credentials[provider] = creds.map((c) => ({
177
+ id: c.id,
178
+ type: c.credential.type,
179
+ data: c.credential.type === "api_key" ? { key: c.credential.key } : { ...c.credential },
180
+ }));
181
+ }
182
+ const runtimeOverrides: Record<string, string> = {};
183
+ for (const [k, v] of this.runtimeOverrides.entries()) {
184
+ runtimeOverrides[k] = v;
185
+ }
186
+ return {
187
+ credentials,
188
+ runtimeOverrides: Object.keys(runtimeOverrides).length > 0 ? runtimeOverrides : undefined,
189
+ };
190
+ }
191
+
119
192
  /**
120
193
  * Converts legacy auth.json path to agent.db path, or returns .db path as-is.
121
194
  * @param authPath - Path to auth.json or agent.db
@@ -668,15 +741,41 @@ export class AuthStorage {
668
741
  const normalizedBase = this.normalizeCodexBaseUrl(baseUrl);
669
742
  const cacheKey = this.getCodexUsageCacheKey(accountId, normalizedBase);
670
743
  const now = Date.now();
671
- const cached = this.codexUsageCache.get(cacheKey);
672
- if (cached && cached.expiresAt > now) {
673
- return cached.usage;
744
+
745
+ // Check in-memory cache first (fastest)
746
+ const memCached = this.codexUsageCache.get(cacheKey);
747
+ if (memCached && memCached.expiresAt > now) {
748
+ return memCached.usage;
749
+ }
750
+
751
+ // Check DB cache (survives restarts)
752
+ const dbCached = this.storage.getCache(`codex_usage:${cacheKey}`);
753
+ if (dbCached) {
754
+ try {
755
+ const parsed = JSON.parse(dbCached) as CodexUsage;
756
+ // Store in memory for faster subsequent access
757
+ this.codexUsageCache.set(cacheKey, {
758
+ fetchedAt: now,
759
+ expiresAt: now + AuthStorage.codexUsageCacheTtlMs,
760
+ usage: parsed,
761
+ });
762
+ return parsed;
763
+ } catch {
764
+ // Invalid cache, continue to fetch
765
+ }
674
766
  }
675
767
 
768
+ // Fetch from API
676
769
  const usage = await this.fetchCodexUsage(credential, normalizedBase);
677
770
  if (usage) {
678
771
  const expiresAt = this.getCodexUsageExpiryMs(usage, now);
679
772
  this.codexUsageCache.set(cacheKey, { fetchedAt: now, expiresAt, usage });
773
+ // Store in DB with 60s TTL
774
+ this.storage.setCache(
775
+ `codex_usage:${cacheKey}`,
776
+ JSON.stringify(usage),
777
+ Math.floor((now + AuthStorage.codexUsageCacheTtlMs) / 1000),
778
+ );
680
779
  return usage;
681
780
  }
682
781
 
@@ -142,7 +142,7 @@ function createOutputSink(
142
142
  * @returns Promise resolving to execution result
143
143
  */
144
144
  export async function executeBash(command: string, options?: BashExecutorOptions): Promise<BashResult> {
145
- const { shell, args, env, prefix } = getShellConfig();
145
+ const { shell, args, env, prefix } = await getShellConfig();
146
146
 
147
147
  // Get or create shell snapshot (for aliases, functions, options)
148
148
  const snapshotPath = await getOrCreateSnapshot(shell, env);
@@ -9,7 +9,7 @@ import * as os from "node:os";
9
9
  import * as path from "node:path";
10
10
  import * as typebox from "@sinclair/typebox";
11
11
  import { toolCapability } from "../../capability/tool";
12
- import { type CustomTool, loadSync } from "../../discovery";
12
+ import { type CustomTool, loadCapability } from "../../discovery";
13
13
  import * as piCodingAgent from "../../index";
14
14
  import { theme } from "../../modes/interactive/theme/theme";
15
15
  import type { ExecOptions } from "../exec";
@@ -225,7 +225,7 @@ export async function discoverAndLoadCustomTools(
225
225
  };
226
226
 
227
227
  // 1. Discover tools via capability system (user + project from all providers)
228
- const discoveredTools = loadSync<CustomTool>(toolCapability.id, { cwd });
228
+ const discoveredTools = await loadCapability<CustomTool>(toolCapability.id, { cwd });
229
229
  for (const tool of discoveredTools.items) {
230
230
  addPath(tool.path, {
231
231
  provider: tool._source.provider,
@@ -8,7 +8,7 @@ import * as path from "node:path";
8
8
  import type { KeyId } from "@oh-my-pi/pi-tui";
9
9
  import * as TypeBox from "@sinclair/typebox";
10
10
  import { type ExtensionModule, extensionModuleCapability } from "../../capability/extension-module";
11
- import { loadSync } from "../../discovery";
11
+ import { loadCapability } from "../../discovery";
12
12
  import { getExtensionNameFromPath } from "../../discovery/helpers";
13
13
  import * as piCodingAgent from "../../index";
14
14
  import { createEventBus, type EventBus } from "../event-bus";
@@ -408,7 +408,7 @@ export async function discoverAndLoadExtensions(
408
408
  };
409
409
 
410
410
  // 1. Discover extension modules via capability API (native .omp/.pi only)
411
- const discovered = loadSync<ExtensionModule>(extensionModuleCapability.id, { cwd });
411
+ const discovered = await loadCapability<ExtensionModule>(extensionModuleCapability.id, { cwd });
412
412
  for (const ext of discovered.items) {
413
413
  if (ext._source.provider !== "native") continue;
414
414
  if (isDisabledName(ext.name)) continue;
@@ -756,7 +756,7 @@ export type GetActiveToolsHandler = () => string[];
756
756
 
757
757
  export type GetAllToolsHandler = () => string[];
758
758
 
759
- export type SetActiveToolsHandler = (toolNames: string[]) => void;
759
+ export type SetActiveToolsHandler = (toolNames: string[]) => Promise<void>;
760
760
 
761
761
  export type SetModelHandler = (model: Model<any>) => Promise<boolean>;
762
762
 
@@ -7,7 +7,7 @@ import * as path from "node:path";
7
7
  import * as typebox from "@sinclair/typebox";
8
8
  import { hookCapability } from "../../capability/hook";
9
9
  import type { Hook } from "../../discovery";
10
- import { loadSync } from "../../discovery";
10
+ import { loadCapability } from "../../discovery";
11
11
  import * as piCodingAgent from "../../index";
12
12
  import { logger } from "../logger";
13
13
  import type { HookMessage } from "../messages";
@@ -278,7 +278,7 @@ export async function discoverAndLoadHooks(configuredPaths: string[], cwd: strin
278
278
  };
279
279
 
280
280
  // 1. Discover hooks via capability API
281
- const discovered = loadSync<Hook>(hookCapability.id, { cwd });
281
+ const discovered = await loadCapability<Hook>(hookCapability.id, { cwd });
282
282
  addPaths(discovered.items.map((hook) => hook.path));
283
283
 
284
284
  // 2. Explicitly configured paths (can override/add)
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { mcpCapability } from "../../capability/mcp";
8
8
  import type { MCPServer } from "../../discovery";
9
- import { load } from "../../discovery";
9
+ import { loadCapability } from "../../discovery";
10
10
  import type { MCPServerConfig } from "./types";
11
11
 
12
12
  /** Options for loading MCP configs */
@@ -81,7 +81,7 @@ export async function loadAllMCPConfigs(cwd: string, options?: LoadMCPConfigsOpt
81
81
  const filterExa = options?.filterExa ?? true;
82
82
 
83
83
  // Load MCP servers via capability system
84
- const result = await load<MCPServer>(mcpCapability.id, { cwd });
84
+ const result = await loadCapability<MCPServer>(mcpCapability.id, { cwd });
85
85
 
86
86
  // Filter out project-level configs if disabled
87
87
  const servers = enableProjectConfig
@@ -86,6 +86,15 @@ interface ProviderOverride {
86
86
  apiKey?: string;
87
87
  }
88
88
 
89
+ /**
90
+ * Serialized representation of ModelRegistry for passing to subagent workers.
91
+ */
92
+ export interface SerializedModelRegistry {
93
+ models: Model<Api>[];
94
+ customProviderApiKeys?: Record<string, string>;
95
+ loadError?: string;
96
+ }
97
+
89
98
  /** Result of loading custom models from models.json */
90
99
  interface CustomModelsResult {
91
100
  models: Model<Api>[];
@@ -140,6 +149,43 @@ export class ModelRegistry {
140
149
  this.loadModels();
141
150
  }
142
151
 
152
+ /**
153
+ * Create an in-memory ModelRegistry instance from serialized data.
154
+ * Used by subagent workers to bypass discovery and use parent's models.
155
+ */
156
+ static fromSerialized(data: SerializedModelRegistry, authStorage: AuthStorage): ModelRegistry {
157
+ const instance = Object.create(ModelRegistry.prototype) as ModelRegistry;
158
+ (instance as any).authStorage = authStorage;
159
+ instance.models = data.models;
160
+ instance.customProviderApiKeys = new Map(Object.entries(data.customProviderApiKeys ?? {}));
161
+ instance.loadError = data.loadError;
162
+
163
+ authStorage.setFallbackResolver((provider) => {
164
+ const keyConfig = instance.customProviderApiKeys.get(provider);
165
+ if (keyConfig) {
166
+ return resolveApiKeyConfig(keyConfig);
167
+ }
168
+ return undefined;
169
+ });
170
+
171
+ return instance;
172
+ }
173
+
174
+ /**
175
+ * Serialize ModelRegistry for passing to subagent workers.
176
+ */
177
+ serialize(): SerializedModelRegistry {
178
+ const customProviderApiKeys: Record<string, string> = {};
179
+ for (const [k, v] of this.customProviderApiKeys.entries()) {
180
+ customProviderApiKeys[k] = v;
181
+ }
182
+ return {
183
+ models: this.models,
184
+ customProviderApiKeys: Object.keys(customProviderApiKeys).length > 0 ? customProviderApiKeys : undefined,
185
+ loadError: this.loadError,
186
+ };
187
+ }
188
+
143
189
  /**
144
190
  * Reload models from disk (built-in + custom from models.json).
145
191
  */
package/src/core/sdk.ts CHANGED
@@ -33,7 +33,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
33
33
  import chalk from "chalk";
34
34
  // Import discovery to register all providers on startup
35
35
  import "../discovery";
36
- import { loadSync as loadCapability } from "../capability/index";
36
+ import { loadCapability } from "../capability/index";
37
37
  import { type Rule, ruleCapability } from "../capability/rule";
38
38
  import { getAgentDir, getConfigDirPaths } from "../config";
39
39
  import { initializeWithSettings } from "../discovery";
@@ -153,6 +153,9 @@ export interface CreateAgentSessionOptions {
153
153
  /** Enable MCP server discovery from .mcp.json files. Default: true */
154
154
  enableMCP?: boolean;
155
155
 
156
+ /** Enable LSP integration (tool, formatting, diagnostics, warmup). Default: true */
157
+ enableLsp?: boolean;
158
+
156
159
  /** Tool names explicitly requested (enables disabled-by-default tools) */
157
160
  toolNames?: string[];
158
161
 
@@ -286,12 +289,12 @@ export async function discoverExtensions(cwd?: string): Promise<LoadExtensionsRe
286
289
  /**
287
290
  * Discover skills from cwd and agentDir.
288
291
  */
289
- export function discoverSkills(
292
+ export async function discoverSkills(
290
293
  cwd?: string,
291
294
  _agentDir?: string,
292
295
  settings?: SkillsSettings,
293
- ): { skills: Skill[]; warnings: SkillWarning[] } {
294
- return loadSkillsInternal({
296
+ ): Promise<{ skills: Skill[]; warnings: SkillWarning[] }> {
297
+ return await loadSkillsInternal({
295
298
  ...settings,
296
299
  cwd: cwd ?? process.cwd(),
297
300
  });
@@ -301,11 +304,11 @@ export function discoverSkills(
301
304
  * Discover context files (AGENTS.md) walking up from cwd.
302
305
  * Returns files sorted by depth (farther from cwd first, so closer files appear last/more prominent).
303
306
  */
304
- export function discoverContextFiles(
307
+ export async function discoverContextFiles(
305
308
  cwd?: string,
306
309
  _agentDir?: string,
307
- ): Array<{ path: string; content: string; depth?: number }> {
308
- return loadContextFilesInternal({
310
+ ): Promise<Array<{ path: string; content: string; depth?: number }>> {
311
+ return await loadContextFilesInternal({
309
312
  cwd: cwd ?? process.cwd(),
310
313
  });
311
314
  }
@@ -323,7 +326,7 @@ export async function discoverPromptTemplates(cwd?: string, agentDir?: string):
323
326
  /**
324
327
  * Discover file-based slash commands from commands/ directories.
325
328
  */
326
- export function discoverSlashCommands(cwd?: string): FileSlashCommand[] {
329
+ export async function discoverSlashCommands(cwd?: string): Promise<FileSlashCommand[]> {
327
330
  return loadSlashCommandsInternal({ cwd: cwd ?? process.cwd() });
328
331
  }
329
332
 
@@ -364,8 +367,8 @@ export interface BuildSystemPromptOptions {
364
367
  /**
365
368
  * Build the default system prompt.
366
369
  */
367
- export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): string {
368
- return buildSystemPromptInternal({
370
+ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): Promise<string> {
371
+ return await buildSystemPromptInternal({
369
372
  cwd: options.cwd,
370
373
  skills: options.skills,
371
374
  contextFiles: options.contextFiles,
@@ -378,8 +381,8 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
378
381
  /**
379
382
  * Load settings from agentDir/settings.json merged with cwd/.omp/settings.json.
380
383
  */
381
- export function loadSettings(cwd?: string, agentDir?: string): Settings {
382
- const manager = SettingsManager.create(cwd ?? process.cwd(), agentDir ?? getDefaultAgentDir());
384
+ export async function loadSettings(cwd?: string, agentDir?: string): Promise<Settings> {
385
+ const manager = await SettingsManager.create(cwd ?? process.cwd(), agentDir ?? getDefaultAgentDir());
383
386
  return {
384
387
  modelRoles: manager.getModelRoles(),
385
388
  defaultThinkingLevel: manager.getDefaultThinkingLevel(),
@@ -543,7 +546,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
543
546
  const modelRegistry = options.modelRegistry ?? (await discoverModels(authStorage, agentDir));
544
547
  time("discoverModels");
545
548
 
546
- const settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);
549
+ const settingsManager = options.settingsManager ?? (await SettingsManager.create(cwd, agentDir));
547
550
  time("settingsManager");
548
551
  initializeWithSettings(settingsManager);
549
552
  time("initializeWithSettings");
@@ -596,12 +599,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
596
599
 
597
600
  // Fall back to first available model with a valid API key
598
601
  if (!model) {
599
- for (const m of modelRegistry.getAll()) {
600
- if (await modelRegistry.getApiKey(m, sessionId)) {
601
- model = m;
602
- break;
603
- }
604
- }
602
+ const allModels = modelRegistry.getAll();
603
+ const keyResults = await Promise.all(
604
+ allModels.map(async (m) => ({ model: m, hasKey: !!(await modelRegistry.getApiKey(m, sessionId)) })),
605
+ );
606
+ model = keyResults.find((r) => r.hasKey)?.model;
605
607
  time("findAvailableModel");
606
608
  if (model) {
607
609
  if (modelFallbackMessage) {
@@ -636,7 +638,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
636
638
  skills = options.skills;
637
639
  skillWarnings = [];
638
640
  } else {
639
- const discovered = discoverSkills(cwd, agentDir, settingsManager.getSkillsSettings());
641
+ const discovered = await discoverSkills(cwd, agentDir, settingsManager.getSkillsSettings());
640
642
  skills = discovered.skills;
641
643
  skillWarnings = discovered.warnings;
642
644
  }
@@ -644,7 +646,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
644
646
 
645
647
  // Discover rules
646
648
  const ttsrManager = createTtsrManager(settingsManager.getTtsrSettings());
647
- const rulesResult = loadCapability<Rule>(ruleCapability.id, { cwd });
649
+ const rulesResult = await loadCapability<Rule>(ruleCapability.id, { cwd });
648
650
  for (const rule of rulesResult.items) {
649
651
  if (rule.ttsrTrigger) {
650
652
  ttsrManager.addRule(rule);
@@ -653,7 +655,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
653
655
  time("discoverTtsrRules");
654
656
 
655
657
  // Filter rules for the rulebook (non-TTSR, non-alwaysApply, with descriptions)
656
- const rulebookRules = rulesResult.items.filter((rule) => {
658
+ const rulebookRules = rulesResult.items.filter((rule: Rule) => {
657
659
  if (rule.ttsrTrigger) return false;
658
660
  if (rule.alwaysApply) return false;
659
661
  if (!rule.description) return false;
@@ -661,15 +663,18 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
661
663
  });
662
664
  time("filterRulebookRules");
663
665
 
664
- const contextFiles = options.contextFiles ?? discoverContextFiles(cwd, agentDir);
666
+ const contextFiles = options.contextFiles ?? (await discoverContextFiles(cwd, agentDir));
665
667
  time("discoverContextFiles");
666
668
 
667
669
  let agent: Agent;
668
670
  let session: AgentSession;
669
671
 
672
+ const enableLsp = options.enableLsp ?? true;
673
+
670
674
  const toolSession: ToolSession = {
671
675
  cwd,
672
676
  hasUI: options.hasUI ?? false,
677
+ enableLsp,
673
678
  eventBus,
674
679
  outputSchema: options.outputSchema,
675
680
  requireCompleteTool: options.requireCompleteTool,
@@ -681,6 +686,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
681
686
  return activeModel ? formatModelString(activeModel) : undefined;
682
687
  },
683
688
  settings: settingsManager,
689
+ authStorage,
690
+ modelRegistry,
684
691
  };
685
692
 
686
693
  const builtinTools = await createTools(toolSession, options.toolNames);
@@ -706,6 +713,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
706
713
  });
707
714
  time("discoverAndLoadMCPTools");
708
715
  mcpManager = mcpResult.manager;
716
+ toolSession.mcpManager = mcpManager;
709
717
 
710
718
  // If we extracted Exa API keys from MCP configs and EXA_API_KEY isn't set, use the first one
711
719
  if (mcpResult.exaApiKeys.length > 0 && !process.env.EXA_API_KEY) {
@@ -846,9 +854,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
846
854
  }
847
855
  time("combineTools");
848
856
 
849
- const rebuildSystemPrompt = (toolNames: string[], tools: Map<string, AgentTool>): string => {
857
+ const rebuildSystemPrompt = async (toolNames: string[], tools: Map<string, AgentTool>): Promise<string> => {
850
858
  toolContextStore.setToolNames(toolNames);
851
- const defaultPrompt = buildSystemPromptInternal({
859
+ const defaultPrompt = await buildSystemPromptInternal({
852
860
  cwd,
853
861
  skills,
854
862
  contextFiles,
@@ -862,7 +870,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
862
870
  return defaultPrompt;
863
871
  }
864
872
  if (typeof options.systemPrompt === "string") {
865
- return buildSystemPromptInternal({
873
+ return await buildSystemPromptInternal({
866
874
  cwd,
867
875
  skills,
868
876
  contextFiles,
@@ -876,13 +884,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
876
884
  return options.systemPrompt(defaultPrompt);
877
885
  };
878
886
 
879
- const systemPrompt = rebuildSystemPrompt(Array.from(toolRegistry.keys()), toolRegistry);
887
+ const systemPrompt = await rebuildSystemPrompt(Array.from(toolRegistry.keys()), toolRegistry);
880
888
  time("buildSystemPrompt");
881
889
 
882
890
  const promptTemplates = options.promptTemplates ?? (await discoverPromptTemplates(cwd, agentDir));
883
891
  time("discoverPromptTemplates");
884
892
 
885
- const slashCommands = options.slashCommands ?? discoverSlashCommands(cwd);
893
+ const slashCommands = options.slashCommands ?? (await discoverSlashCommands(cwd));
886
894
  time("discoverSlashCommands");
887
895
 
888
896
  // Create convertToLlm wrapper that filters images if blockImages is enabled (defense-in-depth)
@@ -991,7 +999,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
991
999
 
992
1000
  // Warm up LSP servers (connects to detected servers)
993
1001
  let lspServers: CreateAgentSessionResult["lspServers"];
994
- if (settingsManager.getLspDiagnosticsOnWrite()) {
1002
+ if (enableLsp && settingsManager.getLspDiagnosticsOnWrite()) {
995
1003
  try {
996
1004
  const result = await warmupLspServers(cwd, {
997
1005
  onConnecting: (serverNames) => {