orquesta-cli 0.2.107 → 0.2.111

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 (38) hide show
  1. package/dist/cli.js +27 -4
  2. package/dist/core/commands/help.js +4 -0
  3. package/dist/core/commands/index.js +7 -0
  4. package/dist/core/commands/lsp.d.ts +3 -0
  5. package/dist/core/commands/lsp.js +37 -0
  6. package/dist/core/commands/mcp.d.ts +3 -0
  7. package/dist/core/commands/mcp.js +46 -0
  8. package/dist/core/commands/undo.d.ts +4 -0
  9. package/dist/core/commands/undo.js +45 -0
  10. package/dist/core/config/auto-detect.js +4 -4
  11. package/dist/core/config/config-manager.d.ts +7 -1
  12. package/dist/core/config/config-manager.js +36 -0
  13. package/dist/core/config/providers.d.ts +3 -0
  14. package/dist/core/config/providers.js +87 -3
  15. package/dist/core/file-snapshot-store.d.ts +25 -0
  16. package/dist/core/file-snapshot-store.js +104 -0
  17. package/dist/core/lsp/index.d.ts +6 -0
  18. package/dist/core/lsp/index.js +75 -0
  19. package/dist/core/lsp/jsonrpc.d.ts +18 -0
  20. package/dist/core/lsp/jsonrpc.js +38 -0
  21. package/dist/core/lsp/lsp-client.d.ts +40 -0
  22. package/dist/core/lsp/lsp-client.js +201 -0
  23. package/dist/core/lsp/server-registry.d.ts +14 -0
  24. package/dist/core/lsp/server-registry.js +85 -0
  25. package/dist/eval/eval-runner.js +14 -0
  26. package/dist/orchestration/plan-executor.js +2 -0
  27. package/dist/tools/llm/simple/file-tools.js +8 -2
  28. package/dist/tools/mcp/index.d.ts +3 -0
  29. package/dist/tools/mcp/index.js +3 -0
  30. package/dist/tools/mcp/mcp-client.d.ts +16 -0
  31. package/dist/tools/mcp/mcp-client.js +180 -0
  32. package/dist/tools/mcp/mcp-config.d.ts +4 -0
  33. package/dist/tools/mcp/mcp-config.js +87 -0
  34. package/dist/types/index.d.ts +21 -0
  35. package/dist/ui/hooks/slashCommandProcessor.js +17 -0
  36. package/package.json +2 -1
  37. package/dist/core/git-auto-updater.d.ts +0 -58
  38. package/dist/core/git-auto-updater.js +0 -374
package/dist/cli.js CHANGED
@@ -28,7 +28,7 @@ import { sessionManager } from './core/session/session-manager.js';
28
28
  import { connectWithToken, showConnectionStatus, disconnectFromOrquesta, switchProject } from './setup/first-run-setup.js';
29
29
  import { syncOrquestaConfigs } from './orquesta/config-sync.js';
30
30
  import { scanProviders, scanProvider, toEndpointConfig } from './core/config/auto-detect.js';
31
- import { PROVIDERS } from './core/config/providers.js';
31
+ import { getProviders, refreshCatalogFromServer } from './core/config/providers.js';
32
32
  import { shouldShowOnboarding, runOnboarding } from './core/onboarding.js';
33
33
  const require = createRequire(import.meta.url);
34
34
  const packageJson = require('../package.json');
@@ -224,7 +224,7 @@ program
224
224
  console.log(chalk.yellow('\nNo LLM providers detected.'));
225
225
  console.log(chalk.dim('Set environment variables (e.g., OPENAI_API_KEY) or start a local provider (Ollama, LM Studio).'));
226
226
  console.log(chalk.dim('\nSupported providers:'));
