@opencoven/coven-code 0.0.1

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 (86) hide show
  1. package/README.md +145 -0
  2. package/bin/coven-code-sdk.mjs +12 -0
  3. package/bin/coven-code.mjs +19 -0
  4. package/docs/CLI.md +192 -0
  5. package/docs/CONFIGURATION.md +107 -0
  6. package/docs/DEVELOPMENT.md +104 -0
  7. package/docs/DOGFOOD-PROTOCOL.md +263 -0
  8. package/docs/MCP-SKILLS-PLUGINS.md +127 -0
  9. package/docs/README.md +38 -0
  10. package/docs/RELEASE.md +33 -0
  11. package/docs/SDK.md +107 -0
  12. package/docs/superpowers/plans/2026-05-25-coven-code-panel-tui.md +904 -0
  13. package/docs/superpowers/plans/2026-05-25-coven-code-rebrand.md +670 -0
  14. package/docs/superpowers/specs/2026-05-25-coven-code-panel-tui-design.md +235 -0
  15. package/docs/superpowers/specs/2026-05-26-slash-first-tui-review.md +63 -0
  16. package/package.json +36 -0
  17. package/src/agent/lane.mjs +136 -0
  18. package/src/agent/local.mjs +95 -0
  19. package/src/cli/dispatch.mjs +66 -0
  20. package/src/cli/execute.mjs +588 -0
  21. package/src/cli/help.mjs +58 -0
  22. package/src/cli/interactive-core.mjs +302 -0
  23. package/src/cli/notifications.mjs +13 -0
  24. package/src/cli/parse.mjs +83 -0
  25. package/src/cli/reasoning.mjs +45 -0
  26. package/src/cli/refs.mjs +162 -0
  27. package/src/cli/repl.mjs +61 -0
  28. package/src/cli/slash-commands.mjs +357 -0
  29. package/src/cli/stream-json.mjs +116 -0
  30. package/src/cli/tui.mjs +757 -0
  31. package/src/commands/agents.mjs +53 -0
  32. package/src/commands/config.mjs +27 -0
  33. package/src/commands/ide.mjs +17 -0
  34. package/src/commands/login.mjs +84 -0
  35. package/src/commands/mcp.mjs +176 -0
  36. package/src/commands/permissions.mjs +328 -0
  37. package/src/commands/plugins.mjs +86 -0
  38. package/src/commands/review.mjs +74 -0
  39. package/src/commands/skill.mjs +23 -0
  40. package/src/commands/threads.mjs +165 -0
  41. package/src/commands/tools.mjs +77 -0
  42. package/src/commands/update.mjs +31 -0
  43. package/src/commands/usage.mjs +34 -0
  44. package/src/constants.mjs +46 -0
  45. package/src/main.mjs +87 -0
  46. package/src/mcp/discover.mjs +154 -0
  47. package/src/mcp/permissions.mjs +52 -0
  48. package/src/mcp/probe.mjs +424 -0
  49. package/src/mcp/registry.mjs +96 -0
  50. package/src/plugins/discover.mjs +880 -0
  51. package/src/sdk-install.mjs +187 -0
  52. package/src/sdk.mjs +314 -0
  53. package/src/settings/load.mjs +134 -0
  54. package/src/settings/paths.mjs +101 -0
  55. package/src/skills/builtin/building-skills/SKILL.md +20 -0
  56. package/src/skills/discover.mjs +95 -0
  57. package/src/threads/store.mjs +176 -0
  58. package/src/tools/builtin/bash.mjs +110 -0
  59. package/src/tools/builtin/create-file.mjs +66 -0
  60. package/src/tools/builtin/edit-file.mjs +76 -0
  61. package/src/tools/builtin/finder.mjs +73 -0
  62. package/src/tools/builtin/glob.mjs +74 -0
  63. package/src/tools/builtin/grep.mjs +82 -0
  64. package/src/tools/builtin/index.mjs +83 -0
  65. package/src/tools/builtin/librarian.mjs +97 -0
  66. package/src/tools/builtin/look-at.mjs +92 -0
  67. package/src/tools/builtin/mcp.mjs +51 -0
  68. package/src/tools/builtin/mermaid.mjs +59 -0
  69. package/src/tools/builtin/oracle.mjs +56 -0
  70. package/src/tools/builtin/painter.mjs +81 -0
  71. package/src/tools/builtin/plugin-tool.mjs +53 -0
  72. package/src/tools/builtin/read-mcp-resource.mjs +63 -0
  73. package/src/tools/builtin/read-web-page.mjs +72 -0
  74. package/src/tools/builtin/read.mjs +59 -0
  75. package/src/tools/builtin/runtime.mjs +215 -0
  76. package/src/tools/builtin/task.mjs +63 -0
  77. package/src/tools/builtin/toolbox-tool.mjs +57 -0
  78. package/src/tools/builtin/undo-edit.mjs +97 -0
  79. package/src/tools/builtin/web-search.mjs +128 -0
  80. package/src/tools/toolbox.mjs +273 -0
  81. package/src/util/fs.mjs +13 -0
  82. package/src/util/glob.mjs +46 -0
  83. package/src/util/html.mjs +21 -0
  84. package/src/util/media.mjs +13 -0
  85. package/src/util/shell.mjs +24 -0
  86. package/src/util/table.mjs +11 -0
