@opencoven/coven-code 0.0.4 → 0.0.7

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 (115) hide show
  1. package/README.md +29 -130
  2. package/bin/coven-code +26 -0
  3. package/install.js +117 -0
  4. package/package.json +25 -22
  5. package/bin/coven-code-sdk.mjs +0 -12
  6. package/bin/coven-code.mjs +0 -19
  7. package/docs/CLI.md +0 -256
  8. package/docs/CONFIGURATION.md +0 -107
  9. package/docs/DEMO.md +0 -453
  10. package/docs/DEVELOPMENT.md +0 -104
  11. package/docs/DOGFOOD-PROTOCOL.md +0 -263
  12. package/docs/MCP-SKILLS-PLUGINS.md +0 -127
  13. package/docs/README.md +0 -39
  14. package/docs/RELEASE.md +0 -33
  15. package/docs/SDK.md +0 -107
  16. package/docs/superpowers/plans/2026-05-25-coven-code-panel-tui.md +0 -904
  17. package/docs/superpowers/plans/2026-05-25-coven-code-rebrand.md +0 -670
  18. package/docs/superpowers/specs/2026-05-25-coven-code-panel-tui-design.md +0 -235
  19. package/docs/superpowers/specs/2026-05-26-slash-first-tui-review.md +0 -63
  20. package/src/agent/fixture.mjs +0 -95
  21. package/src/agent/lane.mjs +0 -136
  22. package/src/cli/dispatch.mjs +0 -66
  23. package/src/cli/execute.mjs +0 -452
  24. package/src/cli/help.mjs +0 -58
  25. package/src/cli/interactive-core.mjs +0 -28
  26. package/src/cli/interactive-io.mjs +0 -101
  27. package/src/cli/interactive-slash.mjs +0 -184
  28. package/src/cli/notifications.mjs +0 -13
  29. package/src/cli/parse.mjs +0 -83
  30. package/src/cli/reasoning.mjs +0 -45
  31. package/src/cli/refs.mjs +0 -162
  32. package/src/cli/repl.mjs +0 -60
  33. package/src/cli/slash-commands.mjs +0 -375
  34. package/src/cli/stream-json.mjs +0 -225
  35. package/src/cli/tui-actions.mjs +0 -72
  36. package/src/cli/tui-blessed.mjs +0 -198
  37. package/src/cli/tui-keys.mjs +0 -80
  38. package/src/cli/tui-lane.mjs +0 -73
  39. package/src/cli/tui-render.mjs +0 -169
  40. package/src/cli/tui-submit.mjs +0 -82
  41. package/src/cli/tui.mjs +0 -174
  42. package/src/commands/agents.mjs +0 -53
  43. package/src/commands/config.mjs +0 -27
  44. package/src/commands/ide.mjs +0 -17
  45. package/src/commands/login.mjs +0 -84
  46. package/src/commands/mcp.mjs +0 -176
  47. package/src/commands/permissions-eval.mjs +0 -122
  48. package/src/commands/permissions-rules.mjs +0 -53
  49. package/src/commands/permissions-text.mjs +0 -112
  50. package/src/commands/permissions.mjs +0 -62
  51. package/src/commands/plugins.mjs +0 -86
  52. package/src/commands/review.mjs +0 -74
  53. package/src/commands/skill.mjs +0 -23
  54. package/src/commands/threads.mjs +0 -165
  55. package/src/commands/tools.mjs +0 -77
  56. package/src/commands/update.mjs +0 -31
  57. package/src/commands/usage.mjs +0 -34
  58. package/src/constants.mjs +0 -52
  59. package/src/main.mjs +0 -87
  60. package/src/mcp/discover.mjs +0 -154
  61. package/src/mcp/local.mjs +0 -55
  62. package/src/mcp/parsers.mjs +0 -46
  63. package/src/mcp/permissions.mjs +0 -52
  64. package/src/mcp/probe.mjs +0 -85
  65. package/src/mcp/registry.mjs +0 -96
  66. package/src/mcp/remote-oauth.mjs +0 -55
  67. package/src/mcp/remote-session.mjs +0 -54
  68. package/src/mcp/remote-sse.mjs +0 -82
  69. package/src/mcp/remote.mjs +0 -74
  70. package/src/plugins/api.mjs +0 -187
  71. package/src/plugins/configuration.mjs +0 -124
  72. package/src/plugins/discover.mjs +0 -84
  73. package/src/plugins/helpers.mjs +0 -187
  74. package/src/plugins/subsystems.mjs +0 -198
  75. package/src/plugins/validators.mjs +0 -142
  76. package/src/sdk-execute.mjs +0 -82
  77. package/src/sdk-install.mjs +0 -187
  78. package/src/sdk-settings.mjs +0 -88
  79. package/src/sdk.mjs +0 -163
  80. package/src/settings/load.mjs +0 -134
  81. package/src/settings/paths.mjs +0 -101
  82. package/src/skills/builtin/building-skills/SKILL.md +0 -20
  83. package/src/skills/discover.mjs +0 -95
  84. package/src/threads/store.mjs +0 -176
  85. package/src/tools/builtin/bash.mjs +0 -110
  86. package/src/tools/builtin/create-file.mjs +0 -66
  87. package/src/tools/builtin/edit-file.mjs +0 -76
  88. package/src/tools/builtin/finder.mjs +0 -73
  89. package/src/tools/builtin/glob.mjs +0 -74
  90. package/src/tools/builtin/grep.mjs +0 -82
  91. package/src/tools/builtin/index.mjs +0 -83
  92. package/src/tools/builtin/librarian.mjs +0 -97
  93. package/src/tools/builtin/look-at.mjs +0 -92
  94. package/src/tools/builtin/mcp.mjs +0 -51
  95. package/src/tools/builtin/mermaid.mjs +0 -59
  96. package/src/tools/builtin/oracle.mjs +0 -56
  97. package/src/tools/builtin/painter.mjs +0 -81
  98. package/src/tools/builtin/plugin-tool.mjs +0 -53
  99. package/src/tools/builtin/read-mcp-resource.mjs +0 -63
  100. package/src/tools/builtin/read-web-page.mjs +0 -72
  101. package/src/tools/builtin/read.mjs +0 -59
  102. package/src/tools/builtin/runtime-content.mjs +0 -31
  103. package/src/tools/builtin/runtime-decisions.mjs +0 -115
  104. package/src/tools/builtin/runtime.mjs +0 -85
  105. package/src/tools/builtin/task.mjs +0 -63
  106. package/src/tools/builtin/toolbox-tool.mjs +0 -57
  107. package/src/tools/builtin/undo-edit.mjs +0 -97
  108. package/src/tools/builtin/web-search.mjs +0 -128
  109. package/src/tools/toolbox.mjs +0 -273
  110. package/src/util/fs.mjs +0 -13
  111. package/src/util/glob.mjs +0 -46
  112. package/src/util/html.mjs +0 -21
  113. package/src/util/media.mjs +0 -13
  114. package/src/util/shell.mjs +0 -24
  115. package/src/util/table.mjs +0 -11
