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.
- package/dist/cli.js +27 -4
- package/dist/core/commands/help.js +4 -0
- package/dist/core/commands/index.js +7 -0
- package/dist/core/commands/lsp.d.ts +3 -0
- package/dist/core/commands/lsp.js +37 -0
- package/dist/core/commands/mcp.d.ts +3 -0
- package/dist/core/commands/mcp.js +46 -0
- package/dist/core/commands/undo.d.ts +4 -0
- package/dist/core/commands/undo.js +45 -0
- package/dist/core/config/auto-detect.js +4 -4
- package/dist/core/config/config-manager.d.ts +7 -1
- package/dist/core/config/config-manager.js +36 -0
- package/dist/core/config/providers.d.ts +3 -0
- package/dist/core/config/providers.js +87 -3
- package/dist/core/file-snapshot-store.d.ts +25 -0
- package/dist/core/file-snapshot-store.js +104 -0
- package/dist/core/lsp/index.d.ts +6 -0
- package/dist/core/lsp/index.js +75 -0
- package/dist/core/lsp/jsonrpc.d.ts +18 -0
- package/dist/core/lsp/jsonrpc.js +38 -0
- package/dist/core/lsp/lsp-client.d.ts +40 -0
- package/dist/core/lsp/lsp-client.js +201 -0
- package/dist/core/lsp/server-registry.d.ts +14 -0
- package/dist/core/lsp/server-registry.js +85 -0
- package/dist/eval/eval-runner.js +14 -0
- package/dist/orchestration/plan-executor.js +2 -0
- package/dist/tools/llm/simple/file-tools.js +8 -2
- package/dist/tools/mcp/index.d.ts +3 -0
- package/dist/tools/mcp/index.js +3 -0
- package/dist/tools/mcp/mcp-client.d.ts +16 -0
- package/dist/tools/mcp/mcp-client.js +180 -0
- package/dist/tools/mcp/mcp-config.d.ts +4 -0
- package/dist/tools/mcp/mcp-config.js +87 -0
- package/dist/types/index.d.ts +21 -0
- package/dist/ui/hooks/slashCommandProcessor.js +17 -0
- package/package.json +2 -1
- package/dist/core/git-auto-updater.d.ts +0 -58
- 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 {
|
|
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
|
|
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 =
|
|
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,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,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,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 {
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|