@@ -0,0 +1,53 @@
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
+ }
@@ -0,0 +1,27 @@
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
+ }
@@ -0,0 +1,17 @@
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
+ }
@@ -0,0 +1,84 @@
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
+ }
@@ -0,0 +1,176 @@
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
+ }
@@ -0,0 +1,328 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { BUILTIN_PERMISSIONS } from '../constants.mjs';
3
+ import { readSettings, readEffectiveSettings, writeSettings } from '../settings/load.mjs';
4
+ import { globMatch } from '../util/glob.mjs';
5
+ import { shellQuote, splitShellWords } from '../util/shell.mjs';
6
+ import { UsageError } from '../cli/parse.mjs';
7
+
8
+ const PERMISSIONS_SETTING = 'covenCode.permissions';
9
+ const COMMAND_ALLOWLIST_SETTING = 'covenCode.commands.allowlist';
10
+ const DANGEROUSLY_ALLOW_ALL_SETTING = 'covenCode.dangerouslyAllowAll';
11
+ const GUARDED_FILES_ALLOWLIST_SETTING = 'covenCode.guardedFiles.allowlist';
12
+ const UNDEFINED_MATCH_VALUE = Object.freeze({ __covenCodeLiteral: 'undefined' });
13
+
14
+ export async function runPermissions(args, stdin = '', parsed = {}) {
15
+ const subcommand = args[0] ?? 'list';
16
+ if (subcommand === 'list') {
17
+ const rules = args.includes('--builtin')
18
+ ? builtinPermissionRules()
19
+ : [...loadUserPermissionRules(parsed), ...builtinPermissionRules()];
20
+ for (const rule of rules) console.log(formatPermissionRule(rule));
21
+ return;
22
+ }
23
+
24
+ if (subcommand === 'add') {
25
+ const settings = readSettings(parsed);
26
+ const rule = parsePermissionRule(args.slice(1));
27
+ settings[PERMISSIONS_SETTING] = [rule, ...(settings[PERMISSIONS_SETTING] ?? [])];
28
+ await writeSettings(settings, parsed);
29
+ console.log(`Added permission rule: ${rule.action} ${rule.tool}`);
30
+ return;
31
+ }
32
+
33
+ if (subcommand === 'edit') {
34
+ const settings = readSettings(parsed);
35
+ settings[PERMISSIONS_SETTING] = parsePermissionText(stdin);
36
+ await writeSettings(settings, parsed);
37
+ console.log(`Wrote ${settings[PERMISSIONS_SETTING].length} permission rule(s).`);
38
+ return;
39
+ }
40
+
41
+ if (subcommand === 'test') {
42
+ const tool = args[1];
43
+ if (!tool) throw new UsageError('permissions test requires a tool name');
44
+ const toolArgs = parseFlagMatches(args.slice(2));
45
+ const context = typeof toolArgs.context === 'string' ? toolArgs.context : 'thread';
46
+ delete toolArgs.context;
47
+ const decision = evaluatePermission(tool, toolArgs, parsed, { context });
48
+ console.log(`tool: ${tool}`);
49
+ console.log(`arguments: ${JSON.stringify(toolArgs)}`);
50
+ console.log(`action: ${decision.action}`);
51
+ if (decision.to) console.log(`to: ${decision.to}`);
52
+ if (decision.matchedRule !== undefined) console.log(`matched-rule: ${decision.matchedRule}`);
53
+ console.log(`source: ${decision.source}`);
54
+ return;
55
+ }
56
+
57
+ throw new UsageError(`Unknown permissions command: ${subcommand}`);
58
+ }
59
+
60
+ export function loadUserPermissionRules(parsed = {}) {
61
+ const settings = readEffectiveSettings(parsed);
62
+ return Array.isArray(settings[PERMISSIONS_SETTING]) ? settings[PERMISSIONS_SETTING] : [];
63
+ }
64
+
65
+ function formatPermissionRule(rule) {
66
+ const parts = [rule.action];
67
+ for (const key of ['context', 'to', 'message']) {
68
+ if (rule[key] !== undefined) parts.push(`--${key}`, formatPermissionToken(rule[key]));
69
+ }
70
+ parts.push(formatPermissionToken(rule.tool));
71
+ for (const [key, value] of flattenMatches(rule.matches)) {
72
+ parts.push(`--${key}`, formatPermissionToken(value));
73
+ }
74
+ return parts.join(' ');
75
+ }
76
+
77
+ function flattenMatches(matches, prefix = '') {
78
+ if (!matches) return [];
79
+ const entries = [];
80
+ for (const [key, value] of Object.entries(matches)) {
81
+ const fullKey = prefix ? `${prefix}.${key}` : key;
82
+ if (Array.isArray(value)) {
83
+ for (const entry of value) entries.push(...flattenMatchValue(fullKey, entry));
84
+ } else {
85
+ entries.push(...flattenMatchValue(fullKey, value));
86
+ }
87
+ }
88
+ return entries;
89
+ }
90
+
91
+ function flattenMatchValue(key, value) {
92
+ if (isUndefinedMatchValue(value)) return [[key, value]];
93
+ if (value && typeof value === 'object' && !Array.isArray(value)) return flattenMatches(value, key);
94
+ return [[key, value]];
95
+ }
96
+
97
+ function formatPermissionToken(value) {
98
+ if (isUndefinedMatchValue(value)) return 'undefined';
99
+ if (value === null) return 'null';
100
+ if (value === undefined) return 'undefined';
101
+ if (typeof value === 'boolean' || typeof value === 'number') return String(value);
102
+ const text = String(value);
103
+ return /^[A-Za-z0-9_./:@=-]+$/.test(text) ? text : shellQuote(text);
104
+ }
105
+
106
+ function parsePermissionText(input) {
107
+ return input
108
+ .split(/\r?\n/)
109
+ .map((line) => line.trim())
110
+ .filter((line) => line && !line.startsWith('#'))
111
+ .map((line) => parsePermissionRule(splitShellWords(line)));
112
+ }
113
+
114
+ function parsePermissionRule(tokens) {
115
+ const [action] = tokens;
116
+ const { flags: actionArgs, index: toolIndex } = parseActionArgs(tokens, 1);
117
+ const tool = tokens[toolIndex];
118
+ const rest = tokens.slice(toolIndex + 1);
119
+ if (!action || !tool) throw new UsageError('permission rules require: <action> <tool>');
120
+ const rule = { action, tool, ...actionArgs };
121
+ const matches = parseFlagMatches(rest, { undefinedPattern: true });
122
+ if (Object.keys(matches).length > 0) rule.matches = matches;
123
+ if (matches.to) {
124
+ rule.to = matches.to;
125
+ delete rule.matches.to;
126
+ if (Object.keys(rule.matches).length === 0) delete rule.matches;
127
+ }
128
+ return rule;
129
+ }
130
+
131
+ function parseActionArgs(tokens, startIndex) {
132
+ const flags = {};
133
+ let index = startIndex;
134
+ while (tokens[index]?.startsWith('--')) {
135
+ const key = tokens[index].slice(2);
136
+ flags[key] = parsePermissionValue(tokens[index + 1] ?? '');
137
+ index += 2;
138
+ }
139
+ return { flags, index };
140
+ }
141
+
142
+ export function parseFlagMatches(tokens, options = {}) {
143
+ return parseFlagMatchesWithOptions(tokens, options);
144
+ }
145
+
146
+ function parseFlagMatchesWithOptions(tokens, options = {}) {
147
+ const matches = {};
148
+ for (let index = 0; index < tokens.length; index += 1) {
149
+ const token = tokens[index];
150
+ if (!token.startsWith('--')) continue;
151
+ const key = token.slice(2);
152
+ const value = parsePermissionValue(tokens[index + 1] ?? '', options);
153
+ index += 1;
154
+ setMatchValue(matches, key, value);
155
+ }
156
+ return matches;
157
+ }
158
+
159
+ function parsePermissionValue(value, options = {}) {
160
+ if (value === 'undefined') return options.undefinedPattern ? { ...UNDEFINED_MATCH_VALUE } : undefined;
161
+ if (/^(?:true|false|null|-?\d+(?:\.\d+)?)$/.test(value)) return JSON.parse(value);
162
+ return value;
163
+ }
164
+
165
+ function setMatchValue(target, key, value) {
166
+ const parts = key.split('.').filter(Boolean);
167
+ let current = target;
168
+ for (const part of parts.slice(0, -1)) {
169
+ if (!current[part] || typeof current[part] !== 'object' || Array.isArray(current[part])) current[part] = {};
170
+ current = current[part];
171
+ }
172
+ const finalKey = parts.at(-1) ?? key;
173
+ if (!hasOwn(current, finalKey)) current[finalKey] = value;
174
+ else if (Array.isArray(current[finalKey])) current[finalKey].push(value);
175
+ else current[finalKey] = [current[finalKey], value];
176
+ }
177
+
178
+ export function evaluatePermission(tool, toolArgs, parsed = {}, options = {}) {
179
+ const ruleGroups = [
180
+ { source: 'user', rules: loadUserPermissionRules(parsed) },
181
+ { source: 'command-allowlist', rules: commandAllowlistPermissionRules(parsed) },
182
+ {
183
+ source: 'built-in',
184
+ rules: builtinPermissionRules(),
185
+ },
186
+ ];
187
+
188
+ for (const group of ruleGroups) {
189
+ for (const [index, rule] of group.rules.entries()) {
190
+ if (!globMatch(rule.tool, tool)) continue;
191
+ if (!matchesContext(rule.context, options.context ?? 'thread')) continue;
192
+ if (!matchesArguments(rule.matches, toolArgs)) continue;
193
+ return {
194
+ action: rule.action,
195
+ to: rule.to,
196
+ message: rule.message,
197
+ matchedRule: index,
198
+ source: group.source,
199
+ };
200
+ }
201
+ }
202
+
203
+ return { action: 'reject', source: 'default' };
204
+ }
205
+
206
+ function builtinPermissionRules() {
207
+ return BUILTIN_PERMISSIONS.map(([action, builtinTool, cmd]) => ({
208
+ action,
209
+ tool: builtinTool,
210
+ matches: builtinTool === 'Bash' ? { cmd: `${cmd}*` } : undefined,
211
+ }));
212
+ }
213
+
214
+ function commandAllowlistPermissionRules(parsed = {}) {
215
+ const allowlist = readEffectiveSettings(parsed)[COMMAND_ALLOWLIST_SETTING];
216
+ if (!Array.isArray(allowlist)) return [];
217
+ return allowlist
218
+ .map((command) => String(command).trim())
219
+ .filter(Boolean)
220
+ .map((command) => ({
221
+ action: 'allow',
222
+ tool: 'Bash',
223
+ matches: { cmd: commandAllowlistPattern(command) },
224
+ }));
225
+ }
226
+
227
+ function commandAllowlistPattern(command) {
228
+ return `/^${escapeRegex(command)}(?:\\s|$).*/`;
229
+ }
230
+
231
+ function escapeRegex(value) {
232
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
233
+ }
234
+
235
+ export function resolvePermissionDecision(tool, toolArgs, parsed = {}, options = {}) {
236
+ const settings = readEffectiveSettings(parsed);
237
+ if (isDangerouslyAllowAll(parsed, settings)) return { action: 'allow', source: 'dangerously-allow-all' };
238
+ if (!legacyPermissionsConfigured(settings)) return { action: 'allow', source: 'default-no-approval' };
239
+ const decision = evaluatePermission(tool, toolArgs, parsed, options);
240
+ if (decision.action !== 'delegate') return decision;
241
+ if (!decision.to) {
242
+ return { ...decision, action: 'reject', message: 'Delegate permission rule is missing a target program' };
243
+ }
244
+
245
+ const result = spawnSync(decision.to, {
246
+ input: `${JSON.stringify(toolArgs)}\n`,
247
+ env: {
248
+ ...process.env,
249
+ AGENT: 'coven-code',
250
+ AGENT_TOOL_NAME: tool,
251
+ COVEN_CODE_THREAD_ID: options.threadId ?? '',
252
+ AGENT_THREAD_ID: options.threadId ?? '',
253
+ },
254
+ encoding: 'utf8',
255
+ shell: false,
256
+ });
257
+
258
+ if (result.status === 0) return { ...decision, action: 'allow', delegatedAction: 'allow' };
259
+ if (result.status === 1) return { ...decision, action: 'ask', delegatedAction: 'ask' };
260
+ return {
261
+ ...decision,
262
+ action: 'reject',
263
+ delegatedAction: 'reject',
264
+ message: result.stderr.trim() || result.error?.message || `Permission delegate ${decision.to} rejected the tool call`,
265
+ };
266
+ }
267
+
268
+ function isDangerouslyAllowAll(parsed = {}, settings = readEffectiveSettings(parsed)) {
269
+ if (parsed.dangerouslyAllowAll) return true;
270
+ return settings[DANGEROUSLY_ALLOW_ALL_SETTING] === true;
271
+ }
272
+
273
+ function legacyPermissionsConfigured(settings = {}) {
274
+ return hasOwn(settings, PERMISSIONS_SETTING)
275
+ || hasOwn(settings, GUARDED_FILES_ALLOWLIST_SETTING)
276
+ || hasOwn(settings, COMMAND_ALLOWLIST_SETTING)
277
+ || settings[DANGEROUSLY_ALLOW_ALL_SETTING] === false;
278
+ }
279
+
280
+ function hasOwn(value, key) {
281
+ return Object.prototype.hasOwnProperty.call(value, key);
282
+ }
283
+
284
+ function matchesContext(ruleContext, actualContext) {
285
+ return !ruleContext || ruleContext === actualContext;
286
+ }
287
+
288
+ function matchesArguments(matches, toolArgs) {
289
+ if (!matches || Object.keys(matches).length === 0) return true;
290
+ for (const [key, pattern] of Object.entries(matches)) {
291
+ const actual = valueAtPath(toolArgs, key);
292
+ if (!matchesPattern(pattern, actual)) return false;
293
+ }
294
+ return true;
295
+ }
296
+
297
+ function matchesPattern(pattern, actual) {
298
+ if (isUndefinedMatchValue(pattern)) return actual === undefined;
299
+ if (Array.isArray(pattern)) return pattern.some((entry) => matchesPattern(entry, actual));
300
+ if (pattern && typeof pattern === 'object') {
301
+ if (!actual || typeof actual !== 'object') return false;
302
+ return Object.entries(pattern).every(([key, value]) => matchesPattern(value, valueAtPath(actual, key)));
303
+ }
304
+ if (typeof pattern === 'string') {
305
+ if (typeof actual !== 'string') return false;
306
+ if (isRegexPattern(pattern)) return new RegExp(pattern.slice(1, -1)).test(actual);
307
+ return globMatch(pattern, actual);
308
+ }
309
+ return Object.is(pattern, actual);
310
+ }
311
+
312
+ function isRegexPattern(value) {
313
+ return value.length >= 2 && value.startsWith('/') && value.endsWith('/');
314
+ }
315
+
316
+ function valueAtPath(value, key) {
317
+ return key.split('.').reduce((current, part) => current?.[part], value);
318
+ }
319
+
320
+ function isUndefinedMatchValue(value) {
321
+ return Boolean(
322
+ value
323
+ && typeof value === 'object'
324
+ && !Array.isArray(value)
325
+ && value.__covenCodeLiteral === 'undefined'
326
+ && Object.keys(value).length === 1,
327
+ );
328
+ }