227
- for (const p of PROVIDERS) {
227
+ for (const p of getProviders()) {
228
228
  const envHint = p.envVars.length > 0 ? chalk.dim(` (${p.envVars[0]})`) : p.isLocal ? chalk.dim(` (port ${p.localPort})`) : '';
229
229
  console.log(chalk.white(` ${p.name}${envHint}`));
230
230
  }
@@ -265,7 +265,7 @@ program
265
265
  const detected = await scanProvider(options.addProvider);
266
266
  if (!detected) {
267
267
  spinner.fail(chalk.red(`Provider '${options.addProvider}' not found or not available`));
268
- const provider = PROVIDERS.find(p => p.id === options.addProvider);
268
+ const provider = getProviders().find(p => p.id === options.addProvider);
269
269
  if (provider && provider.envVars.length > 0) {
270
270
  console.log(chalk.dim(`Set ${provider.envVars[0]} environment variable and try again.`));
271
271
  }
@@ -349,6 +349,16 @@ program
349
349
  const toolsPromise = initializeOptionalTools().then(() => {
350
350
  logger.flow('Optional tools initialized');
351
351
  });
352
+ const mcpPromise = import('./tools/mcp/index.js')
353
+ .then(({ initializeMcpServers }) => initializeMcpServers())
354
+ .then((r) => {
355
+ if (r.servers > 0 || r.errors.length > 0) {
356
+ logger.flow('MCP initialized', { servers: r.servers, tools: r.tools, errors: r.errors.length });
357
+ }
358
+ })
359
+ .catch((error) => {
360
+ logger.warn('MCP init failed', { error: error instanceof Error ? error.message : String(error) });
361
+ });
352
362
  const syncPromise = (configManager.hasOrquestaConnection() && configManager.shouldAutoSync())
353
363
  ? syncOrquestaConfigs().then(syncResult => {
354
364
  if (syncResult.success && (syncResult.added > 0 || syncResult.updated > 0)) {
@@ -375,7 +385,7 @@ program
375
385
  logger.flow('No LLM endpoints configured');
376
386
  }
377
387
  await Promise.race([
378
- Promise.all([toolsPromise, syncPromise]),
388
+ Promise.all([toolsPromise, syncPromise, mcpPromise]),
379
389
  new Promise(resolve => setTimeout(resolve, Number(process.env['ORQUESTA_INIT_TIMEOUT_MS']) || 12000).unref?.()),
380
390
  ]);
381
391
  spinner.stop();
@@ -414,6 +424,18 @@ program
414
424
  sessionId: sessionManager.getCurrentSessionId(),
415
425
  exitReason: 'normal',
416
426
  });
427
+ try {
428
+ const { disconnectMcpServers } = await import('./tools/mcp/index.js');
429
+ await disconnectMcpServers();
430
+ }
431
+ catch {
432
+ }
433
+ try {
434
+ const { shutdownAllLspClients } = await import('./core/lsp/index.js');
435
+ await shutdownAllLspClients();
436
+ }
437
+ catch {
438
+ }
417
439
  if (cleanup) {
418
440
  await cleanup();
419
441
  }
@@ -495,5 +517,6 @@ program.on('command:*', () => {
495
517
  console.log(chalk.white('\nUse /help in interactive mode for more help.\n'));
496
518
  process.exit(1);
497
519
  });
520
+ void refreshCatalogFromServer();
498
521
  program.parse(process.argv);
499
522
  //# sourceMappingURL=cli.js.map
@@ -6,12 +6,16 @@ export const helpCommand = {
6
6
  Available commands:
7
7
  /exit, /quit - Exit the application
8
8
  /clear - Clear conversation and TODOs
9
+ /undo - Revert the file changes from the last turn
10
+ /redo - Re-apply the changes undone by /undo
9
11
  /compact - Compact conversation to free up context
10
12
  /memory - Persistent memory: /memory add <note> | list | remove <n> | clear
11
13
  /settings - Open settings menu
12
14
  /model - Switch between LLM models
13
15
  /project - Switch between Orquesta projects
14
16
  /tool - Enable/disable optional tools (Browser, Background)
17
+ /mcp - List connected MCP servers and their tools
18
+ /lsp - LSP diagnostics after edits: status | on | off
15
19
  /load - Load a saved session
16
20
  /usage - Show token usage statistics
17
21
  /cost - Estimated USD spend this process (by model)
@@ -4,10 +4,17 @@ import { compactCommand } from './compact.js';
4
4
  import { memoryCommand } from './memory.js';
5
5
  import { recallCommand } from './recall.js';
6
6
  import { helpCommand } from './help.js';
7
+ import { mcpCommand } from './mcp.js';
8
+ import { undoCommand, redoCommand } from './undo.js';
9
+ import { lspCommand } from './lsp.js';
7
10
  commandRegistry.register(clearCommand);
8
11
  commandRegistry.register(compactCommand);
9
12
  commandRegistry.register(memoryCommand);
10
13
  commandRegistry.register(recallCommand);
11
14
  commandRegistry.register(helpCommand);
15
+ commandRegistry.register(mcpCommand);
16
+ commandRegistry.register(undoCommand);
17
+ commandRegistry.register(redoCommand);
18
+ commandRegistry.register(lspCommand);
12
19
  export { commandRegistry } from './registry.js';
13
20
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,3 @@
1
+ import { SlashCommand } from './types.js';
2
+ export declare const lspCommand: SlashCommand;
3
+ //# sourceMappingURL=lsp.d.ts.map
@@ -0,0 +1,37 @@
1
+ import { configManager } from '../config/config-manager.js';
2
+ import { getServerDefs, resolveServerBinary, getActiveLspClientCount } from '../lsp/index.js';
3
+ export const lspCommand = {
4
+ name: '/lsp',
5
+ description: 'LSP diagnostics: /lsp | on | off',
6
+ async execute(context, rawInput) {
7
+ const reply = (content) => {
8
+ const updatedMessages = [...context.messages, { role: 'assistant', content }];
9
+ context.setMessages(updatedMessages);
10
+ return { handled: true, shouldContinue: false, updatedContext: { messages: updatedMessages } };
11
+ };
12
+ const arg = rawInput.slice('/lsp'.length).trim().toLowerCase();
13
+ if (arg === 'on' || arg === 'enable') {
14
+ await configManager.setLspEnabled(true);
15
+ return reply('✓ LSP diagnostics enabled.');
16
+ }
17
+ if (arg === 'off' || arg === 'disable') {
18
+ await configManager.setLspEnabled(false);
19
+ return reply('✓ LSP diagnostics disabled.');
20
+ }
21
+ const enabled = configManager.isLspEnabled();
22
+ const lines = [
23
+ `🩺 LSP diagnostics: ${enabled ? 'ON' : 'OFF'} (${getActiveLspClientCount()} server(s) running)`,
24
+ '',
25
+ 'Language servers (✓ = found on PATH):',
26
+ ];
27
+ for (const def of getServerDefs()) {
28
+ const bin = resolveServerBinary(def.command);
29
+ const mark = bin ? '✓' : '·';
30
+ const where = bin ? `→ ${bin}` : 'not installed';
31
+ lines.push(` ${mark} ${def.id} (${def.extensions.join(', ')}) ${def.command} ${where}`);
32
+ }
33
+ lines.push('', 'Toggle with /lsp on | /lsp off');
34
+ return reply(lines.join('\n'));
35
+ },
36
+ };
37
+ //# sourceMappingURL=lsp.js.map
@@ -0,0 +1,3 @@
1
+ import { SlashCommand } from './types.js';
2
+ export declare const mcpCommand: SlashCommand;
3
+ //# sourceMappingURL=mcp.d.ts.map
@@ -0,0 +1,46 @@
1
+ import { getConnectedMcpServers } from '../../tools/mcp/index.js';
2
+ import { loadMcpServerConfigs } from '../../tools/mcp/mcp-config.js';
3
+ export const mcpCommand = {
4
+ name: '/mcp',
5
+ description: 'List connected MCP servers and their tools',
6
+ async execute(context) {
7
+ const reply = (content) => {
8
+ const updatedMessages = [...context.messages, { role: 'assistant', content }];
9
+ context.setMessages(updatedMessages);
10
+ return { handled: true, shouldContinue: false, updatedContext: { messages: updatedMessages } };
11
+ };
12
+ const live = getConnectedMcpServers();
13
+ const configured = loadMcpServerConfigs();
14
+ if (configured.length === 0 && live.length === 0) {
15
+ return reply('No MCP servers configured.\n\n' +
16
+ 'Add one to ~/.orquesta-cli/config.json:\n' +
17
+ ' "mcpServers": [\n' +
18
+ ' { "name": "filesystem", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "."] }\n' +
19
+ ' ]\n\n' +
20
+ 'Or drop a Claude-compatible .mcp.json in the project root.');
21
+ }
22
+ const liveByName = new Map(live.map((s) => [s.name, s]));
23
+ const lines = [`🔌 MCP servers (${live.length} connected / ${configured.length} configured):`];
24
+ for (const cfg of configured) {
25
+ const conn = liveByName.get(cfg.name);
26
+ const transport = cfg.url ? `http ${cfg.url}` : `stdio ${cfg.command ?? ''}`.trim();
27
+ if (conn) {
28
+ lines.push(`\n ✓ ${cfg.name} (${transport}) — ${conn.tools.length} tool(s)`);
29
+ for (const t of conn.tools)
30
+ lines.push(` • ${t}`);
31
+ }
32
+ else {
33
+ lines.push(`\n ✗ ${cfg.name} (${transport}) — not connected`);
34
+ }
35
+ }
36
+ for (const s of live) {
37
+ if (!configured.some((c) => c.name === s.name)) {
38
+ lines.push(`\n ✓ ${s.name} (runtime) — ${s.tools.length} tool(s)`);
39
+ for (const t of s.tools)
40
+ lines.push(` • ${t}`);
41
+ }
42
+ }
43
+ return reply(lines.join('\n'));
44
+ },
45
+ };
46
+ //# sourceMappingURL=mcp.js.map
@@ -0,0 +1,4 @@
1
+ import { SlashCommand } from './types.js';
2
+ export declare const undoCommand: SlashCommand;
3
+ export declare const redoCommand: SlashCommand;
4
+ //# sourceMappingURL=undo.d.ts.map
@@ -0,0 +1,45 @@
1
+ import { fileSnapshotStore } from '../file-snapshot-store.js';
2
+ import * as path from 'path';
3
+ function rel(p) {
4
+ const r = path.relative(process.cwd(), p);
5
+ return r && !r.startsWith('..') ? r : p;
6
+ }
7
+ function formatResult(verb, result) {
8
+ const lines = [`↩️ ${verb} "${result.label}" — ${result.restored.length} file(s):`];
9
+ for (const p of result.restored)
10
+ lines.push(` • ${rel(p)}`);
11
+ if (result.failed.length > 0) {
12
+ lines.push(` ⚠️ ${result.failed.length} could not be restored:`);
13
+ for (const p of result.failed)
14
+ lines.push(` ✗ ${rel(p)}`);
15
+ }
16
+ return lines.join('\n');
17
+ }
18
+ function reply(context, content) {
19
+ const updatedMessages = [...context.messages, { role: 'assistant', content }];
20
+ context.setMessages(updatedMessages);
21
+ return { handled: true, shouldContinue: false, updatedContext: { messages: updatedMessages } };
22
+ }
23
+ export const undoCommand = {
24
+ name: '/undo',
25
+ description: 'Revert the file changes from the last turn',
26
+ async execute(context) {
27
+ const result = await fileSnapshotStore.undo();
28
+ if (!result) {
29
+ return reply(context, 'Nothing to undo. (Only agent create_file/edit_file changes are tracked — not bash edits.)');
30
+ }
31
+ return reply(context, formatResult('Reverted', result));
32
+ },
33
+ };
34
+ export const redoCommand = {
35
+ name: '/redo',
36
+ description: 'Re-apply the changes undone by /undo',
37
+ async execute(context) {
38
+ const result = await fileSnapshotStore.redo();
39
+ if (!result) {
40
+ return reply(context, 'Nothing to redo.');
41
+ }
42
+ return reply(context, formatResult('Re-applied', result));
43
+ },
44
+ };
45
+ //# sourceMappingURL=undo.js.map
@@ -1,9 +1,9 @@
1
1
  import axios from 'axios';
2
- import { PROVIDERS, buildAuthHeaders } from './providers.js';
2
+ import { getProviders, buildAuthHeaders } from './providers.js';
3
3
  export async function scanProviders() {
4
4
  const detected = [];
5
5
  const notFound = [];
6
- for (const provider of PROVIDERS.filter((p) => !p.isLocal)) {
6
+ for (const provider of getProviders().filter((p) => !p.isLocal)) {
7
7
  const apiKey = findEnvVar(provider.envVars);
8
8
  if (apiKey) {
9
9
  const models = await fetchModels(provider, apiKey).catch(() => []);
@@ -18,7 +18,7 @@ export async function scanProviders() {
18
18
  notFound.push(provider.id);
19
19
  }
20
20
  }
21
- const localProbes = PROVIDERS.filter((p) => p.isLocal).map(async (provider) => {
21
+ const localProbes = getProviders().filter((p) => p.isLocal).map(async (provider) => {
22
22
  const running = await probeLocal(provider);
23
23
  if (running) {
24
24
  const models = await fetchModels(provider, '').catch(() => []);
@@ -36,7 +36,7 @@ export async function scanProviders() {
36
36
  return { detected, notFound };
37
37
  }
38
38
  export async function scanProvider(providerId, apiKey) {
39
- const provider = PROVIDERS.find((p) => p.id === providerId);
39
+ const provider = getProviders().find((p) => p.id === providerId);
40
40
  if (!provider)
41
41
  return null;
42
42
  const key = apiKey || findEnvVar(provider.envVars) || '';
@@ -1,4 +1,4 @@
1
- import { OpenConfig, EndpointConfig, ModelInfo, OrquestaConfig, OrchestrationConfig, OrchestrationRole } from '../../types/index.js';
1
+ import { OpenConfig, EndpointConfig, ModelInfo, OrquestaConfig, OrchestrationConfig, OrchestrationRole, McpServerConfig, LspServerConfig } from '../../types/index.js';
2
2
  export declare class ConfigManager {
3
3
  private config;
4
4
  private initialized;
@@ -62,6 +62,12 @@ export declare class ConfigManager {
62
62
  setRefinerEnabled(enabled: boolean): Promise<void>;
63
63
  isSingleAgentMode(): boolean;
64
64
  setSingleAgentMode(enabled: boolean): Promise<void>;
65
+ getMcpServers(): McpServerConfig[];
66
+ addMcpServer(server: McpServerConfig): Promise<void>;
67
+ removeMcpServer(name: string): Promise<boolean>;
68
+ getLspServers(): LspServerConfig[];
69
+ isLspEnabled(): boolean;
70
+ setLspEnabled(enabled: boolean): Promise<void>;
65
71
  isWorktreeIsolationEnabled(): boolean;
66
72
  setWorktreeIsolationEnabled(enabled: boolean): Promise<void>;
67
73
  }
@@ -425,6 +425,42 @@ export class ConfigManager {
425
425
  config.orchestration.singleAgentMode = enabled;
426
426
  await this.saveConfig();
427
427
  }
428
+ getMcpServers() {
429
+ return this.config?.mcpServers ?? [];
430
+ }
431
+ async addMcpServer(server) {
432
+ const config = this.getConfig();
433
+ const list = config.mcpServers ?? [];
434
+ const idx = list.findIndex((s) => s.name === server.name);
435
+ if (idx >= 0)
436
+ list[idx] = server;
437
+ else
438
+ list.push(server);
439
+ config.mcpServers = list;
440
+ await this.saveConfig();
441
+ }
442
+ async removeMcpServer(name) {
443
+ const config = this.getConfig();
444
+ const list = config.mcpServers ?? [];
445
+ const next = list.filter((s) => s.name !== name);
446
+ const removed = next.length !== list.length;
447
+ if (removed) {
448
+ config.mcpServers = next;
449
+ await this.saveConfig();
450
+ }
451
+ return removed;
452
+ }
453
+ getLspServers() {
454
+ return this.config?.lspServers ?? [];
455
+ }
456
+ isLspEnabled() {
457
+ return this.config?.lsp?.enabled ?? true;
458
+ }
459
+ async setLspEnabled(enabled) {
460
+ const config = this.getConfig();
461
+ config.lsp = { ...config.lsp, enabled };
462
+ await this.saveConfig();
463
+ }
428
464
  isWorktreeIsolationEnabled() {
429
465
  return this.config?.orchestration?.worktreeIsolation ?? false;
430
466
  }
@@ -22,6 +22,9 @@ export interface ProviderDefinition {
22
22
  healthCheckPath?: string;
23
23
  }
24
24
  export declare const PROVIDERS: ProviderDefinition[];
25
+ export declare function refreshCatalogFromServer(timeoutMs?: number): Promise<boolean>;
26
+ export declare function getCatalogVersion(): number;
27
+ export declare function getProviders(): ProviderDefinition[];
25
28
  export declare function getProvider(id: string): ProviderDefinition | undefined;
26
29
  export declare function detectProviderFromUrl(baseUrl: string): ProviderDefinition | undefined;
27
30
  export declare function modelHasCapability(providerId: string, modelId: string, capability: ModelCapability): boolean;
@@ -1,3 +1,6 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
1
4
  export const PROVIDERS = [
2
5
  {
3
6
  id: 'openai',
@@ -302,17 +305,98 @@ export const PROVIDERS = [
302
305
  knownModels: [],
303
306
  },
304
307
  ];
308
+ const CATALOG_CACHE_DIR = path.join(os.homedir(), '.orquesta-cli');
309
+ const CATALOG_CACHE_FILE = path.join(CATALOG_CACHE_DIR, 'catalog.json');
310
+ const CATALOG_ENDPOINT_PATH = '/api/orquesta-cli/catalog';
311
+ let effectiveProviders = PROVIDERS;
312
+ let effectiveVersion = 0;
313
+ function overlayProviders(incoming) {
314
+ const byId = new Map();
315
+ for (const p of PROVIDERS)
316
+ byId.set(p.id, p);
317
+ for (const p of incoming) {
318
+ if (p && typeof p.id === 'string' && Array.isArray(p.knownModels)) {
319
+ byId.set(p.id, p);
320
+ }
321
+ }
322
+ return Array.from(byId.values());
323
+ }
324
+ function isValidCatalogProviders(value) {
325
+ return (Array.isArray(value) &&
326
+ value.length > 0 &&
327
+ value.every((p) => p &&
328
+ typeof p.id === 'string' &&
329
+ typeof p.baseUrl === 'string' &&
330
+ Array.isArray(p.knownModels)));
331
+ }
332
+ function seedCatalogFromDisk() {
333
+ try {
334
+ const raw = fs.readFileSync(CATALOG_CACHE_FILE, 'utf-8');
335
+ const cache = JSON.parse(raw);
336
+ if (isValidCatalogProviders(cache.providers)) {
337
+ effectiveProviders = overlayProviders(cache.providers);
338
+ effectiveVersion = typeof cache.version === 'number' ? cache.version : 0;
339
+ }
340
+ }
341
+ catch {
342
+ }
343
+ }
344
+ seedCatalogFromDisk();
345
+ function catalogUrl() {
346
+ const base = process.env['ORQUESTA_API_URL'] || 'https://getorquesta.com';
347
+ return base.replace(/\/$/, '') + CATALOG_ENDPOINT_PATH;
348
+ }
349
+ export async function refreshCatalogFromServer(timeoutMs = 4000) {
350
+ try {
351
+ const controller = new AbortController();
352
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
353
+ const res = await fetch(catalogUrl(), {
354
+ signal: controller.signal,
355
+ headers: { Accept: 'application/json' },
356
+ });
357
+ clearTimeout(timer);
358
+ if (!res.ok)
359
+ return false;
360
+ const json = (await res.json());
361
+ if (!isValidCatalogProviders(json.providers))
362
+ return false;
363
+ effectiveProviders = overlayProviders(json.providers);
364
+ effectiveVersion = typeof json.version === 'number' ? json.version : effectiveVersion;
365
+ try {
366
+ if (!fs.existsSync(CATALOG_CACHE_DIR))
367
+ fs.mkdirSync(CATALOG_CACHE_DIR, { recursive: true });
368
+ const cache = {
369
+ version: effectiveVersion,
370
+ providers: json.providers,
371
+ fetchedAt: new Date().toISOString(),
372
+ };
373
+ fs.writeFileSync(CATALOG_CACHE_FILE, JSON.stringify(cache, null, 2));
374
+ }
375
+ catch {
376
+ }
377
+ return true;
378
+ }
379
+ catch {
380
+ return false;
381
+ }
382
+ }
383
+ export function getCatalogVersion() {
384
+ return effectiveVersion;
385
+ }
386
+ export function getProviders() {
387
+ return effectiveProviders;
388
+ }
305
389
  export function getProvider(id) {
306
- return PROVIDERS.find((p) => p.id === id);
390
+ return effectiveProviders.find((p) => p.id === id);
307
391
  }
308
392
  export function detectProviderFromUrl(baseUrl) {
309
393
  const url = baseUrl.toLowerCase();
310
- for (const provider of PROVIDERS) {
394
+ for (const provider of effectiveProviders) {
311
395
  const providerHost = new URL(provider.baseUrl).hostname;
312
396
  if (url.includes(providerHost))
313
397
  return provider;
314
398
  }
315
- for (const provider of PROVIDERS.filter((p) => p.isLocal && p.localPort)) {
399
+ for (const provider of effectiveProviders.filter((p) => p.isLocal && p.localPort)) {
316
400
  if (url.includes(`:${provider.localPort}`))
317
401
  return provider;
318
402
  }
@@ -0,0 +1,25 @@
1
+ export interface UndoRedoResult {
2
+ label: string;
3
+ restored: string[];
4
+ failed: string[];
5
+ }
6
+ declare class FileSnapshotStore {
7
+ private current;
8
+ private undoStack;
9
+ private redoStack;
10
+ beginTurn(label: string): void;
11
+ seal(): void;
12
+ record(absPath: string, priorContent: string | null): void;
13
+ private apply;
14
+ undo(): Promise<UndoRedoResult | null>;
15
+ redo(): Promise<UndoRedoResult | null>;
16
+ getStatus(): {
17
+ undo: number;
18
+ redo: number;
19
+ pendingEdits: number;
20
+ };
21
+ reset(): void;
22
+ }
23
+ export declare const fileSnapshotStore: FileSnapshotStore;
24
+ export {};
25
+ //# sourceMappingURL=file-snapshot-store.d.ts.map
@@ -0,0 +1,104 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import { invalidateCache } from './file-cache.js';
4
+ import { logger } from '../utils/logger.js';
5
+ const MAX_SNAPSHOT_BYTES = 5 * 1024 * 1024;
6
+ const MAX_STACK = 50;
7
+ class FileSnapshotStore {
8
+ current = null;
9
+ undoStack = [];
10
+ redoStack = [];
11
+ beginTurn(label) {
12
+ this.seal();
13
+ this.redoStack = [];
14
+ this.current = { label: label.slice(0, 120), ts: Date.now(), entries: [] };
15
+ }
16
+ seal() {
17
+ if (this.current && this.current.entries.length > 0) {
18
+ this.undoStack.push(this.current);
19
+ if (this.undoStack.length > MAX_STACK)
20
+ this.undoStack.shift();
21
+ }
22
+ this.current = null;
23
+ }
24
+ record(absPath, priorContent) {
25
+ if (!this.current) {
26
+ this.current = { label: '(edits)', ts: Date.now(), entries: [] };
27
+ }
28
+ if (this.current.entries.some((e) => e.path === absPath))
29
+ return;
30
+ if (priorContent !== null && Buffer.byteLength(priorContent, 'utf-8') > MAX_SNAPSHOT_BYTES) {
31
+ logger.warn('Skipping snapshot: file too large for undo', { path: absPath });
32
+ return;
33
+ }
34
+ this.current.entries.push({ path: absPath, content: priorContent });
35
+ }
36
+ async apply(cp) {
37
+ const restored = [];
38
+ const failed = [];
39
+ for (const entry of cp.entries) {
40
+ let captured;
41
+ try {
42
+ captured = await fs.readFile(entry.path, 'utf-8');
43
+ }
44
+ catch {
45
+ captured = null;
46
+ }
47
+ try {
48
+ if (entry.content === null) {
49
+ await fs.rm(entry.path, { force: true });
50
+ }
51
+ else {
52
+ await fs.mkdir(path.dirname(entry.path), { recursive: true });
53
+ await fs.writeFile(entry.path, entry.content, 'utf-8');
54
+ }
55
+ invalidateCache(entry.path);
56
+ entry.content = captured;
57
+ restored.push(entry.path);
58
+ }
59
+ catch (err) {
60
+ logger.warn('Undo/redo failed for path', {
61
+ path: entry.path,
62
+ error: err instanceof Error ? err.message : String(err),
63
+ });
64
+ failed.push(entry.path);
65
+ }
66
+ }
67
+ return { label: cp.label, restored, failed };
68
+ }
69
+ async undo() {
70
+ this.seal();
71
+ const cp = this.undoStack.pop();
72
+ if (!cp)
73
+ return null;
74
+ const result = await this.apply(cp);
75
+ this.redoStack.push(cp);
76
+ if (this.redoStack.length > MAX_STACK)
77
+ this.redoStack.shift();
78
+ return result;
79
+ }
80
+ async redo() {
81
+ const cp = this.redoStack.pop();
82
+ if (!cp)
83
+ return null;
84
+ const result = await this.apply(cp);
85
+ this.undoStack.push(cp);
86
+ if (this.undoStack.length > MAX_STACK)
87
+ this.undoStack.shift();
88
+ return result;
89
+ }
90
+ getStatus() {
91
+ return {
92
+ undo: this.undoStack.length + (this.current && this.current.entries.length > 0 ? 1 : 0),
93
+ redo: this.redoStack.length,
94
+ pendingEdits: this.current?.entries.length ?? 0,
95
+ };
96
+ }
97
+ reset() {
98
+ this.current = null;
99
+ this.undoStack = [];
100
+ this.redoStack = [];
101
+ }
102
+ }
103
+ export const fileSnapshotStore = new FileSnapshotStore();
104
+ //# sourceMappingURL=file-snapshot-store.js.map
@@ -0,0 +1,6 @@
1
+ import { shutdownAllLspClients, getActiveLspClientCount, Diagnostic } from './lsp-client.js';
2
+ export declare function formatDiagnostics(relPath: string, diags: Diagnostic[]): string;
3
+ export declare function getDiagnosticsForFile(absPath: string): Promise<string>;
4
+ export { shutdownAllLspClients, getActiveLspClientCount };
5
+ export { findServerForFile, getServerDefs, resolveServerBinary } from './server-registry.js';
6
+ //# sourceMappingURL=index.d.ts.map