@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,101 @@
1
+ import { existsSync } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { CONFIG_SUBDIR, PROJECT_SUBDIR } from '../constants.mjs';
5
+
6
+ const TOOLBOX_ENV = 'COVEN_CODE_TOOLBOX';
7
+ const SETTINGS_FILE_ENV = 'COVEN_CODE_SETTINGS_FILE';
8
+ const MANAGED_SETTINGS_FILE_ENV = 'COVEN_CODE_MANAGED_SETTINGS_FILE';
9
+
10
+ export function configDir() {
11
+ return process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
12
+ }
13
+
14
+ export function toolsDirs(parsed = {}) {
15
+ if (parsed.toolbox) return parsed.toolbox.split(path.delimiter).filter(Boolean).map(expandHomePath);
16
+ if (Object.hasOwn(process.env, TOOLBOX_ENV)) {
17
+ if (process.env[TOOLBOX_ENV] === '') return [];
18
+ return process.env[TOOLBOX_ENV].split(path.delimiter).filter(Boolean);
19
+ }
20
+ return [path.join(configDir(), CONFIG_SUBDIR, 'tools')];
21
+ }
22
+
23
+ export function writableToolsDir() {
24
+ return toolsDirs()[0] || path.join(configDir(), CONFIG_SUBDIR, 'tools');
25
+ }
26
+
27
+ export function workspaceSettingsFile(cwd) {
28
+ return path.join(cwd, PROJECT_SUBDIR, 'settings.json');
29
+ }
30
+
31
+ export function findWorkspaceSettingsFile(cwd) {
32
+ let current = path.resolve(cwd);
33
+ const boundary = findGitRoot(current) ?? current;
34
+ while (true) {
35
+ for (const name of ['settings.json', 'settings.jsonc']) {
36
+ const candidate = path.join(current, PROJECT_SUBDIR, name);
37
+ if (existsSync(candidate)) return candidate;
38
+ }
39
+ if (current === boundary) return undefined;
40
+ const parent = path.dirname(current);
41
+ if (parent === current) return undefined;
42
+ current = parent;
43
+ }
44
+ }
45
+
46
+ export function findUserSettingsFile() {
47
+ const dir = path.join(configDir(), CONFIG_SUBDIR);
48
+ for (const name of ['settings.json', 'settings.jsonc']) {
49
+ const candidate = path.join(dir, name);
50
+ if (existsSync(candidate)) return candidate;
51
+ }
52
+ return undefined;
53
+ }
54
+
55
+ export function findManagedSettingsFile() {
56
+ if (process.env[MANAGED_SETTINGS_FILE_ENV]) return process.env[MANAGED_SETTINGS_FILE_ENV];
57
+ const candidates = [];
58
+ if (process.platform === 'win32') {
59
+ if (process.env.ProgramData) {
60
+ candidates.push(path.join(process.env.ProgramData, CONFIG_SUBDIR, 'managed-settings.json'));
61
+ }
62
+ } else if (process.platform === 'darwin') {
63
+ candidates.push('/Library/Application Support/coven-code/managed-settings.json');
64
+ } else {
65
+ candidates.push('/etc/coven-code/managed-settings.json');
66
+ }
67
+ return candidates.find((candidate) => existsSync(candidate));
68
+ }
69
+
70
+ export function settingsFile(parsed = {}) {
71
+ if (parsed.settingsFile || process.env[SETTINGS_FILE_ENV]) {
72
+ return parsed.settingsFile || process.env[SETTINGS_FILE_ENV];
73
+ }
74
+ return findUserSettingsFile() || path.join(configDir(), CONFIG_SUBDIR, 'settings.json');
75
+ }
76
+
77
+ export function findProjectRoot(cwd) {
78
+ let current = cwd;
79
+ while (true) {
80
+ if (existsSync(path.join(current, 'package.json')) || existsSync(path.join(current, '.git'))) return current;
81
+ const parent = path.dirname(current);
82
+ if (parent === current || current === os.homedir()) return cwd;
83
+ current = parent;
84
+ }
85
+ }
86
+
87
+ function findGitRoot(cwd) {
88
+ let current = path.resolve(cwd);
89
+ while (true) {
90
+ if (existsSync(path.join(current, '.git'))) return current;
91
+ const parent = path.dirname(current);
92
+ if (parent === current || current === os.homedir()) return undefined;
93
+ current = parent;
94
+ }
95
+ }
96
+
97
+ export function expandHomePath(value) {
98
+ if (value === '~') return os.homedir();
99
+ if (value.startsWith(`~${path.sep}`)) return path.join(os.homedir(), value.slice(2));
100
+ return value;
101
+ }
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: building-skills
3
+ description: Create Coven Code skills for a codebase or workflow
4
+ ---
5
+
6
+ # Building Skills
7
+
8
+ Use this skill when the user asks to create a new Coven Code skill for a codebase,
9
+ workflow, service, or repeated task.
10
+
11
+ ## Workflow
12
+
13
+ 1. Identify the task the skill should help with and where it should live.
14
+ 2. Create a skill directory with a `SKILL.md` file.
15
+ 3. Add YAML frontmatter with a unique `name` and concise `description`.
16
+ 4. Write clear instructions, examples, and any resource paths the agent should use.
17
+ 5. Keep the skill focused so it is loaded only when its description matches.
18
+
19
+ Project skills live in `.agents/skills/<name>/`. User-wide skills live in
20
+ `~/.config/agents/skills/<name>/`.
@@ -0,0 +1,95 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { CONFIG_SUBDIR } from '../constants.mjs';
6
+ import { configDir, expandHomePath } from '../settings/paths.mjs';
7
+ import { readSettings } from '../settings/load.mjs';
8
+ import { UsageError } from '../cli/parse.mjs';
9
+
10
+ const BUILTIN_SKILLS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), 'builtin');
11
+
12
+ export function listSkills(options = {}) {
13
+ const seen = new Set();
14
+ const skills = [];
15
+ for (const root of [...parsedSkillRoots(options.parsed), ...skillSearchRoots(options.cwd)]) {
16
+ if (!existsSync(root.dir)) continue;
17
+ for (const entry of readdirSync(root.dir)) {
18
+ const dir = path.join(root.dir, entry);
19
+ const filePath = path.join(dir, 'SKILL.md');
20
+ if (!existsSync(filePath) || !statSync(dir).isDirectory()) continue;
21
+ const metadata = readSkillMetadata(dir);
22
+ const skill = { ...metadata, dir, filePath, source: root.source };
23
+ if (options.includeShadowed || !seen.has(skill.name)) skills.push(skill);
24
+ seen.add(skill.name);
25
+ }
26
+ }
27
+ return skills;
28
+ }
29
+
30
+ export function findSkill(name) {
31
+ return listSkills().find((skill) => skill.name === name);
32
+ }
33
+
34
+ export function skillSearchRoots(cwd = process.cwd()) {
35
+ const userRoots = [
36
+ { source: 'user', dir: path.join(configDir(), 'agents', 'skills') },
37
+ { source: 'user', dir: path.join(configDir(), CONFIG_SUBDIR, 'skills') },
38
+ ...configuredSkillRoots(readSettings({})),
39
+ ];
40
+ const projectRoots = projectSkillRoots(cwd, '.agents', 'skills');
41
+ if (readSettings({})['covenCode.skills.disableLegacySkillRoots'] === true) return [...userRoots, ...projectRoots];
42
+ return [
43
+ ...userRoots,
44
+ ...projectRoots,
45
+ ...projectSkillRoots(cwd, '.claude', 'skills'),
46
+ { source: 'user', dir: path.join(os.homedir(), '.claude', 'skills') },
47
+ { source: 'built-in', dir: BUILTIN_SKILLS_DIR },
48
+ ];
49
+ }
50
+
51
+ function projectSkillRoots(cwd, ...parts) {
52
+ const roots = [];
53
+ const home = os.homedir();
54
+ let current = path.resolve(cwd);
55
+ while (true) {
56
+ roots.push({ source: 'project', dir: path.join(current, ...parts) });
57
+ if (current === home || current === path.dirname(current)) break;
58
+ current = path.dirname(current);
59
+ }
60
+ return roots;
61
+ }
62
+
63
+ export function parsedSkillRoots(parsed = {}) {
64
+ return splitSkillPath(parsed.skills).map((entry) => ({ source: 'cli', dir: expandHomePath(entry) }));
65
+ }
66
+
67
+ function configuredSkillRoots(settings) {
68
+ const rawPath = settings['covenCode.skills.path'];
69
+ return splitSkillPath(rawPath).map((entry) => ({ source: 'user', dir: expandHomePath(entry) }));
70
+ }
71
+
72
+ function splitSkillPath(rawPath) {
73
+ if (typeof rawPath !== 'string' || rawPath.trim() === '') return [];
74
+ return rawPath
75
+ .split(path.delimiter)
76
+ .filter(Boolean);
77
+ }
78
+
79
+ export function readSkillMetadata(dir) {
80
+ const filePath = path.join(dir, 'SKILL.md');
81
+ if (!existsSync(filePath)) throw new UsageError(`Skill at ${dir} is missing SKILL.md`);
82
+ const text = readFileSync(filePath, 'utf8');
83
+ const frontmatter = text.match(/^---\n([\s\S]*?)\n---/);
84
+ const metadata = {};
85
+ if (frontmatter) {
86
+ for (const line of frontmatter[1].split(/\r?\n/)) {
87
+ const match = line.match(/^([A-Za-z0-9_.-]+):\s*(.*)$/);
88
+ if (match) metadata[match[1]] = match[2].replace(/^["']|["']$/g, '');
89
+ }
90
+ }
91
+ return {
92
+ name: metadata.name || path.basename(dir),
93
+ description: metadata.description || '',
94
+ };
95
+ }
@@ -0,0 +1,176 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { CONFIG_SUBDIR } from '../constants.mjs';
4
+ import { configDir } from '../settings/paths.mjs';
5
+ import { readEffectiveSettings, readSettingsFile, writeSettingsFile } from '../settings/load.mjs';
6
+ import { displayCwd } from '../util/fs.mjs';
7
+ import { UsageError } from '../cli/parse.mjs';
8
+
9
+ export const THREAD_VISIBILITIES = ['private', 'public', 'workspace', 'group', 'unlisted'];
10
+ export const THREAD_VISIBILITY_INPUTS = ['private', 'public', 'workspace', 'workspace-shared', 'group', 'group-shared', 'unlisted'];
11
+
12
+ export function threadsDir() {
13
+ return path.join(configDir(), CONFIG_SUBDIR, 'threads');
14
+ }
15
+
16
+ export function threadFile(id) {
17
+ return path.join(threadsDir(), `${id}.json`);
18
+ }
19
+
20
+ export function readThread(id) {
21
+ const filePath = threadFile(id);
22
+ if (!existsSync(filePath)) return undefined;
23
+ return readSettingsFile(filePath);
24
+ }
25
+
26
+ export function listThreads() {
27
+ const dir = threadsDir();
28
+ if (!existsSync(dir)) return [];
29
+ return readdirSync(dir)
30
+ .filter((entry) => entry.endsWith('.json'))
31
+ .map((entry) => readSettingsFile(path.join(dir, entry)))
32
+ .filter((thread) => thread.id)
33
+ .sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)));
34
+ }
35
+
36
+ export function latestActiveThread() {
37
+ return listThreads()
38
+ .filter((thread) => !thread.archived)
39
+ .sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)))[0];
40
+ }
41
+
42
+ export async function writeThread(thread) {
43
+ await writeSettingsFile(threadFile(thread.id), thread);
44
+ }
45
+
46
+ export function requireThread(id) {
47
+ if (!id) throw new UsageError('threads command requires a thread id');
48
+ const thread = readThread(id);
49
+ if (!thread) throw new UsageError(`Unknown thread: ${id}`);
50
+ return thread;
51
+ }
52
+
53
+ export function threadSearchText(thread) {
54
+ return [
55
+ thread.id,
56
+ thread.title,
57
+ thread.cwd,
58
+ thread.createdAt,
59
+ thread.updatedAt,
60
+ ...(thread.labels ?? []),
61
+ ...(thread.messages ?? []).map((message) => message.content),
62
+ ].join(' ');
63
+ }
64
+
65
+ export function defaultThreadVisibility(parsed = {}) {
66
+ const configured = readEffectiveSettings(parsed)['covenCode.defaultVisibility'];
67
+ const originKey = currentRepositoryOriginKey();
68
+ const visibility = typeof configured === 'string'
69
+ ? configured
70
+ : configured?.[originKey] ?? configured?.default;
71
+ return normalizeThreadVisibility(parsed.visibility) ?? normalizeThreadVisibility(visibility) ?? 'private';
72
+ }
73
+
74
+ export function normalizeThreadVisibility(visibility) {
75
+ if (visibility === 'workspace-shared') return 'workspace';
76
+ if (visibility === 'group-shared') return 'group';
77
+ return THREAD_VISIBILITIES.includes(visibility) ? visibility : undefined;
78
+ }
79
+
80
+ export async function saveThread(id, prompt, result, mode, parsed = {}) {
81
+ return saveThreadMessages(id, [
82
+ { role: 'user', content: prompt },
83
+ { role: 'assistant', content: result },
84
+ ], mode, parsed);
85
+ }
86
+
87
+ export async function persistThreadTurn(id, prompt, result, mode, thread, parsed = {}) {
88
+ return persistThreadMessages(id, [
89
+ { role: 'user', content: prompt },
90
+ { role: 'assistant', content: result },
91
+ ], mode, thread, parsed);
92
+ }
93
+
94
+ export async function saveThreadMessages(id, messages, mode, parsed = {}) {
95
+ const now = new Date().toISOString();
96
+ const firstUser = messages.find((message) => message.role === 'user')?.content ?? '';
97
+ const existing = readThread(id);
98
+ const thread = {
99
+ id,
100
+ title: existing?.title === '(pending thread)' || !existing?.title
101
+ ? firstUser.split(/\r?\n/).find(Boolean)?.slice(0, 120) || '(empty prompt)'
102
+ : existing.title,
103
+ cwd: displayCwd(),
104
+ mode,
105
+ visibility: defaultThreadVisibility(parsed),
106
+ labels: normalizedLabels(parsed.labels),
107
+ archived: Boolean(parsed.archive),
108
+ createdAt: existing?.createdAt ?? now,
109
+ updatedAt: now,
110
+ messages: [...(existing?.messages ?? []), ...messages],
111
+ };
112
+ await writeThread(thread);
113
+ return thread;
114
+ }
115
+
116
+ export async function persistThreadMessages(id, messages, mode, thread, parsed = {}) {
117
+ if (!thread) {
118
+ return saveThreadMessages(id, messages, mode, parsed);
119
+ }
120
+ thread.mode = mode;
121
+ thread.visibility = normalizeThreadVisibility(parsed.visibility) ?? thread.visibility;
122
+ thread.labels = mergeLabels(thread.labels, parsed.labels);
123
+ if (parsed.archive) thread.archived = true;
124
+ thread.updatedAt = new Date().toISOString();
125
+ thread.messages.push(...messages);
126
+ await writeThread(thread);
127
+ return thread;
128
+ }
129
+
130
+ export function threadContinuationPrompt(thread, prompt) {
131
+ return `[thread:${thread.id}]\n${thread.messages.map((message) => `${message.role}: ${message.content}`).join('\n')}\n[/thread]\n${prompt}`;
132
+ }
133
+
134
+ function normalizedLabels(labels = []) {
135
+ return [...new Set(labels.map((label) => String(label).trim()).filter(Boolean))];
136
+ }
137
+
138
+ function mergeLabels(existing = [], labels = []) {
139
+ return [...new Set([...existing, ...normalizedLabels(labels)])];
140
+ }
141
+
142
+ function currentRepositoryOriginKey() {
143
+ const configPath = findGitConfig(process.cwd());
144
+ if (!configPath) return undefined;
145
+ try {
146
+ const config = readFileSync(configPath, 'utf8');
147
+ const originUrl = config.match(/\[remote "origin"\][\s\S]*?\n\s*url\s*=\s*([^\r\n]+)/)?.[1]?.trim();
148
+ return normalizeGitOrigin(originUrl);
149
+ } catch {
150
+ return undefined;
151
+ }
152
+ }
153
+
154
+ function findGitConfig(cwd) {
155
+ let current = path.resolve(cwd);
156
+ while (true) {
157
+ const candidate = path.join(current, '.git', 'config');
158
+ if (existsSync(candidate)) return candidate;
159
+ const parent = path.dirname(current);
160
+ if (parent === current || current === path.resolve(configDir(), '..')) return undefined;
161
+ current = parent;
162
+ }
163
+ }
164
+
165
+ function normalizeGitOrigin(originUrl = '') {
166
+ const text = originUrl.replace(/\.git$/, '');
167
+ const scp = text.match(/^git@([^:]+):(.+)$/);
168
+ if (scp) return `${scp[1]}/${scp[2]}`;
169
+ try {
170
+ const url = new URL(text);
171
+ const pathName = url.pathname.replace(/^\/+/, '');
172
+ return pathName ? `${url.hostname}/${pathName}` : undefined;
173
+ } catch {
174
+ return undefined;
175
+ }
176
+ }
@@ -0,0 +1,110 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { THREAD_URL_BASE } from '../../constants.mjs';
3
+ import { resolvePermissionDecision } from '../../commands/permissions.mjs';
4
+ import { runPluginEventHandlers } from '../../plugins/discover.mjs';
5
+ import { readEffectiveSettings } from '../../settings/load.mjs';
6
+ import { shellQuote, splitShellWords } from '../../util/shell.mjs';
7
+ import { isToolDisabled } from '../toolbox.mjs';
8
+ import {
9
+ applyToolCallDecision,
10
+ createToolUseID,
11
+ permissionDeniedOutput,
12
+ pluginResultOutput,
13
+ pluginToolCallEvent,
14
+ pluginToolResultDecisionExitCode,
15
+ pluginToolResultEvent,
16
+ pluginToolUseBlock,
17
+ toolCallDecisionToolRun,
18
+ validateToolCallDecision,
19
+ } from './runtime.mjs';
20
+
21
+ export const TOOL_NAME = 'Bash';
22
+
23
+ export async function executePromptBashToolRequest(request, stdin = '', parsed = {}, plugins = { handlers: {} }, threadId = '') {
24
+ if (isToolDisabled(TOOL_NAME, 'built-in', parsed)) return { output: `Tool disabled: ${TOOL_NAME}` };
25
+ request = { ...request, flags: normalizeBashInput(request.flags) };
26
+ if (!request.flags.command) return { output: 'Bash requires --command' };
27
+ const toolUseID = createToolUseID();
28
+ const callDecision = await runPluginEventHandlers(
29
+ plugins.handlers['tool.call'],
30
+ pluginToolCallEvent(TOOL_NAME, request.flags, threadId, toolUseID),
31
+ validateToolCallDecision,
32
+ );
33
+ const callResult = applyToolCallDecision(TOOL_NAME, request, callDecision);
34
+ if (callResult.output) return toolCallDecisionToolRun(TOOL_NAME, request.flags, toolUseID, callResult);
35
+ request = { ...callResult.request, flags: normalizeBashInput(callResult.request.flags) };
36
+ const decision = resolvePermissionDecision(TOOL_NAME, request.flags, parsed, { threadId });
37
+ if (!parsed.dangerouslyAllowAll && decision.action !== 'allow') {
38
+ return {
39
+ output: permissionDeniedOutput(TOOL_NAME, decision),
40
+ permissionDenials: [{ tool: TOOL_NAME, action: decision.action, reason: 'permission' }],
41
+ };
42
+ }
43
+ const result = spawnSync(commandForBashExecution(request.flags.command, parsed, threadId), {
44
+ cwd: process.cwd(),
45
+ input: stdin,
46
+ shell: true,
47
+ encoding: 'utf8',
48
+ env: {
49
+ ...process.env,
50
+ COVEN_CODE_THREAD_ID: threadId,
51
+ AGENT_THREAD_ID: threadId,
52
+ },
53
+ });
54
+ if (result.stderr) process.stderr.write(result.stderr);
55
+ process.exitCode = result.status ?? 0;
56
+ const output = (result.stdout ?? '').trimEnd();
57
+ const resultDecision = await runPluginEventHandlers(
58
+ plugins.handlers['tool.result'],
59
+ pluginToolResultEvent(TOOL_NAME, request.flags, (result.status ?? 0) === 0 ? 'done' : 'error', output, threadId, toolUseID),
60
+ );
61
+ const exitCode = pluginToolResultDecisionExitCode(resultDecision, result.status ?? 0);
62
+ return {
63
+ output: pluginResultOutput(resultDecision, output),
64
+ exitCode,
65
+ toolUse: pluginToolUseBlock(TOOL_NAME, request.flags, toolUseID),
66
+ };
67
+ }
68
+
69
+ function normalizeBashInput(input = {}) {
70
+ const command = String(input.command ?? input.cmd ?? '');
71
+ return { ...input, command, cmd: command };
72
+ }
73
+
74
+ function commandForBashExecution(command, parsed = {}, threadId = '') {
75
+ if (!isGitCommitCommand(command)) return command;
76
+ const trailers = gitCommitTrailers(parsed, threadId);
77
+ if (!trailers.length) return command;
78
+ return `${command} ${trailers.map((trailer) => `--trailer ${shellQuote(trailer)}`).join(' ')}`;
79
+ }
80
+
81
+ function isGitCommitCommand(command) {
82
+ const words = splitShellWords(command);
83
+ if (words[0] !== 'git') return false;
84
+ return gitSubcommand(words) === 'commit';
85
+ }
86
+
87
+ function gitSubcommand(words) {
88
+ for (let index = 1; index < words.length; index += 1) {
89
+ const word = words[index];
90
+ if (word === '-C' || word === '-c' || word === '--git-dir' || word === '--work-tree' || word === '--namespace') {
91
+ index += 1;
92
+ continue;
93
+ }
94
+ if (word.startsWith('-')) continue;
95
+ return word;
96
+ }
97
+ return '';
98
+ }
99
+
100
+ function gitCommitTrailers(parsed, threadId) {
101
+ const settings = readEffectiveSettings(parsed);
102
+ const trailers = [];
103
+ if (settings['covenCode.git.commit.thread.enabled'] !== false && threadId) {
104
+ trailers.push(`Coven-Code-Thread: ${THREAD_URL_BASE}/${threadId}`);
105
+ }
106
+ if (settings['covenCode.git.commit.coauthor.enabled'] === true) {
107
+ trailers.push('Co-authored-by: Coven Code <coven-code@opencoven.local>');
108
+ }
109
+ return trailers;
110
+ }
@@ -0,0 +1,66 @@
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { resolvePermissionDecision } from '../../commands/permissions.mjs';
4
+ import { runPluginEventHandlers } from '../../plugins/discover.mjs';
5
+ import { isToolDisabled } from '../toolbox.mjs';
6
+ import {
7
+ applyToolCallDecision,
8
+ createToolUseID,
9
+ permissionDeniedOutput,
10
+ pluginTextToolRunResult,
11
+ pluginToolCallEvent,
12
+ pluginToolResultEvent,
13
+ pluginToolUseBlock,
14
+ toolCallDecisionToolRun,
15
+ validateToolCallDecision,
16
+ } from './runtime.mjs';
17
+
18
+ export const TOOL_NAME = 'create_file';
19
+
20
+ export async function executePromptCreateFileToolRequest(request, parsed = {}, plugins = { handlers: {} }, threadId = '') {
21
+ if (isToolDisabled(TOOL_NAME, 'built-in', parsed)) return { output: `Tool disabled: ${TOOL_NAME}` };
22
+ request = { ...request, flags: normalizeCreateFileInput(request.flags) };
23
+ if (!request.flags.path) return { output: 'create_file requires --path' };
24
+ const toolUseID = createToolUseID();
25
+ const callDecision = await runPluginEventHandlers(
26
+ plugins.handlers['tool.call'],
27
+ pluginToolCallEvent(TOOL_NAME, request.flags, threadId, toolUseID),
28
+ validateToolCallDecision,
29
+ );
30
+ const callResult = applyToolCallDecision(TOOL_NAME, request, callDecision);
31
+ if (callResult.output) return toolCallDecisionToolRun(TOOL_NAME, request.flags, toolUseID, callResult);
32
+ request = { ...callResult.request, flags: normalizeCreateFileInput(callResult.request.flags) };
33
+ const decision = resolvePermissionDecision(TOOL_NAME, request.flags, parsed, { threadId });
34
+ if (!parsed.dangerouslyAllowAll && decision.action !== 'allow') {
35
+ return {
36
+ output: permissionDeniedOutput(TOOL_NAME, decision),
37
+ permissionDenials: [{ tool: TOOL_NAME, action: decision.action, reason: 'permission' }],
38
+ };
39
+ }
40
+ const output = createBuiltinFile(request.flags.path, request.flags.content);
41
+ const resultDecision = await runPluginEventHandlers(
42
+ plugins.handlers['tool.result'],
43
+ pluginToolResultEvent(TOOL_NAME, request.flags, 'done', output, threadId, toolUseID),
44
+ );
45
+ return {
46
+ ...pluginTextToolRunResult(resultDecision, output),
47
+ toolUse: pluginToolUseBlock(TOOL_NAME, request.flags, toolUseID),
48
+ };
49
+ }
50
+
51
+ function normalizeCreateFileInput(input = {}) {
52
+ const filePath = input.path ?? input.file ?? input.file_path;
53
+ const content = input.content ?? input.text ?? input.body ?? '';
54
+ return {
55
+ ...input,
56
+ path: filePath ? String(filePath) : '',
57
+ content: String(content ?? ''),
58
+ };
59
+ }
60
+
61
+ function createBuiltinFile(filePath, content) {
62
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
63
+ mkdirSync(path.dirname(absolutePath), { recursive: true });
64
+ writeFileSync(absolutePath, content, { encoding: 'utf8', flag: 'wx' });
65
+ return `Created ${filePath}`;
66
+ }
@@ -0,0 +1,76 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { resolvePermissionDecision } from '../../commands/permissions.mjs';
4
+ import { runPluginEventHandlers } from '../../plugins/discover.mjs';
5
+ import { isToolDisabled } from '../toolbox.mjs';
6
+ import {
7
+ applyToolCallDecision,
8
+ createToolUseID,
9
+ permissionDeniedOutput,
10
+ pluginTextToolRunResult,
11
+ pluginToolCallEvent,
12
+ pluginToolResultEvent,
13
+ pluginToolUseBlock,
14
+ toolCallDecisionToolRun,
15
+ validateToolCallDecision,
16
+ } from './runtime.mjs';
17
+ import { recordEditUndo } from './undo-edit.mjs';
18
+
19
+ export const TOOL_NAME = 'edit_file';
20
+
21
+ export async function executePromptEditFileToolRequest(request, parsed = {}, plugins = { handlers: {} }, threadId = '') {
22
+ if (isToolDisabled(TOOL_NAME, 'built-in', parsed)) return { output: `Tool disabled: ${TOOL_NAME}` };
23
+ request = { ...request, flags: normalizeEditFileInput(request.flags) };
24
+ if (!request.flags.path) return { output: 'edit_file requires --path' };
25
+ if (!request.flags.old_string) return { output: 'edit_file requires --old' };
26
+ const toolUseID = createToolUseID();
27
+ const callDecision = await runPluginEventHandlers(
28
+ plugins.handlers['tool.call'],
29
+ pluginToolCallEvent(TOOL_NAME, request.flags, threadId, toolUseID),
30
+ validateToolCallDecision,
31
+ );
32
+ const callResult = applyToolCallDecision(TOOL_NAME, request, callDecision);
33
+ if (callResult.output) return toolCallDecisionToolRun(TOOL_NAME, request.flags, toolUseID, callResult);
34
+ request = { ...callResult.request, flags: normalizeEditFileInput(callResult.request.flags) };
35
+ const decision = resolvePermissionDecision(TOOL_NAME, request.flags, parsed, { threadId });
36
+ if (!parsed.dangerouslyAllowAll && decision.action !== 'allow') {
37
+ return {
38
+ output: permissionDeniedOutput(TOOL_NAME, decision),
39
+ permissionDenials: [{ tool: TOOL_NAME, action: decision.action, reason: 'permission' }],
40
+ };
41
+ }
42
+ const editResult = editBuiltinFile(request.flags.path, request.flags.old_string, request.flags.new_string, threadId);
43
+ const resultDecision = await runPluginEventHandlers(
44
+ plugins.handlers['tool.result'],
45
+ pluginToolResultEvent(TOOL_NAME, request.flags, editResult.status, editResult.output, threadId, toolUseID),
46
+ );
47
+ return {
48
+ ...pluginTextToolRunResult(resultDecision, editResult.output, editResult.status === 'done' ? 0 : 1),
49
+ toolUse: pluginToolUseBlock(TOOL_NAME, request.flags, toolUseID),
50
+ };
51
+ }
52
+
53
+ function normalizeEditFileInput(input = {}) {
54
+ const filePath = input.path ?? input.file ?? input.file_path;
55
+ const oldString = input.old_string ?? input.old ?? input.search ?? input.find;
56
+ const newString = input.new_string ?? input.new ?? input.replacement ?? input.replace ?? '';
57
+ return {
58
+ ...input,
59
+ path: filePath ? String(filePath) : '',
60
+ old: oldString ? String(oldString) : '',
61
+ new: String(newString ?? ''),
62
+ old_string: oldString ? String(oldString) : '',
63
+ new_string: String(newString ?? ''),
64
+ };
65
+ }
66
+
67
+ function editBuiltinFile(filePath, oldString, newString, threadId = '') {
68
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
69
+ const current = readFileSync(absolutePath, 'utf8');
70
+ if (!current.includes(oldString)) {
71
+ return { status: 'error', output: `No match found in ${filePath}` };
72
+ }
73
+ recordEditUndo(threadId, { path: filePath, absolutePath, content: current });
74
+ writeFileSync(absolutePath, current.replace(oldString, newString), 'utf8');
75
+ return { status: 'done', output: `Edited ${filePath}` };
76
+ }