package/src/cli/tui.mjs DELETED
@@ -1,174 +0,0 @@
1
- import { runInteractive } from './repl.mjs';
2
- import { VERSION } from '../constants.mjs';
3
- import { createInteractiveSession } from './interactive-core.mjs';
4
- import { defaultLaneState } from '../agent/lane.mjs';
5
- import { displayCwd } from '../util/fs.mjs';
6
- import {
7
- buildStaticSlashCommandCatalog,
8
- filterSlashCommands,
9
- } from './slash-commands.mjs';
10
- import {
11
- TABS,
12
- buildPanelSummaries,
13
- renderCompactStatus,
14
- renderComposerLines,
15
- renderSlashOverlay,
16
- renderTabContent,
17
- renderTabLine,
18
- } from './tui-render.mjs';
19
- import { runLiveTui } from './tui-blessed.mjs';
20
- import {
21
- closeSlashMenu,
22
- insertComposerText,
23
- safeBuildSlashCommandCatalog,
24
- updateSlashState,
25
- } from './tui-actions.mjs';
26
- import { submitTuiText } from './tui-submit.mjs';
27
- import {
28
- handleComposerKey,
29
- handlePaletteKey,
30
- handleSlashMenuKey,
31
- } from './tui-keys.mjs';
32
-
33
- export async function runTuiInteractive(parsed, initialInput = '') {
34
- const session = createInteractiveSession(parsed, { silent: true });
35
- const slashCatalog = await safeBuildSlashCommandCatalog(parsed);
36
- const model = createTuiModel({
37
- mode: parsed.mode,
38
- reasoningEffort: parsed.reasoningEffort,
39
- slashCatalog,
40
- parsed,
41
- });
42
- if (process.env.COVEN_CODE_TUI_SCRIPTED === '1') {
43
- for (const line of initialInput.split(/\r?\n/)) {
44
- const text = line.trim();
45
- if (!text) continue;
46
- await submitTuiText(model, session, text);
47
- if (model.status === 'done') break;
48
- }
49
- console.log(renderTuiFrame(model, { columns: process.stdout.columns ?? 80, rows: process.stdout.rows ?? 24 }));
50
- return;
51
- }
52
- if (!process.stdin.isTTY || !process.stdout.isTTY) return runInteractive(parsed, initialInput);
53
- return runLiveTui(model, session, handleTuiKey);
54
- }
55
-
56
- export function createTuiModel(options = {}) {
57
- const slashCatalog = options.slashCatalog ?? buildStaticSlashCommandCatalog();
58
- const composer = options.composer ?? '';
59
- const workspaceCwd = options.cwd ?? process.cwd();
60
- const model = {
61
- version: options.version ?? VERSION,
62
- cwd: displayCwd(workspaceCwd),
63
- workspaceCwd,
64
- mode: options.mode ?? 'smart',
65
- reasoningEffort: options.reasoningEffort ?? 'high',
66
- threadId: options.threadId ?? 'new thread',
67
- toolCount: options.toolCount ?? 0,
68
- queueCount: options.queueCount ?? 0,
69
- activeTab: options.activeTab ?? 'chat',
70
- paletteOpen: false,
71
- paletteIndex: 0,
72
- composer,
73
- composerCursor: options.composerCursor ?? composer.length,
74
- multiline: false,
75
- slashCatalog,
76
- slashOpen: false,
77
- slashIndex: 0,
78
- slashQuery: '',
79
- slashMatches: filterSlashCommands(slashCatalog, ''),
80
- transcript: [],
81
- lane: defaultLaneState({ harness: options.mode ?? 'smart', ...options.lane }),
82
- status: 'idle',
83
- panels: options.panels ?? buildPanelSummaries(options.parsed, slashCatalog, workspaceCwd),
84
- };
85
- updateSlashState(model);
86
- return model;
87
- }
88
-
89
- export function renderTuiFrame(model, size = {}) {
90
- const columns = Math.max(50, size.columns ?? process.stdout.columns ?? 80);
91
- const rows = Math.max(16, size.rows ?? process.stdout.rows ?? 24);
92
- const divider = '-'.repeat(columns);
93
- const header = [
94
- `Coven Code ${model.version}`.slice(0, columns),
95
- `${model.cwd}`.slice(0, columns),
96
- `${renderTabLine(model)} mode: ${model.mode} effort: ${model.reasoningEffort}`.slice(0, columns),
97
- divider,
98
- ];
99
- const status = renderCompactStatus(model).slice(0, columns);
100
- const composerLines = renderComposerLines(model, columns);
101
- const slashLines = model.slashOpen ? renderSlashOverlay(model, columns, Math.min(10, Math.max(4, rows - 10))) : [];
102
- const footer = [
103
- divider,
104
- ...composerLines,
105
- ...slashLines,
106
- status,
107
- ];
108
- const bodyRows = Math.max(1, rows - header.length - footer.length);
109
- const body = renderTabContent(model, bodyRows, columns);
110
- return [
111
- ...header,
112
- ...body,
113
- ...footer,
114
- ].slice(0, rows).join('\n');
115
- }
116
-
117
- export async function handleTuiKey(model, session, key = {}) {
118
- if (key.ctrl && key.name === 'c') {
119
- model.status = 'done';
120
- return;
121
- }
122
-
123
- if (handleComposerKey(model, key)) return;
124
- if (model.slashOpen && await handleSlashMenuKey(model, session, key)) return;
125
-
126
- if (key.name === 'tab') {
127
- const index = TABS.indexOf(model.activeTab);
128
- model.activeTab = TABS[(index + 1) % TABS.length];
129
- return;
130
- }
131
-
132
- if (key.name === 'escape') {
133
- model.paletteOpen = false;
134
- return;
135
- }
136
-
137
- if (key.ctrl && key.name === 'p') {
138
- model.paletteOpen = true;
139
- model.paletteIndex = 0;
140
- return;
141
- }
142
-
143
- if (model.paletteOpen && await handlePaletteKey(model, session, key)) return;
144
-
145
- if (key.ctrl && key.name === 'n') {
146
- await submitTuiText(model, session, '/new');
147
- return;
148
- }
149
-
150
- if (key.ctrl && key.name === 'r') {
151
- await submitTuiText(model, session, '/reasoning next');
152
- return;
153
- }
154
-
155
- if (key.ctrl && key.name === 'm') {
156
- const next = session.parsed.mode === 'smart' ? 'deep' : session.parsed.mode === 'deep' ? 'rush' : 'smart';
157
- await submitTuiText(model, session, `/mode ${next}`);
158
- return;
159
- }
160
-
161
- if ((key.meta || key.shift) && key.name === 'enter') {
162
- insertComposerText(model, '\n');
163
- model.multiline = true;
164
- return;
165
- }
166
-
167
- if (key.name === 'enter') {
168
- const text = model.composer.trim();
169
- model.composer = '';
170
- model.composerCursor = 0;
171
- closeSlashMenu(model);
172
- await submitTuiText(model, session, text);
173
- }
174
- }
@@ -1,53 +0,0 @@
1
- import { existsSync } from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import { CONFIG_SUBDIR } from '../constants.mjs';
5
- import { configDir } from '../settings/paths.mjs';
6
- import { UsageError } from '../cli/parse.mjs';
7
-
8
- export async function runAgents(args) {
9
- if ((args[0] ?? 'list') !== 'list') throw new UsageError(`Unknown agents command: ${args[0] ?? ''}`);
10
- for (const filePath of discoverAgentFiles(process.cwd())) {
11
- console.log(filePath);
12
- }
13
- }
14
-
15
- export function discoverAgentFiles(cwd) {
16
- const files = [];
17
- addFirstGuidanceInDir(files, path.join(configDir(), CONFIG_SUBDIR));
18
- addFirstGuidanceInDir(files, configDir());
19
- addFirstGuidanceInDir(files, path.join(os.homedir(), '.config', CONFIG_SUBDIR));
20
- addFirstGuidanceInDir(files, path.join(os.homedir(), '.config'));
21
- addFirstGuidanceInDir(files, '/etc/coven-code');
22
- addFirstGuidanceInDir(files, '/Library/Application Support/coven-code');
23
- if (process.env.ProgramData) addFirstGuidanceInDir(files, path.join(process.env.ProgramData, CONFIG_SUBDIR));
24
- if (process.env.PROGRAMDATA) addFirstGuidanceInDir(files, path.join(process.env.PROGRAMDATA, CONFIG_SUBDIR));
25
-
26
- const home = os.homedir();
27
- let current = path.resolve(cwd);
28
- while (true) {
29
- addFirstGuidanceInDir(files, current);
30
- if (current === home || current === path.dirname(current)) break;
31
- current = path.dirname(current);
32
- }
33
-
34
- return [...new Set(files)];
35
- }
36
-
37
- function addFirstGuidanceInDir(files, dir) {
38
- for (const name of ['AGENTS.md', 'AGENT.md', 'CLAUDE.md']) {
39
- const filePath = path.join(dir, name);
40
- if (existsSync(filePath)) {
41
- files.push(filePath);
42
- return;
43
- }
44
- }
45
- }
46
-
47
- export function firstGuidanceInDir(dir) {
48
- for (const name of ['AGENTS.md', 'AGENT.md', 'CLAUDE.md']) {
49
- const filePath = path.join(dir, name);
50
- if (existsSync(filePath)) return filePath;
51
- }
52
- return undefined;
53
- }
@@ -1,27 +0,0 @@
1
- import { spawnSync } from 'node:child_process';
2
- import { existsSync } from 'node:fs';
3
- import { findWorkspaceSettingsFile, settingsFile, workspaceSettingsFile } from '../settings/paths.mjs';
4
- import { writeSettingsFile } from '../settings/load.mjs';
5
- import { shellQuote } from '../util/shell.mjs';
6
- import { UsageError } from '../cli/parse.mjs';
7
-
8
- export async function runConfig(args, parsed = {}) {
9
- const subcommand = args[0] ?? 'edit';
10
- if (subcommand !== 'edit') throw new UsageError(`Unknown config command: ${subcommand}`);
11
-
12
- const workspace = args.includes('--workspace');
13
- const filePath = workspace
14
- ? findWorkspaceSettingsFile(process.cwd()) ?? workspaceSettingsFile(process.cwd())
15
- : settingsFile(parsed);
16
- if (!existsSync(filePath)) await writeSettingsFile(filePath, {});
17
-
18
- const editor = process.env.EDITOR || process.env.VISUAL;
19
- if (!editor) throw new UsageError('config edit requires $EDITOR or $VISUAL');
20
-
21
- const result = spawnSync(`${editor} ${shellQuote(filePath)}`, {
22
- stdio: 'inherit',
23
- shell: true,
24
- });
25
- if (result.error) throw new UsageError(`Unable to run editor: ${result.error.message}`);
26
- if ((result.status ?? 0) !== 0) throw new UsageError(`Editor exited with status ${result.status}`);
27
- }
@@ -1,17 +0,0 @@
1
- import { UsageError } from '../cli/parse.mjs';
2
-
3
- export function runIde(args = []) {
4
- const subcommand = args[0] ?? 'connect';
5
- if (subcommand !== 'connect') throw new UsageError(`Unknown ide command: ${subcommand}`);
6
- runIdeConnect(args[1] ?? 'auto');
7
- }
8
-
9
- export function runIdeConnect(name) {
10
- console.log(`ide: ${name}`);
11
- console.log('status: unavailable (local recreation)');
12
- if (name === 'jetbrains') {
13
- console.log('hint: run Coven Code from the JetBrains terminal or install the Coven Code IDE plugin');
14
- } else {
15
- console.log('hint: run Coven Code from a supported IDE terminal or install the Coven Code IDE plugin');
16
- }
17
- }
@@ -1,84 +0,0 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
- import { chmod, mkdir, rm, writeFile } from 'node:fs/promises';
3
- import path from 'node:path';
4
- import { UsageError } from '../cli/parse.mjs';
5
- import { configDir } from '../settings/paths.mjs';
6
-
7
- const API_KEY_ENV = 'COVEN_CODE_API_KEY';
8
- const CONFIG_SUBDIR = 'coven-code';
9
-
10
- export async function runLogin(args = []) {
11
- const subcommand = args[0] ?? '';
12
- if (subcommand === 'status') {
13
- printLoginStatus();
14
- return;
15
- }
16
- if (subcommand === 'logout') {
17
- await rm(authFile(), { force: true });
18
- console.log('Logged out locally');
19
- return;
20
- }
21
- if (subcommand) throw new UsageError(`Unknown login command: ${subcommand}`);
22
-
23
- const apiKey = envApiKey();
24
- if (!apiKey) {
25
- console.log('Create or retrieve a Coven Code access token.');
26
- console.log(`Then run: export ${API_KEY_ENV}=<token>`);
27
- console.log(`Or set ${API_KEY_ENV} and run \`coven-code login\` to store it locally.`);
28
- return;
29
- }
30
-
31
- await writeAuth({ accessToken: apiKey.token, source: apiKey.source });
32
- console.log(`Logged in with ${apiKey.source} (${maskToken(apiKey.token)})`);
33
- }
34
-
35
- function printLoginStatus() {
36
- const apiKey = envApiKey();
37
- if (apiKey) {
38
- console.log('auth_status: logged_in');
39
- console.log(`source: ${apiKey.source}`);
40
- console.log(`token: ${maskToken(apiKey.token)}`);
41
- return;
42
- }
43
- const auth = readAuth();
44
- if (auth?.accessToken) {
45
- console.log('auth_status: logged_in');
46
- console.log(`source: ${auth.source ?? 'local'}`);
47
- console.log(`token: ${maskToken(auth.accessToken)}`);
48
- return;
49
- }
50
- console.log('auth_status: logged_out');
51
- console.log('source: none');
52
- }
53
-
54
- async function writeAuth(auth) {
55
- const filePath = authFile();
56
- await mkdir(path.dirname(filePath), { recursive: true });
57
- await writeFile(filePath, `${JSON.stringify(auth, null, 2)}\n`, 'utf8');
58
- await chmod(filePath, 0o600);
59
- }
60
-
61
- function readAuth() {
62
- const filePath = authFile();
63
- if (!existsSync(filePath)) return undefined;
64
- try {
65
- return JSON.parse(readFileSync(filePath, 'utf8'));
66
- } catch {
67
- return undefined;
68
- }
69
- }
70
-
71
- function envApiKey() {
72
- const token = process.env[API_KEY_ENV]?.trim();
73
- if (token) return { token, source: API_KEY_ENV };
74
- return undefined;
75
- }
76
-
77
- function authFile(subdir = CONFIG_SUBDIR) {
78
- return path.join(configDir(), subdir, 'auth.json');
79
- }
80
-
81
- function maskToken(token = '') {
82
- if (token.length <= 8) return '<redacted>';
83
- return `${token.slice(0, 13)}…${token.slice(-4)}`;
84
- }
@@ -1,176 +0,0 @@
1
- import { rm } from 'node:fs/promises';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import { settingsFile, workspaceSettingsFile } from '../settings/paths.mjs';
5
- import { readSettingsFile, writeSettingsFile } from '../settings/load.mjs';
6
- import { formatMcpServerCommand, listConfiguredMcpServers } from '../mcp/discover.mjs';
7
- import { readWorkspaceMcpApprovals, writeWorkspaceMcpApprovals } from '../mcp/permissions.mjs';
8
- import { mcpServerHealth } from '../mcp/probe.mjs';
9
- import { UsageError } from '../cli/parse.mjs';
10
- import { printRows } from '../util/table.mjs';
11
-
12
- const MCP_SERVERS_SETTING = 'covenCode.mcpServers';
13
-
14
- export async function runMcp(args, parsed = {}) {
15
- const subcommand = args[0] ?? 'list';
16
-
17
- if (subcommand === 'oauth') {
18
- await runMcpOauth(args.slice(1));
19
- return;
20
- }
21
-
22
- if (subcommand === 'add') {
23
- const workspace = args.includes('--workspace');
24
- const tokens = args.slice(1).filter((arg) => arg !== '--workspace');
25
- const separator = tokens.indexOf('--');
26
- const name = tokens[0];
27
- if (!name) throw new UsageError('mcp add requires a server name');
28
- const specArgs = separator === -1 ? tokens.slice(1) : tokens.slice(separator + 1);
29
- const settingsPath = workspace ? workspaceSettingsFile(process.cwd()) : settingsFile(parsed);
30
- const settings = readSettingsFile(settingsPath);
31
- settings[MCP_SERVERS_SETTING] = {
32
- ...(settings[MCP_SERVERS_SETTING] ?? {}),
33
- [name]: parseMcpServerSpec(specArgs),
34
- };
35
- await writeSettingsFile(settingsPath, settings);
36
- console.log(`Added MCP server ${name} to ${workspace ? 'workspace' : 'user'} settings.`);
37
- return;
38
- }
39
-
40
- if (subcommand === 'list') {
41
- const rows = listConfiguredMcpServers(parsed).map((server) => [
42
- server.name,
43
- server.source,
44
- server.status,
45
- formatMcpServerCommand(server.config),
46
- ]);
47
- printRows(rows.length ? rows : [['(none)', '-', '-', '-']]);
48
- return;
49
- }
50
-
51
- if (subcommand === 'doctor') {
52
- const rows = [];
53
- for (const server of listConfiguredMcpServers(parsed)) {
54
- rows.push([
55
- server.name,
56
- server.source,
57
- server.status,
58
- server.status === 'approved' ? await mcpServerHealth(server.config, server.name) : 'not probed',
59
- formatMcpServerCommand(server.config),
60
- ]);
61
- }
62
- printRows(rows.length ? rows : [['(none)', '-', '-', '-', '-']]);
63
- return;
64
- }
65
-
66
- if (subcommand === 'approve') {
67
- const name = args[1];
68
- if (!name) throw new UsageError('mcp approve requires a server name');
69
- const approvals = readWorkspaceMcpApprovals(process.cwd());
70
- approvals[name] = true;
71
- await writeWorkspaceMcpApprovals(process.cwd(), approvals);
72
- console.log(`Approved workspace MCP server ${name}.`);
73
- return;
74
- }
75
-
76
- throw new UsageError(`Unknown mcp command: ${subcommand}`);
77
- }
78
-
79
- async function runMcpOauth(args) {
80
- const subcommand = args[0] ?? '';
81
- if (subcommand === 'login') {
82
- const parsed = parseMcpOauthLoginArgs(args.slice(1));
83
- await writeSettingsFile(mcpOauthCredentialFile(parsed.name), {
84
- serverUrl: parsed.serverUrl,
85
- clientId: parsed.clientId,
86
- clientSecret: parsed.clientSecret,
87
- scopes: parsed.scopes,
88
- });
89
- console.log(`Stored OAuth credentials for ${parsed.name}.`);
90
- return;
91
- }
92
-
93
- if (subcommand === 'logout') {
94
- const name = args[1];
95
- if (!name) throw new UsageError('mcp oauth logout requires a server name');
96
- await rm(mcpOauthCredentialFile(name), { force: true });
97
- console.log(`Removed OAuth credentials for ${name}.`);
98
- return;
99
- }
100
-
101
- throw new UsageError(`Unknown mcp oauth command: ${subcommand}`);
102
- }
103
-
104
- function parseMcpOauthLoginArgs(args) {
105
- const name = args[0];
106
- if (!name || name.startsWith('-')) throw new UsageError('mcp oauth login requires a server name');
107
- const flags = parseFlagPairs(args.slice(1));
108
- const serverUrl = flags.get('server-url') ?? flags.get('serverUrl');
109
- const clientId = flags.get('client-id') ?? flags.get('clientId');
110
- const clientSecret = flags.get('client-secret') ?? flags.get('clientSecret');
111
- if (!serverUrl) throw new UsageError('mcp oauth login requires --server-url');
112
- if (!clientId) throw new UsageError('mcp oauth login requires --client-id');
113
- if (!clientSecret) throw new UsageError('mcp oauth login requires --client-secret');
114
- return {
115
- name,
116
- serverUrl,
117
- clientId,
118
- clientSecret,
119
- scopes: splitScopes(flags.get('scopes') ?? ''),
120
- };
121
- }
122
-
123
- function parseFlagPairs(args) {
124
- const flags = new Map();
125
- for (let index = 0; index < args.length; index += 1) {
126
- const key = args[index];
127
- if (!key.startsWith('--')) continue;
128
- flags.set(key.slice(2), args[index + 1] ?? '');
129
- index += 1;
130
- }
131
- return flags;
132
- }
133
-
134
- function splitScopes(value) {
135
- return String(value).split(/[,\s]+/).map((scope) => scope.trim()).filter(Boolean);
136
- }
137
-
138
- function mcpOauthCredentialFile(name) {
139
- return path.join(os.homedir(), '.coven-code', 'oauth', `${name}.json`);
140
- }
141
-
142
- function parseMcpServerSpec(args) {
143
- if (isRemoteMcpServerSpec(args)) return parseRemoteMcpServerSpec(args);
144
- const [command, ...commandArgs] = args;
145
- if (!command) throw new UsageError('mcp add requires a command or URL after --');
146
- return { command, args: commandArgs };
147
- }
148
-
149
- function isRemoteMcpServerSpec(args) {
150
- return /^https?:\/\//.test(args[0] ?? '') || args[0] === '--header';
151
- }
152
-
153
- function parseRemoteMcpServerSpec(args) {
154
- const headers = {};
155
- const endpoints = [];
156
- for (let index = 0; index < args.length; index += 1) {
157
- const arg = args[index];
158
- if (arg === '--header') {
159
- const header = args[index + 1];
160
- if (!header) throw new UsageError('mcp add --header requires NAME=VALUE');
161
- const separator = header.indexOf('=');
162
- if (separator <= 0) throw new UsageError('mcp add --header requires NAME=VALUE');
163
- headers[header.slice(0, separator).trim()] = header.slice(separator + 1).trim();
164
- index += 1;
165
- } else {
166
- endpoints.push(arg);
167
- }
168
- }
169
- if (endpoints.length !== 1 || !/^https?:\/\//.test(endpoints[0])) {
170
- throw new UsageError('mcp add remote servers require exactly one HTTP URL');
171
- }
172
- return {
173
- url: endpoints[0],
174
- ...(Object.keys(headers).length ? { headers } : {}),
175
- };
176
- }
@@ -1,122 +0,0 @@
1
- import { spawnSync } from 'node:child_process';
2
- import { readEffectiveSettings } from '../settings/load.mjs';
3
- import { globMatch } from '../util/glob.mjs';
4
- import {
5
- COMMAND_ALLOWLIST_SETTING,
6
- DANGEROUSLY_ALLOW_ALL_SETTING,
7
- GUARDED_FILES_ALLOWLIST_SETTING,
8
- PERMISSIONS_SETTING,
9
- builtinPermissionRules,
10
- commandAllowlistPermissionRules,
11
- isUndefinedMatchValue,
12
- loadUserPermissionRules,
13
- } from './permissions-rules.mjs';
14
-
15
- export function evaluatePermission(tool, toolArgs, parsed = {}, options = {}) {
16
- const ruleGroups = [
17
- { source: 'user', rules: loadUserPermissionRules(parsed) },
18
- { source: 'command-allowlist', rules: commandAllowlistPermissionRules(parsed) },
19
- {
20
- source: 'built-in',
21
- rules: builtinPermissionRules(),
22
- },
23
- ];
24
-
25
- for (const group of ruleGroups) {
26
- for (const [index, rule] of group.rules.entries()) {
27
- if (!globMatch(rule.tool, tool)) continue;
28
- if (!matchesContext(rule.context, options.context ?? 'thread')) continue;
29
- if (!matchesArguments(rule.matches, toolArgs)) continue;
30
- return {
31
- action: rule.action,
32
- to: rule.to,
33
- message: rule.message,
34
- matchedRule: index,
35
- source: group.source,
36
- };
37
- }
38
- }
39
-
40
- return { action: 'reject', source: 'default' };
41
- }
42
-
43
- export function resolvePermissionDecision(tool, toolArgs, parsed = {}, options = {}) {
44
- const settings = readEffectiveSettings(parsed);
45
- if (isDangerouslyAllowAll(parsed, settings)) return { action: 'allow', source: 'dangerously-allow-all' };
46
- if (!legacyPermissionsConfigured(settings)) return { action: 'allow', source: 'default-no-approval' };
47
- const decision = evaluatePermission(tool, toolArgs, parsed, options);
48
- if (decision.action !== 'delegate') return decision;
49
- if (!decision.to) {
50
- return { ...decision, action: 'reject', message: 'Delegate permission rule is missing a target program' };
51
- }
52
-
53
- const result = spawnSync(decision.to, {
54
- input: `${JSON.stringify(toolArgs)}\n`,
55
- env: {
56
- ...process.env,
57
- AGENT: 'coven-code',
58
- AGENT_TOOL_NAME: tool,
59
- COVEN_CODE_THREAD_ID: options.threadId ?? '',
60
- AGENT_THREAD_ID: options.threadId ?? '',
61
- },
62
- encoding: 'utf8',
63
- shell: false,
64
- });
65
-
66
- if (result.status === 0) return { ...decision, action: 'allow', delegatedAction: 'allow' };
67
- if (result.status === 1) return { ...decision, action: 'ask', delegatedAction: 'ask' };
68
- return {
69
- ...decision,
70
- action: 'reject',
71
- delegatedAction: 'reject',
72
- message: result.stderr.trim() || result.error?.message || `Permission delegate ${decision.to} rejected the tool call`,
73
- };
74
- }
75
-
76
- function isDangerouslyAllowAll(parsed = {}, settings = readEffectiveSettings(parsed)) {
77
- if (parsed.dangerouslyAllowAll) return true;
78
- return settings[DANGEROUSLY_ALLOW_ALL_SETTING] === true;
79
- }
80
-
81
- function legacyPermissionsConfigured(settings = {}) {
82
- return Object.hasOwn(settings, PERMISSIONS_SETTING)
83
- || Object.hasOwn(settings, GUARDED_FILES_ALLOWLIST_SETTING)
84
- || Object.hasOwn(settings, COMMAND_ALLOWLIST_SETTING)
85
- || settings[DANGEROUSLY_ALLOW_ALL_SETTING] === false;
86
- }
87
-
88
- function matchesContext(ruleContext, actualContext) {
89
- return !ruleContext || ruleContext === actualContext;
90
- }
91
-
92
- function matchesArguments(matches, toolArgs) {
93
- if (!matches || Object.keys(matches).length === 0) return true;
94
- for (const [key, pattern] of Object.entries(matches)) {
95
- const actual = valueAtPath(toolArgs, key);
96
- if (!matchesPattern(pattern, actual)) return false;
97
- }
98
- return true;
99
- }
100
-
101
- function matchesPattern(pattern, actual) {
102
- if (isUndefinedMatchValue(pattern)) return actual === undefined;
103
- if (Array.isArray(pattern)) return pattern.some((entry) => matchesPattern(entry, actual));
104
- if (pattern && typeof pattern === 'object') {
105
- if (!actual || typeof actual !== 'object') return false;
106
- return Object.entries(pattern).every(([key, value]) => matchesPattern(value, valueAtPath(actual, key)));
107
- }
108
- if (typeof pattern === 'string') {
109
- if (typeof actual !== 'string') return false;
110
- if (isRegexPattern(pattern)) return new RegExp(pattern.slice(1, -1)).test(actual);
111
- return globMatch(pattern, actual);
112
- }
113
- return Object.is(pattern, actual);
114
- }
115
-
116
- function isRegexPattern(value) {
117
- return value.length >= 2 && value.startsWith('/') && value.endsWith('/');
118
- }
119
-
120
- function valueAtPath(value, key) {
121
- return key.split('.').reduce((current, part) => current?.[part], value);
122
- }