@opencoven/coven-code 0.0.3 → 0.0.6
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/README.md +29 -130
- package/bin/coven-code +26 -0
- package/install.js +117 -0
- package/package.json +26 -23
- package/bin/coven-code-sdk.mjs +0 -12
- package/bin/coven-code.mjs +0 -19
- package/docs/CLI.md +0 -256
- package/docs/CONFIGURATION.md +0 -107
- package/docs/DEMO.md +0 -453
- package/docs/DEVELOPMENT.md +0 -104
- package/docs/DOGFOOD-PROTOCOL.md +0 -263
- package/docs/MCP-SKILLS-PLUGINS.md +0 -127
- package/docs/README.md +0 -39
- package/docs/RELEASE.md +0 -33
- package/docs/SDK.md +0 -107
- package/docs/superpowers/plans/2026-05-25-coven-code-panel-tui.md +0 -904
- package/docs/superpowers/plans/2026-05-25-coven-code-rebrand.md +0 -670
- package/docs/superpowers/specs/2026-05-25-coven-code-panel-tui-design.md +0 -235
- package/docs/superpowers/specs/2026-05-26-slash-first-tui-review.md +0 -63
- package/src/agent/fixture.mjs +0 -95
- package/src/agent/lane.mjs +0 -136
- package/src/cli/dispatch.mjs +0 -66
- package/src/cli/execute.mjs +0 -590
- package/src/cli/help.mjs +0 -58
- package/src/cli/interactive-core.mjs +0 -28
- package/src/cli/interactive-io.mjs +0 -101
- package/src/cli/interactive-slash.mjs +0 -184
- package/src/cli/notifications.mjs +0 -13
- package/src/cli/parse.mjs +0 -83
- package/src/cli/reasoning.mjs +0 -45
- package/src/cli/refs.mjs +0 -162
- package/src/cli/repl.mjs +0 -60
- package/src/cli/slash-commands.mjs +0 -375
- package/src/cli/stream-json.mjs +0 -116
- package/src/cli/tui-actions.mjs +0 -72
- package/src/cli/tui-blessed.mjs +0 -198
- package/src/cli/tui-keys.mjs +0 -80
- package/src/cli/tui-lane.mjs +0 -73
- package/src/cli/tui-render.mjs +0 -169
- package/src/cli/tui-submit.mjs +0 -82
- package/src/cli/tui.mjs +0 -174
- package/src/commands/agents.mjs +0 -53
- package/src/commands/config.mjs +0 -27
- package/src/commands/ide.mjs +0 -17
- package/src/commands/login.mjs +0 -84
- package/src/commands/mcp.mjs +0 -176
- package/src/commands/permissions-eval.mjs +0 -122
- package/src/commands/permissions-rules.mjs +0 -53
- package/src/commands/permissions-text.mjs +0 -112
- package/src/commands/permissions.mjs +0 -62
- package/src/commands/plugins.mjs +0 -86
- package/src/commands/review.mjs +0 -74
- package/src/commands/skill.mjs +0 -23
- package/src/commands/threads.mjs +0 -165
- package/src/commands/tools.mjs +0 -77
- package/src/commands/update.mjs +0 -31
- package/src/commands/usage.mjs +0 -34
- package/src/constants.mjs +0 -52
- package/src/main.mjs +0 -87
- package/src/mcp/discover.mjs +0 -154
- package/src/mcp/local.mjs +0 -55
- package/src/mcp/parsers.mjs +0 -46
- package/src/mcp/permissions.mjs +0 -52
- package/src/mcp/probe.mjs +0 -85
- package/src/mcp/registry.mjs +0 -96
- package/src/mcp/remote-oauth.mjs +0 -55
- package/src/mcp/remote-session.mjs +0 -54
- package/src/mcp/remote-sse.mjs +0 -82
- package/src/mcp/remote.mjs +0 -74
- package/src/plugins/api.mjs +0 -187
- package/src/plugins/configuration.mjs +0 -124
- package/src/plugins/discover.mjs +0 -84
- package/src/plugins/helpers.mjs +0 -187
- package/src/plugins/subsystems.mjs +0 -198
- package/src/plugins/validators.mjs +0 -142
- package/src/sdk-execute.mjs +0 -82
- package/src/sdk-install.mjs +0 -187
- package/src/sdk-settings.mjs +0 -88
- package/src/sdk.mjs +0 -163
- package/src/settings/load.mjs +0 -134
- package/src/settings/paths.mjs +0 -101
- package/src/skills/builtin/building-skills/SKILL.md +0 -20
- package/src/skills/discover.mjs +0 -95
- package/src/threads/store.mjs +0 -176
- package/src/tools/builtin/bash.mjs +0 -110
- package/src/tools/builtin/create-file.mjs +0 -66
- package/src/tools/builtin/edit-file.mjs +0 -76
- package/src/tools/builtin/finder.mjs +0 -73
- package/src/tools/builtin/glob.mjs +0 -74
- package/src/tools/builtin/grep.mjs +0 -82
- package/src/tools/builtin/index.mjs +0 -83
- package/src/tools/builtin/librarian.mjs +0 -97
- package/src/tools/builtin/look-at.mjs +0 -92
- package/src/tools/builtin/mcp.mjs +0 -51
- package/src/tools/builtin/mermaid.mjs +0 -59
- package/src/tools/builtin/oracle.mjs +0 -56
- package/src/tools/builtin/painter.mjs +0 -81
- package/src/tools/builtin/plugin-tool.mjs +0 -53
- package/src/tools/builtin/read-mcp-resource.mjs +0 -63
- package/src/tools/builtin/read-web-page.mjs +0 -72
- package/src/tools/builtin/read.mjs +0 -59
- package/src/tools/builtin/runtime-content.mjs +0 -31
- package/src/tools/builtin/runtime-decisions.mjs +0 -115
- package/src/tools/builtin/runtime.mjs +0 -85
- package/src/tools/builtin/task.mjs +0 -63
- package/src/tools/builtin/toolbox-tool.mjs +0 -57
- package/src/tools/builtin/undo-edit.mjs +0 -97
- package/src/tools/builtin/web-search.mjs +0 -128
- package/src/tools/toolbox.mjs +0 -273
- package/src/util/fs.mjs +0 -13
- package/src/util/glob.mjs +0 -46
- package/src/util/html.mjs +0 -21
- package/src/util/media.mjs +0 -13
- package/src/util/shell.mjs +0 -24
- package/src/util/table.mjs +0 -11
package/src/commands/usage.mjs
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { listThreads } from '../threads/store.mjs';
|
|
2
|
-
import { estimateTokenCount } from '../agent/fixture.mjs';
|
|
3
|
-
import { readEffectiveSettings } from '../settings/load.mjs';
|
|
4
|
-
|
|
5
|
-
export function runUsage(parsed = {}) {
|
|
6
|
-
const summary = localUsageSummary();
|
|
7
|
-
const showCosts = readEffectiveSettings(parsed)['covenCode.showCosts'] !== false;
|
|
8
|
-
if (showCosts) console.log('remote_balance: unavailable (local recreation)');
|
|
9
|
-
console.log(`threads: ${summary.threads}`);
|
|
10
|
-
console.log(`turns: ${summary.turns}`);
|
|
11
|
-
if (showCosts) {
|
|
12
|
-
console.log(`input_tokens_estimate: ${summary.inputTokens}`);
|
|
13
|
-
console.log(`output_tokens_estimate: ${summary.outputTokens}`);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function localUsageSummary() {
|
|
18
|
-
const threads = listThreads();
|
|
19
|
-
let turns = 0;
|
|
20
|
-
let inputTokens = 0;
|
|
21
|
-
let outputTokens = 0;
|
|
22
|
-
for (const thread of threads) {
|
|
23
|
-
const messages = Array.isArray(thread.messages) ? thread.messages : [];
|
|
24
|
-
for (const message of messages) {
|
|
25
|
-
if (message.role === 'user') {
|
|
26
|
-
turns += 1;
|
|
27
|
-
inputTokens += estimateTokenCount(message.content ?? '');
|
|
28
|
-
} else if (message.role === 'assistant') {
|
|
29
|
-
outputTokens += estimateTokenCount(message.content ?? '');
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return { threads: threads.length, turns, inputTokens, outputTokens };
|
|
34
|
-
}
|
package/src/constants.mjs
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
|
|
3
|
-
const packageJson = JSON.parse(
|
|
4
|
-
readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
|
|
5
|
-
);
|
|
6
|
-
|
|
7
|
-
export const VERSION = packageJson.version;
|
|
8
|
-
export const PRODUCT_NAME = 'Coven Code';
|
|
9
|
-
export const CLI_NAME = 'coven-code';
|
|
10
|
-
export const PACKAGE_NAME = '@opencoven/coven-code';
|
|
11
|
-
export const CONFIG_SUBDIR = 'coven-code';
|
|
12
|
-
export const PROJECT_SUBDIR = '.coven-code';
|
|
13
|
-
export const THREAD_URL_BASE = 'https://coven-code.local/threads';
|
|
14
|
-
|
|
15
|
-
export const FILE_MENTION_MAX_LINES = 500;
|
|
16
|
-
export const FILE_MENTION_MAX_LINE_LENGTH = 2048;
|
|
17
|
-
export const REPL_HISTORY_LIMIT = 500;
|
|
18
|
-
|
|
19
|
-
export const AGENT_MODES = ['smart', 'deep', 'rush', 'large'];
|
|
20
|
-
|
|
21
|
-
export const BUILTIN_TOOLS = [
|
|
22
|
-
['Bash', 'built-in', "Executes the given shell command in the user's default shell"],
|
|
23
|
-
['Read', 'built-in', 'Reads a UTF-8 text file from disk'],
|
|
24
|
-
['Grep', 'built-in', 'Searches files by regular expression'],
|
|
25
|
-
['glob', 'built-in', 'Finds files by glob pattern'],
|
|
26
|
-
['create_file', 'built-in', 'Creates a new file'],
|
|
27
|
-
['edit_file', 'built-in', 'Edits an existing file'],
|
|
28
|
-
['oracle', 'built-in', 'Asks for second-opinion review or reasoning'],
|
|
29
|
-
['librarian', 'built-in', 'Researches code across repositories or the current workspace'],
|
|
30
|
-
['Task', 'built-in', 'Runs an isolated subagent for delegated work'],
|
|
31
|
-
['painter', 'built-in', 'Generates or edits local image artifacts'],
|
|
32
|
-
['mermaid', 'built-in', 'Renders a Mermaid diagram from the provided code'],
|
|
33
|
-
['look_at', 'built-in', 'Inspects images, PDFs, or media files with a goal'],
|
|
34
|
-
['web_search', 'built-in', 'Searches the web for information'],
|
|
35
|
-
['read_web_page', 'built-in', 'Reads and extracts text from a web page'],
|
|
36
|
-
['undo_edit', 'built-in', 'Undoes the latest edit_file change'],
|
|
37
|
-
['find_thread', 'built-in', 'Finds prior threads by keyword, file, or task context'],
|
|
38
|
-
['finder', 'built-in', 'Finds prior threads by keyword, file, or task context'],
|
|
39
|
-
['read_mcp_resource', 'built-in', 'Reads a resource from a configured MCP server'],
|
|
40
|
-
];
|
|
41
|
-
|
|
42
|
-
export const BUILTIN_PERMISSIONS = [
|
|
43
|
-
['allow', 'Bash', 'ls'],
|
|
44
|
-
['allow', 'Bash', 'git status'],
|
|
45
|
-
['allow', 'Bash', 'git diff'],
|
|
46
|
-
['allow', 'Bash', 'git log'],
|
|
47
|
-
['allow', 'Bash', 'npm test'],
|
|
48
|
-
['allow', 'Bash', 'cargo build'],
|
|
49
|
-
['ask', 'Bash', 'git push'],
|
|
50
|
-
['ask', 'Bash', 'rm -rf'],
|
|
51
|
-
['ask', 'Bash', 'sudo'],
|
|
52
|
-
];
|
package/src/main.mjs
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { parseGlobalArgs, UsageError } from './cli/parse.mjs';
|
|
2
|
-
import { printHelp } from './cli/help.mjs';
|
|
3
|
-
import { runCommand } from './cli/dispatch.mjs';
|
|
4
|
-
import { runExecute } from './cli/execute.mjs';
|
|
5
|
-
import { runInteractive } from './cli/repl.mjs';
|
|
6
|
-
import { runTuiInteractive } from './cli/tui.mjs';
|
|
7
|
-
import { runIdeConnect } from './commands/ide.mjs';
|
|
8
|
-
import { readStdin } from './util/shell.mjs';
|
|
9
|
-
import { AGENT_MODES, VERSION } from './constants.mjs';
|
|
10
|
-
import { latestActiveThread, requireThread } from './threads/store.mjs';
|
|
11
|
-
import { reasoningEffortForMode } from './cli/reasoning.mjs';
|
|
12
|
-
|
|
13
|
-
export { UsageError };
|
|
14
|
-
|
|
15
|
-
export async function main() {
|
|
16
|
-
const args = process.argv.slice(2);
|
|
17
|
-
const parsed = parseGlobalArgs(args);
|
|
18
|
-
const stdin = readStdin();
|
|
19
|
-
|
|
20
|
-
if (parsed.help) {
|
|
21
|
-
printHelp();
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (parsed.version) {
|
|
26
|
-
console.log(VERSION);
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (parsed.ide) {
|
|
31
|
-
runIdeConnect(parsed.ide);
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const command = parsed.positionals[0];
|
|
36
|
-
if (command) {
|
|
37
|
-
await runCommand(command, parsed.positionals.slice(1), parsed, stdin);
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (parsed.streamJson && !parsed.execute) {
|
|
42
|
-
throw new UsageError('--stream-json requires --execute');
|
|
43
|
-
}
|
|
44
|
-
if (parsed.streamJsonInput && !parsed.streamJson) {
|
|
45
|
-
throw new UsageError('--stream-json-input requires --stream-json');
|
|
46
|
-
}
|
|
47
|
-
if (parsed.continueThread && !parsed.execute) {
|
|
48
|
-
throw new UsageError('--continue requires --execute');
|
|
49
|
-
}
|
|
50
|
-
if (!AGENT_MODES.includes(parsed.mode)) {
|
|
51
|
-
throw new UsageError(`mode must be one of: ${AGENT_MODES.join(', ')}`);
|
|
52
|
-
}
|
|
53
|
-
parsed.reasoningEffort = reasoningEffortForMode(parsed.mode, parsed.reasoningEffort);
|
|
54
|
-
|
|
55
|
-
if (parsed.execute || (stdin.length > 0 && !process.stdout.isTTY && process.env.COVEN_CODE_TUI_SCRIPTED !== '1')) {
|
|
56
|
-
await runExecute(parsed, stdin, { thread: continuationThread(parsed) });
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (process.env.COVEN_CODE_TUI_SCRIPTED === '1' || (process.stdout.isTTY && (process.stdin.isTTY || stdin.length > 0))) {
|
|
61
|
-
const runner = selectInteractiveRunner({
|
|
62
|
-
stdinIsTTY: Boolean(process.stdin.isTTY),
|
|
63
|
-
stdoutIsTTY: Boolean(process.stdout.isTTY),
|
|
64
|
-
env: process.env,
|
|
65
|
-
});
|
|
66
|
-
if (runner === 'tui') await runTuiInteractive(parsed, stdin);
|
|
67
|
-
else await runInteractive(parsed, stdin);
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
printHelp();
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function selectInteractiveRunner({ stdinIsTTY, stdoutIsTTY, env }) {
|
|
75
|
-
if (env.COVEN_CODE_TUI_SCRIPTED === '1') return 'tui';
|
|
76
|
-
if (env.COVEN_CODE_REPL === '1') return 'repl';
|
|
77
|
-
if (stdinIsTTY && stdoutIsTTY) return 'tui';
|
|
78
|
-
return 'repl';
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function continuationThread(parsed) {
|
|
82
|
-
if (!parsed.continueThread) return undefined;
|
|
83
|
-
if (typeof parsed.continueThread === 'string') return requireThread(parsed.continueThread);
|
|
84
|
-
const thread = latestActiveThread();
|
|
85
|
-
if (!thread) throw new UsageError('No active thread to continue');
|
|
86
|
-
return thread;
|
|
87
|
-
}
|
package/src/mcp/discover.mjs
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { readManagedSettings, readSettings, readSettingsFile } from '../settings/load.mjs';
|
|
4
|
-
import { findWorkspaceSettingsFile } from '../settings/paths.mjs';
|
|
5
|
-
import {
|
|
6
|
-
isMcpServerAllowed,
|
|
7
|
-
mcpPermissionRules,
|
|
8
|
-
mcpPermissionStatus,
|
|
9
|
-
readWorkspaceMcpApprovals,
|
|
10
|
-
} from './permissions.mjs';
|
|
11
|
-
import { mcpRegistryGate, mcpRegistryStatus } from './registry.mjs';
|
|
12
|
-
import { listSkills } from '../skills/discover.mjs';
|
|
13
|
-
|
|
14
|
-
const MCP_SERVERS_SETTING = 'covenCode.mcpServers';
|
|
15
|
-
|
|
16
|
-
export function listConfiguredMcpServers(parsed = {}) {
|
|
17
|
-
const { userSettings, workspaceSettings, managedSettings, approvals, permissionRules, registryGate } = mcpSettings(parsed);
|
|
18
|
-
const byName = new Map();
|
|
19
|
-
|
|
20
|
-
for (const [name, config] of Object.entries(userSettings[MCP_SERVERS_SETTING] ?? {})) {
|
|
21
|
-
const expandedConfig = expandMcpServerConfigEnv(config);
|
|
22
|
-
byName.set(name, {
|
|
23
|
-
name,
|
|
24
|
-
config: expandedConfig,
|
|
25
|
-
source: 'user',
|
|
26
|
-
status: mcpServerStatus('approved', expandedConfig, permissionRules, registryGate),
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
for (const [name, config] of Object.entries(workspaceSettings[MCP_SERVERS_SETTING] ?? {})) {
|
|
30
|
-
const expandedConfig = expandMcpServerConfigEnv(config);
|
|
31
|
-
byName.set(name, {
|
|
32
|
-
name,
|
|
33
|
-
config: expandedConfig,
|
|
34
|
-
source: 'workspace',
|
|
35
|
-
status: mcpServerStatus(approvals[name] ? 'approved' : 'awaiting approval', expandedConfig, permissionRules, registryGate),
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
for (const [name, config] of Object.entries(managedSettings[MCP_SERVERS_SETTING] ?? {})) {
|
|
39
|
-
const expandedConfig = expandMcpServerConfigEnv(config);
|
|
40
|
-
byName.set(name, {
|
|
41
|
-
name,
|
|
42
|
-
config: expandedConfig,
|
|
43
|
-
source: 'managed',
|
|
44
|
-
status: mcpServerStatus('approved', expandedConfig, permissionRules, registryGate),
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function listActiveMcpServerEntries(parsed = {}, prompt = '') {
|
|
52
|
-
const { permissionRules, registryGate } = mcpSettings(parsed);
|
|
53
|
-
const inline = parseInlineMcpServers(parsed.mcpConfig)
|
|
54
|
-
.filter((server) => !isMcpServerDisabled(server.config))
|
|
55
|
-
.filter((server) => !mcpRegistryStatus(server.config, registryGate))
|
|
56
|
-
.filter((server) => isMcpServerAllowed(server.config, permissionRules))
|
|
57
|
-
.map((server) => ({
|
|
58
|
-
name: server.name,
|
|
59
|
-
status: 'connected',
|
|
60
|
-
source: 'cli',
|
|
61
|
-
config: server.config,
|
|
62
|
-
}));
|
|
63
|
-
const inlineNames = new Set(inline.map((server) => server.name));
|
|
64
|
-
const configured = listConfiguredMcpServers(parsed)
|
|
65
|
-
.filter((server) => server.status === 'approved')
|
|
66
|
-
.filter((server) => !inlineNames.has(server.name))
|
|
67
|
-
.map((server) => ({ ...server, status: 'connected' }));
|
|
68
|
-
const occupiedNames = new Set([...inline, ...configured].map((server) => server.name));
|
|
69
|
-
const skillServers = listSkillMcpServers(prompt, parsed)
|
|
70
|
-
.filter((server) => !isMcpServerDisabled(server.config))
|
|
71
|
-
.filter((server) => !mcpRegistryStatus(server.config, registryGate))
|
|
72
|
-
.filter((server) => isMcpServerAllowed(server.config, permissionRules))
|
|
73
|
-
.filter((server) => !occupiedNames.has(server.name))
|
|
74
|
-
.map((server) => ({ ...server, status: 'connected' }));
|
|
75
|
-
return [...inline, ...configured, ...skillServers];
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function mcpSettings(parsed = {}) {
|
|
79
|
-
const userSettings = readSettings(parsed);
|
|
80
|
-
const workspacePath = findWorkspaceSettingsFile(process.cwd());
|
|
81
|
-
const workspaceSettings = workspacePath ? readSettingsFile(workspacePath) : {};
|
|
82
|
-
const managedSettings = readManagedSettings();
|
|
83
|
-
return {
|
|
84
|
-
userSettings,
|
|
85
|
-
workspaceSettings,
|
|
86
|
-
managedSettings,
|
|
87
|
-
approvals: readWorkspaceMcpApprovals(process.cwd()),
|
|
88
|
-
permissionRules: mcpPermissionRules(userSettings, workspaceSettings, managedSettings),
|
|
89
|
-
registryGate: mcpRegistryGate(userSettings, workspaceSettings, managedSettings),
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function mcpServerStatus(defaultStatus, config, permissionRules, registryGate) {
|
|
94
|
-
if (isMcpServerDisabled(config)) return 'disabled';
|
|
95
|
-
const registryStatus = mcpRegistryStatus(config, registryGate);
|
|
96
|
-
if (registryStatus) return registryStatus;
|
|
97
|
-
return mcpPermissionStatus(defaultStatus, config, permissionRules);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function isMcpServerDisabled(config = {}) {
|
|
101
|
-
return config.disabled === true;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function listActiveMcpServers(parsed = {}) {
|
|
105
|
-
return listActiveMcpServerEntries(parsed).map(({ name, source }) => ({
|
|
106
|
-
name,
|
|
107
|
-
status: 'connected',
|
|
108
|
-
source,
|
|
109
|
-
}));
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function parseInlineMcpServers(raw) {
|
|
113
|
-
if (!raw) return [];
|
|
114
|
-
try {
|
|
115
|
-
const parsed = JSON.parse(raw);
|
|
116
|
-
const servers = parsed.mcpServers ?? parsed[MCP_SERVERS_SETTING] ?? parsed;
|
|
117
|
-
return Object.keys(servers).map((name) => ({ name, config: expandMcpServerConfigEnv(servers[name]) }));
|
|
118
|
-
} catch {
|
|
119
|
-
return [{ name: 'inline', config: { command: expandEnvVars(raw) } }];
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export function expandMcpServerConfigEnv(value) {
|
|
124
|
-
if (typeof value === 'string') return expandEnvVars(value);
|
|
125
|
-
if (Array.isArray(value)) return value.map((entry) => expandMcpServerConfigEnv(entry));
|
|
126
|
-
if (value && typeof value === 'object') {
|
|
127
|
-
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, expandMcpServerConfigEnv(entry)]));
|
|
128
|
-
}
|
|
129
|
-
return value;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export function expandEnvVars(value) {
|
|
133
|
-
return String(value).replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, name) => process.env[name] ?? '');
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export function formatMcpServerCommand(config) {
|
|
137
|
-
if (config.url) return config.url;
|
|
138
|
-
return [config.command, ...(config.args ?? [])].filter(Boolean).join(' ');
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function listSkillMcpServers(prompt = '', parsed = {}) {
|
|
142
|
-
const lower = prompt.toLowerCase();
|
|
143
|
-
const servers = [];
|
|
144
|
-
for (const skill of listSkills({ parsed })) {
|
|
145
|
-
if (!lower.includes(skill.name.toLowerCase())) continue;
|
|
146
|
-
const mcpPath = path.join(skill.dir, 'mcp.json');
|
|
147
|
-
if (!existsSync(mcpPath)) continue;
|
|
148
|
-
const mcp = readSettingsFile(mcpPath);
|
|
149
|
-
for (const [name, config] of Object.entries(mcp)) {
|
|
150
|
-
servers.push({ name, config: expandMcpServerConfigEnv(config), source: `skill:${skill.name}` });
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return servers;
|
|
154
|
-
}
|
package/src/mcp/local.mjs
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { parseMcpCallOutput, parseMcpResourceOutput, parseMcpToolsOutput } from './parsers.mjs';
|
|
3
|
-
|
|
4
|
-
export function queryLocalMcpTools(config = {}) {
|
|
5
|
-
return parseMcpToolsOutput(queryLocalMcpToolsResult(config).stdout ?? '');
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function queryLocalMcpToolsResult(config = {}) {
|
|
9
|
-
if (!config.command) return [];
|
|
10
|
-
return spawnSync(config.command, config.args ?? [], {
|
|
11
|
-
input: localMcpRequestInput('tools/list', {}),
|
|
12
|
-
env: { ...process.env, ...(config.env ?? {}) },
|
|
13
|
-
encoding: 'utf8',
|
|
14
|
-
timeout: 1500,
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function callLocalMcpTool(config = {}, name, args = {}) {
|
|
19
|
-
if (!config.command) return '';
|
|
20
|
-
const result = spawnSync(config.command, config.args ?? [], {
|
|
21
|
-
input: localMcpRequestInput('tools/call', { name, arguments: args }),
|
|
22
|
-
env: { ...process.env, ...(config.env ?? {}) },
|
|
23
|
-
encoding: 'utf8',
|
|
24
|
-
timeout: 1500,
|
|
25
|
-
});
|
|
26
|
-
return parseMcpCallOutput(result.stdout);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function readLocalMcpResource(config = {}, uri) {
|
|
30
|
-
if (!config.command) return '';
|
|
31
|
-
const result = spawnSync(config.command, config.args ?? [], {
|
|
32
|
-
input: localMcpRequestInput('resources/read', { uri }),
|
|
33
|
-
env: { ...process.env, ...(config.env ?? {}) },
|
|
34
|
-
encoding: 'utf8',
|
|
35
|
-
timeout: 1500,
|
|
36
|
-
});
|
|
37
|
-
return parseMcpResourceOutput(result.stdout);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function localMcpRequestInput(method, params = {}) {
|
|
41
|
-
return [
|
|
42
|
-
{
|
|
43
|
-
jsonrpc: '2.0',
|
|
44
|
-
id: 1,
|
|
45
|
-
method: 'initialize',
|
|
46
|
-
params: {
|
|
47
|
-
protocolVersion: '2025-06-18',
|
|
48
|
-
capabilities: {},
|
|
49
|
-
clientInfo: { name: 'coven-code', version: '0.0.0' },
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
{ jsonrpc: '2.0', method: 'notifications/initialized' },
|
|
53
|
-
{ jsonrpc: '2.0', id: 2, method, params },
|
|
54
|
-
].map((message) => JSON.stringify(message)).join('\n') + '\n';
|
|
55
|
-
}
|
package/src/mcp/parsers.mjs
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
export function parseMcpCallOutput(stdout = '') {
|
|
2
|
-
for (const line of stdout.split(/\r?\n/).filter(Boolean)) {
|
|
3
|
-
try {
|
|
4
|
-
const message = JSON.parse(line);
|
|
5
|
-
const content = message.result?.content ?? message.content;
|
|
6
|
-
if (Array.isArray(content)) {
|
|
7
|
-
return content.map((entry) => entry.text ?? entry.content ?? JSON.stringify(entry)).join('\n');
|
|
8
|
-
}
|
|
9
|
-
if (typeof content === 'string') return content;
|
|
10
|
-
if (message.result !== undefined) return JSON.stringify(message.result);
|
|
11
|
-
} catch {
|
|
12
|
-
// Non-JSON diagnostic output from MCP server startup is ignored.
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
return '';
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function parseMcpResourceOutput(stdout = '') {
|
|
19
|
-
for (const line of stdout.split(/\r?\n/).filter(Boolean)) {
|
|
20
|
-
try {
|
|
21
|
-
const message = JSON.parse(line);
|
|
22
|
-
const contents = message.result?.contents ?? message.contents;
|
|
23
|
-
if (Array.isArray(contents)) {
|
|
24
|
-
return contents.map((entry) => entry.text ?? entry.content ?? entry.blob ?? JSON.stringify(entry)).join('\n');
|
|
25
|
-
}
|
|
26
|
-
if (typeof contents === 'string') return contents;
|
|
27
|
-
if (message.result !== undefined) return JSON.stringify(message.result);
|
|
28
|
-
} catch {
|
|
29
|
-
// Non-JSON diagnostic output from MCP server startup is ignored.
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return '';
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function parseMcpToolsOutput(stdout = '') {
|
|
36
|
-
for (const line of stdout.split(/\r?\n/).filter(Boolean)) {
|
|
37
|
-
try {
|
|
38
|
-
const message = JSON.parse(line);
|
|
39
|
-
if (Array.isArray(message.result?.tools)) return message.result.tools;
|
|
40
|
-
if (Array.isArray(message.tools)) return message.tools;
|
|
41
|
-
} catch {
|
|
42
|
-
// Non-JSON diagnostic output from MCP server startup is ignored.
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return [];
|
|
46
|
-
}
|
package/src/mcp/permissions.mjs
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import { findWorkspaceSettingsFile, workspaceSettingsFile } from '../settings/paths.mjs';
|
|
3
|
-
import { readManagedSettings, readSettings, readSettingsFile, writeSettingsFile } from '../settings/load.mjs';
|
|
4
|
-
import { globMatch } from '../util/glob.mjs';
|
|
5
|
-
|
|
6
|
-
const MCP_PERMISSIONS_SETTING = 'covenCode.mcpPermissions';
|
|
7
|
-
|
|
8
|
-
export function mcpPermissionRulesForCwd(parsed = {}) {
|
|
9
|
-
const userSettings = readSettings(parsed);
|
|
10
|
-
const workspacePath = findWorkspaceSettingsFile(process.cwd());
|
|
11
|
-
const workspaceSettings = workspacePath ? readSettingsFile(workspacePath) : {};
|
|
12
|
-
return mcpPermissionRules(userSettings, workspaceSettings, readManagedSettings());
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function mcpPermissionRules(userSettings = {}, workspaceSettings = {}, managedSettings = {}) {
|
|
16
|
-
if (Array.isArray(managedSettings[MCP_PERMISSIONS_SETTING])) return managedSettings[MCP_PERMISSIONS_SETTING];
|
|
17
|
-
if (Array.isArray(workspaceSettings[MCP_PERMISSIONS_SETTING])) return workspaceSettings[MCP_PERMISSIONS_SETTING];
|
|
18
|
-
if (Array.isArray(userSettings[MCP_PERMISSIONS_SETTING])) return userSettings[MCP_PERMISSIONS_SETTING];
|
|
19
|
-
return [];
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function mcpPermissionStatus(defaultStatus, config, rules) {
|
|
23
|
-
return isMcpServerAllowed(config, rules) ? defaultStatus : 'rejected';
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function isMcpServerAllowed(config = {}, rules = []) {
|
|
27
|
-
for (const rule of rules) {
|
|
28
|
-
if (!rule || typeof rule !== 'object' || !rule.matches || typeof rule.matches !== 'object') continue;
|
|
29
|
-
if (!mcpPermissionRuleMatches(config, rule.matches)) continue;
|
|
30
|
-
return rule.action !== 'reject';
|
|
31
|
-
}
|
|
32
|
-
return true;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function mcpPermissionRuleMatches(config = {}, matches = {}) {
|
|
36
|
-
const entries = Object.entries(matches);
|
|
37
|
-
if (entries.length === 0) return false;
|
|
38
|
-
return entries.every(([field, pattern]) => {
|
|
39
|
-
if (field === 'command') return globMatch(pattern, config.command ?? '');
|
|
40
|
-
if (field === 'args') return globMatch(pattern, (config.args ?? []).join(' '));
|
|
41
|
-
if (field === 'url') return globMatch(pattern, config.url ?? '');
|
|
42
|
-
return false;
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function readWorkspaceMcpApprovals(cwd) {
|
|
47
|
-
return readSettingsFile(path.join(path.dirname(workspaceSettingsFile(cwd)), 'mcp-approvals.json'));
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export async function writeWorkspaceMcpApprovals(cwd, approvals) {
|
|
51
|
-
await writeSettingsFile(path.join(path.dirname(workspaceSettingsFile(cwd)), 'mcp-approvals.json'), approvals);
|
|
52
|
-
}
|
package/src/mcp/probe.mjs
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { globMatch } from '../util/glob.mjs';
|
|
2
|
-
import {
|
|
3
|
-
callLocalMcpTool,
|
|
4
|
-
queryLocalMcpTools,
|
|
5
|
-
queryLocalMcpToolsResult,
|
|
6
|
-
readLocalMcpResource,
|
|
7
|
-
} from './local.mjs';
|
|
8
|
-
import { parseMcpToolsOutput } from './parsers.mjs';
|
|
9
|
-
import {
|
|
10
|
-
callRemoteMcpTool,
|
|
11
|
-
queryRemoteMcpTools,
|
|
12
|
-
readRemoteMcpResource,
|
|
13
|
-
} from './remote.mjs';
|
|
14
|
-
|
|
15
|
-
export async function discoverMcpToolRows(servers) {
|
|
16
|
-
const rows = [];
|
|
17
|
-
for (const server of servers) {
|
|
18
|
-
const toolDefs = filterIncludedMcpTools(
|
|
19
|
-
server.config,
|
|
20
|
-
server.source?.startsWith('skill:')
|
|
21
|
-
? skillMcpTools(server.config)
|
|
22
|
-
: await queryMcpTools(server.config, server.name),
|
|
23
|
-
);
|
|
24
|
-
for (const tool of toolDefs) {
|
|
25
|
-
rows.push([
|
|
26
|
-
`mcp__${server.name}__${tool.name}`,
|
|
27
|
-
'local-mcp',
|
|
28
|
-
tool.description || `Tool from ${server.name}`,
|
|
29
|
-
]);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return rows;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export async function mcpServerHealth(config = {}, serverName = '') {
|
|
36
|
-
if (config.url) {
|
|
37
|
-
const tools = await queryRemoteMcpTools(config, serverName);
|
|
38
|
-
return formatToolHealth(tools);
|
|
39
|
-
}
|
|
40
|
-
const result = queryLocalMcpToolsResult(config);
|
|
41
|
-
if (result.error) return `error ${result.error.code ?? result.error.message}`;
|
|
42
|
-
if ((result.status ?? 0) !== 0) return `error exit ${result.status}`;
|
|
43
|
-
return formatToolHealth(parseMcpToolsOutput(result.stdout));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function formatToolHealth(tools = []) {
|
|
47
|
-
return `ok ${tools.length} ${tools.length === 1 ? 'tool' : 'tools'}`;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function filterIncludedMcpTools(config = {}, tools = []) {
|
|
51
|
-
return tools.filter((tool) => isMcpToolIncluded(config, tool.name));
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function isMcpToolIncluded(config = {}, toolName = '') {
|
|
55
|
-
if (!Array.isArray(config.includeTools) || config.includeTools.length === 0) return true;
|
|
56
|
-
return config.includeTools.some((pattern) => globMatch(pattern, toolName));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function queryMcpTools(config = {}, serverName = '') {
|
|
60
|
-
if (config.url) return queryRemoteMcpTools(config, serverName);
|
|
61
|
-
return queryLocalMcpTools(config);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export async function callMcpTool(config = {}, name, args = {}, serverName = '') {
|
|
65
|
-
if (config.url) return callRemoteMcpTool(config, name, args, serverName);
|
|
66
|
-
return callLocalMcpTool(config, name, args);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export async function readMcpResource(config = {}, uri, serverName = '') {
|
|
70
|
-
if (config.url) return readRemoteMcpResource(config, uri, serverName);
|
|
71
|
-
return readLocalMcpResource(config, uri);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function skillMcpTools(config = {}) {
|
|
75
|
-
return (config.includeTools ?? []).map((name) => ({
|
|
76
|
-
name,
|
|
77
|
-
description: `Tool from skill MCP server ${config.command || config.url || ''}`.trim(),
|
|
78
|
-
}));
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function parseMcpToolName(name) {
|
|
82
|
-
const match = name.match(/^mcp__([^_]+(?:_[^_]+)*)__([\s\S]+)$/);
|
|
83
|
-
if (!match) return undefined;
|
|
84
|
-
return { serverName: match[1], toolName: match[2] };
|
|
85
|
-
}
|
package/src/mcp/registry.mjs
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
|
|
5
|
-
const MCP_REGISTRY_SETTING = 'covenCode.mcpRegistry';
|
|
6
|
-
const MCP_REGISTRY_URL_SETTING = 'covenCode.mcpRegistry.url';
|
|
7
|
-
|
|
8
|
-
export function mcpRegistryGate(userSettings = {}, workspaceSettings = {}, managedSettings = {}) {
|
|
9
|
-
const policy = registryPolicy(managedSettings) ?? registryPolicy(workspaceSettings) ?? registryPolicy(userSettings);
|
|
10
|
-
if (!policy?.url) return { enabled: false };
|
|
11
|
-
const payload = registryPayload(policy.url);
|
|
12
|
-
if (!payload) return { enabled: true, reachable: false, allowedNames: new Set(), allowedUrls: new Set() };
|
|
13
|
-
try {
|
|
14
|
-
return { enabled: true, reachable: true, ...registryAllowlist(JSON.parse(payload)) };
|
|
15
|
-
} catch {
|
|
16
|
-
return { enabled: true, reachable: false, allowedNames: new Set(), allowedUrls: new Set() };
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function mcpRegistryStatus(config = {}, gate = { enabled: false }) {
|
|
21
|
-
if (!gate.enabled) return undefined;
|
|
22
|
-
if (!gate.reachable) return 'registry-blocked';
|
|
23
|
-
return registryAllowsConfig(config, gate) ? undefined : 'registry-blocked';
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function registryPolicy(settings = {}) {
|
|
27
|
-
const value = settings[MCP_REGISTRY_SETTING];
|
|
28
|
-
if (typeof value === 'string' && value.trim()) return { url: value.trim() };
|
|
29
|
-
if (value && typeof value === 'object' && typeof value.url === 'string' && value.url.trim()) {
|
|
30
|
-
return { url: value.url.trim() };
|
|
31
|
-
}
|
|
32
|
-
const url = settings[MCP_REGISTRY_URL_SETTING];
|
|
33
|
-
if (typeof url === 'string' && url.trim()) return { url: url.trim() };
|
|
34
|
-
return undefined;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function registryPayload(url) {
|
|
38
|
-
try {
|
|
39
|
-
if (/^https?:\/\//i.test(url)) return fetchRegistryUrlSync(url);
|
|
40
|
-
if (url.startsWith('file://')) return readFileSync(fileURLToPath(url), 'utf8');
|
|
41
|
-
return readFileSync(url, 'utf8');
|
|
42
|
-
} catch {
|
|
43
|
-
return undefined;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function fetchRegistryUrlSync(url) {
|
|
48
|
-
const script = `
|
|
49
|
-
const url = process.env.COVEN_CODE_MCP_REGISTRY_URL;
|
|
50
|
-
const timeout = AbortSignal.timeout(1500);
|
|
51
|
-
try {
|
|
52
|
-
const response = await fetch(url, { signal: timeout });
|
|
53
|
-
if (!response.ok) process.exit(2);
|
|
54
|
-
process.stdout.write(await response.text());
|
|
55
|
-
} catch {
|
|
56
|
-
process.exit(1);
|
|
57
|
-
}
|
|
58
|
-
`;
|
|
59
|
-
const result = spawnSync(process.execPath, ['--input-type=module', '-e', script], {
|
|
60
|
-
env: { ...process.env, COVEN_CODE_MCP_REGISTRY_URL: url },
|
|
61
|
-
encoding: 'utf8',
|
|
62
|
-
timeout: 2500,
|
|
63
|
-
});
|
|
64
|
-
return result.status === 0 ? result.stdout : undefined;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function registryAllowlist(payload) {
|
|
68
|
-
const allowedNames = new Set();
|
|
69
|
-
const allowedUrls = new Set();
|
|
70
|
-
const entries = Array.isArray(payload?.servers) ? payload.servers : Array.isArray(payload) ? payload : [];
|
|
71
|
-
for (const entry of entries) {
|
|
72
|
-
const server = entry?.server ?? entry;
|
|
73
|
-
if (!server || typeof server !== 'object') continue;
|
|
74
|
-
if (typeof server.name === 'string') allowedNames.add(server.name);
|
|
75
|
-
for (const remote of Array.isArray(server.remotes) ? server.remotes : []) {
|
|
76
|
-
if (typeof remote?.url === 'string') allowedUrls.add(normalizeRegistryUrl(remote.url));
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return { allowedNames, allowedUrls };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function registryAllowsConfig(config = {}, gate = {}) {
|
|
83
|
-
if (typeof config.registryName === 'string' && gate.allowedNames?.has(config.registryName)) return true;
|
|
84
|
-
if (typeof config.url === 'string' && gate.allowedUrls?.has(normalizeRegistryUrl(config.url))) return true;
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function normalizeRegistryUrl(value) {
|
|
89
|
-
try {
|
|
90
|
-
const url = new URL(value);
|
|
91
|
-
url.hash = '';
|
|
92
|
-
return url.href.replace(/\/$/, '');
|
|
93
|
-
} catch {
|
|
94
|
-
return String(value).replace(/\/$/, '');
|
|
95
|
-
}
|
|
96
|
-
}
|