@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.
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 +26 -23
  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 -590
  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 -116
  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
@@ -1,55 +0,0 @@
1
- import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
-
5
- export function oauthMcpHeaders(serverName = '') {
6
- const credential = readMcpOauthCredential(serverName);
7
- return credential.accessToken || credential.access_token ? { Authorization: `Bearer ${credential.accessToken ?? credential.access_token}` } : {};
8
- }
9
-
10
- function readMcpOauthCredential(serverName = '') {
11
- if (!serverName) return {};
12
- try {
13
- return JSON.parse(readFileSync(mcpOauthCredentialPath(serverName), 'utf8'));
14
- } catch {
15
- return {};
16
- }
17
- }
18
-
19
- export async function refreshMcpOauthToken(serverName = '', config = {}) {
20
- if (hasExplicitAuthorizationHeader(config)) return false;
21
- const credential = readMcpOauthCredential(serverName);
22
- const refreshToken = credential.refreshToken ?? credential.refresh_token;
23
- const tokenUrl = credential.tokenUrl ?? credential.token_url;
24
- if (!refreshToken || !tokenUrl) return false;
25
- const response = await fetch(tokenUrl, {
26
- method: 'POST',
27
- headers: { 'content-type': 'application/x-www-form-urlencoded' },
28
- body: new URLSearchParams({
29
- grant_type: 'refresh_token',
30
- refresh_token: refreshToken,
31
- ...(credential.clientId || credential.client_id ? { client_id: credential.clientId ?? credential.client_id } : {}),
32
- ...(credential.clientSecret || credential.client_secret ? { client_secret: credential.clientSecret ?? credential.client_secret } : {}),
33
- }),
34
- });
35
- if (!response.ok) return false;
36
- const token = await response.json();
37
- const nextCredential = {
38
- ...credential,
39
- accessToken: token.access_token ?? token.accessToken ?? credential.accessToken,
40
- refreshToken: token.refresh_token ?? token.refreshToken ?? credential.refreshToken,
41
- ...(token.expires_in || token.expiresIn ? { expiresAt: Date.now() + Number(token.expires_in ?? token.expiresIn) * 1000 } : {}),
42
- };
43
- const credentialPath = mcpOauthCredentialPath(serverName);
44
- mkdirSync(path.dirname(credentialPath), { recursive: true, mode: 0o700 });
45
- writeFileSync(credentialPath, `${JSON.stringify(nextCredential, null, 2)}\n`, { mode: 0o600 });
46
- return Boolean(nextCredential.accessToken);
47
- }
48
-
49
- function hasExplicitAuthorizationHeader(config = {}) {
50
- return Object.keys(config.headers ?? {}).some((key) => key.toLowerCase() === 'authorization');
51
- }
52
-
53
- function mcpOauthCredentialPath(serverName) {
54
- return path.join(os.homedir(), '.coven-code', 'oauth', `${serverName}.json`);
55
- }
@@ -1,54 +0,0 @@
1
- import { oauthMcpHeaders } from './remote-oauth.mjs';
2
-
3
- export const remoteMcpSessions = new Map();
4
-
5
- export function remoteMcpHeaders(config = {}, accept, serverName = '') {
6
- return {
7
- 'content-type': 'application/json',
8
- accept,
9
- ...oauthMcpHeaders(serverName),
10
- ...remoteMcpSessionHeader(config, serverName),
11
- ...(config.headers ?? {}),
12
- };
13
- }
14
-
15
- export async function initializeRemoteMcpSession(config = {}, serverName = '', signal) {
16
- const response = await fetch(config.url, {
17
- method: 'POST',
18
- headers: remoteMcpHeaders(config, 'application/json, text/event-stream', serverName),
19
- body: JSON.stringify({
20
- jsonrpc: '2.0',
21
- id: 0,
22
- method: 'initialize',
23
- params: {
24
- protocolVersion: '2025-06-18',
25
- capabilities: {},
26
- clientInfo: { name: 'coven-code', version: '0.0.0' },
27
- },
28
- }),
29
- signal,
30
- });
31
- rememberRemoteMcpSession(config, serverName, response);
32
- if (!response.ok || !remoteMcpSessions.has(remoteMcpSessionKey(config, serverName))) return false;
33
- await fetch(config.url, {
34
- method: 'POST',
35
- headers: remoteMcpHeaders(config, 'application/json, text/event-stream', serverName),
36
- body: JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }),
37
- signal,
38
- });
39
- return true;
40
- }
41
-
42
- export function rememberRemoteMcpSession(config = {}, serverName = '', response) {
43
- const sessionId = response.headers.get('mcp-session-id');
44
- if (sessionId) remoteMcpSessions.set(remoteMcpSessionKey(config, serverName), sessionId);
45
- }
46
-
47
- function remoteMcpSessionHeader(config = {}, serverName = '') {
48
- const sessionId = remoteMcpSessions.get(remoteMcpSessionKey(config, serverName));
49
- return sessionId ? { 'Mcp-Session-Id': sessionId } : {};
50
- }
51
-
52
- export function remoteMcpSessionKey(config = {}, serverName = '') {
53
- return `${serverName}\n${config.url ?? ''}`;
54
- }
@@ -1,82 +0,0 @@
1
- import { refreshMcpOauthToken } from './remote-oauth.mjs';
2
- import { rememberRemoteMcpSession, remoteMcpHeaders } from './remote-session.mjs';
3
-
4
- export async function postLegacySseMcp(config = {}, body, signal, serverName = '') {
5
- const endpoint = await discoverLegacySseEndpoint(config, signal, serverName);
6
- if (!endpoint) return {};
7
- const response = await fetch(endpoint, {
8
- method: 'POST',
9
- headers: remoteMcpHeaders(config, 'application/json, text/event-stream', serverName),
10
- body,
11
- signal,
12
- });
13
- rememberRemoteMcpSession(config, serverName, response);
14
- if (response.status === 401 && await refreshMcpOauthToken(serverName, config)) {
15
- const retry = await fetch(endpoint, {
16
- method: 'POST',
17
- headers: remoteMcpHeaders(config, 'application/json, text/event-stream', serverName),
18
- body,
19
- signal,
20
- });
21
- rememberRemoteMcpSession(config, serverName, retry);
22
- return parseRemoteMcpResponse(await retry.text());
23
- }
24
- return parseRemoteMcpResponse(await response.text());
25
- }
26
-
27
- async function discoverLegacySseEndpoint(config = {}, signal, serverName = '') {
28
- const response = await fetch(config.url, {
29
- method: 'GET',
30
- headers: remoteMcpHeaders(config, 'text/event-stream', serverName),
31
- signal,
32
- });
33
- if (!response.ok) return '';
34
- return resolveRemoteMcpUrl(config.url, parseLegacySseEndpoint(await response.text()));
35
- }
36
-
37
- function parseLegacySseEndpoint(text = '') {
38
- let event = 'message';
39
- const data = [];
40
- for (const line of text.split(/\r?\n/)) {
41
- if (!line) {
42
- if (event === 'endpoint' && data.length) return data.join('\n').trim();
43
- event = 'message';
44
- data.length = 0;
45
- continue;
46
- }
47
- if (line.startsWith(':')) continue;
48
- const separator = line.indexOf(':');
49
- const field = separator === -1 ? line : line.slice(0, separator);
50
- const value = separator === -1 ? '' : line.slice(separator + 1).replace(/^ /, '');
51
- if (field === 'event') event = value;
52
- if (field === 'data') data.push(value);
53
- }
54
- if (event === 'endpoint' && data.length) return data.join('\n').trim();
55
- return '';
56
- }
57
-
58
- function resolveRemoteMcpUrl(base, endpoint) {
59
- if (!endpoint) return '';
60
- try {
61
- return new URL(endpoint, base).href;
62
- } catch {
63
- return '';
64
- }
65
- }
66
-
67
- export function parseRemoteMcpResponse(text = '') {
68
- for (const chunk of text.split(/\r?\n/).filter(Boolean)) {
69
- const line = chunk.startsWith('data:') ? chunk.slice('data:'.length).trim() : chunk.trim();
70
- if (!line || line === '[DONE]') continue;
71
- try {
72
- return JSON.parse(line);
73
- } catch {
74
- // Remote MCP servers can include diagnostic or event wrapper lines.
75
- }
76
- }
77
- try {
78
- return JSON.parse(text);
79
- } catch {
80
- return {};
81
- }
82
- }
@@ -1,74 +0,0 @@
1
- import { parseMcpCallOutput, parseMcpResourceOutput } from './parsers.mjs';
2
- import { refreshMcpOauthToken } from './remote-oauth.mjs';
3
- import {
4
- initializeRemoteMcpSession,
5
- rememberRemoteMcpSession,
6
- remoteMcpHeaders,
7
- remoteMcpSessionKey,
8
- remoteMcpSessions,
9
- } from './remote-session.mjs';
10
- import { parseRemoteMcpResponse, postLegacySseMcp } from './remote-sse.mjs';
11
-
12
- export async function queryRemoteMcpTools(config = {}, serverName = '') {
13
- const message = await postRemoteMcp(config, 'tools/list', {}, serverName);
14
- if (Array.isArray(message.result?.tools)) return message.result.tools;
15
- if (Array.isArray(message.tools)) return message.tools;
16
- return [];
17
- }
18
-
19
- export async function callRemoteMcpTool(config = {}, name, args = {}, serverName = '') {
20
- const message = await postRemoteMcp(config, 'tools/call', { name, arguments: args }, serverName);
21
- return parseMcpCallOutput(`${JSON.stringify(message)}\n`);
22
- }
23
-
24
- export async function readRemoteMcpResource(config = {}, uri, serverName = '') {
25
- const message = await postRemoteMcp(config, 'resources/read', { uri }, serverName);
26
- return parseMcpResourceOutput(`${JSON.stringify(message)}\n`);
27
- }
28
-
29
- async function postRemoteMcp(config = {}, method, params, serverName = '') {
30
- const controller = new AbortController();
31
- const timeout = setTimeout(() => controller.abort(), 1500);
32
- const body = JSON.stringify({ jsonrpc: '2.0', id: 1, method, params });
33
- try {
34
- if (config.transport === 'sse') {
35
- return await postLegacySseMcp(config, body, controller.signal, serverName);
36
- }
37
- const response = await fetch(config.url, {
38
- method: 'POST',
39
- headers: remoteMcpHeaders(config, 'application/json, text/event-stream', serverName),
40
- body,
41
- signal: controller.signal,
42
- });
43
- rememberRemoteMcpSession(config, serverName, response);
44
- if (response.status === 400 && !remoteMcpSessions.has(remoteMcpSessionKey(config, serverName)) && await initializeRemoteMcpSession(config, serverName, controller.signal)) {
45
- const retry = await fetch(config.url, {
46
- method: 'POST',
47
- headers: remoteMcpHeaders(config, 'application/json, text/event-stream', serverName),
48
- body,
49
- signal: controller.signal,
50
- });
51
- rememberRemoteMcpSession(config, serverName, retry);
52
- return parseRemoteMcpResponse(await retry.text());
53
- }
54
- if (response.status === 401 && await refreshMcpOauthToken(serverName, config)) {
55
- const retry = await fetch(config.url, {
56
- method: 'POST',
57
- headers: remoteMcpHeaders(config, 'application/json, text/event-stream', serverName),
58
- body,
59
- signal: controller.signal,
60
- });
61
- rememberRemoteMcpSession(config, serverName, retry);
62
- return parseRemoteMcpResponse(await retry.text());
63
- }
64
- if (response.status >= 400 && response.status < 500) {
65
- return await postLegacySseMcp(config, body, controller.signal, serverName);
66
- }
67
- const text = await response.text();
68
- return parseRemoteMcpResponse(text);
69
- } catch {
70
- return {};
71
- } finally {
72
- clearTimeout(timeout);
73
- }
74
- }
@@ -1,187 +0,0 @@
1
- import { pathToFileURL } from 'node:url';
2
- import { createPluginConfigurationApi } from './configuration.mjs';
3
- import {
4
- filePathFromURI,
5
- filesModifiedByToolCall,
6
- isPluginUINotAvailableError,
7
- runPluginShell,
8
- toolCallsInMessages,
9
- } from './helpers.mjs';
10
- import {
11
- createPluginAI,
12
- createPluginExperimentalApi,
13
- createPluginLogger,
14
- createPluginSystem,
15
- createPluginThreadContext,
16
- pluginEnvConfirm,
17
- pluginEnvInput,
18
- pluginEnvSelection,
19
- } from './subsystems.mjs';
20
- import { createSubscription } from './configuration.mjs';
21
- import {
22
- removeFirst,
23
- validateCommandAvailability,
24
- validatePluginCommand,
25
- validatePluginConfirmOptions,
26
- validatePluginInputOptions,
27
- validatePluginNotifyMessage,
28
- validatePluginSelectOptions,
29
- validatePluginToolDefinition,
30
- validatePluginToolName,
31
- } from './validators.mjs';
32
-
33
- export function createPluginApi({ cwd, runtime }) {
34
- return {
35
- registerTool(tool) {
36
- const summary = runtime.currentPlugin;
37
- validatePluginToolName(tool?.name);
38
- validatePluginToolDefinition(tool);
39
- const registeredTool = { ...tool };
40
- if (tool?.name) summary?.tools.push(tool.name);
41
- runtime.tools.push(registeredTool);
42
- return createSubscription(() => {
43
- runtime.tools = runtime.tools.filter((entry) => entry !== registeredTool);
44
- if (tool?.name && summary) removeFirst(summary.tools, tool.name);
45
- });
46
- },
47
- registerCommand(name, metadata = {}, handler = async () => undefined) {
48
- const summary = runtime.currentPlugin;
49
- validatePluginCommand(name, metadata);
50
- summary?.commands.push(name);
51
- const commandMetadata = {
52
- ...metadata,
53
- category: metadata.category ?? summary?.name,
54
- };
55
- const command = {
56
- name,
57
- metadata: commandMetadata,
58
- handler,
59
- availability: validateCommandAvailability(name, commandMetadata.availability ?? { type: 'enabled' }),
60
- };
61
- runtime.commands.push(command);
62
- return {
63
- setAvailability(availability) {
64
- command.availability = validateCommandAvailability(name, availability);
65
- },
66
- ...createSubscription(() => {
67
- runtime.commands = runtime.commands.filter((entry) => entry !== command);
68
- if (summary) removeFirst(summary.commands, name);
69
- }),
70
- };
71
- },
72
- on(eventName, handler) {
73
- const summary = runtime.currentPlugin;
74
- summary?.events.push(eventName);
75
- if (!runtime.handlers[eventName]) runtime.handlers[eventName] = [];
76
- runtime.handlers[eventName].push(handler);
77
- return createSubscription(() => {
78
- runtime.handlers[eventName] = (runtime.handlers[eventName] ?? []).filter((entry) => entry !== handler);
79
- if (summary) removeFirst(summary.events, eventName);
80
- });
81
- },
82
- configuration: createPluginConfigurationApi(cwd, runtime),
83
- ai: createPluginAI(),
84
- helpers: {
85
- shellCommandFromToolCall(event = {}) {
86
- if (event.tool !== 'Bash' && event.tool !== 'shell_command') return null;
87
- const command = event.input?.command ?? event.input?.cmd;
88
- if (typeof command !== 'string' || !command) return null;
89
- const dir = typeof event.input?.dir === 'string' && event.input.dir ? event.input.dir : undefined;
90
- return dir ? { command, dir } : { command };
91
- },
92
- toolCallsInMessages(messages = []) {
93
- return toolCallsInMessages(messages);
94
- },
95
- filesModifiedByToolCall(event = {}) {
96
- const files = filesModifiedByToolCall(event);
97
- return files ? files.map((filePath) => pathToFileURL(filePath)) : null;
98
- },
99
- filePathFromURI(uri) {
100
- return filePathFromURI(uri);
101
- },
102
- isPluginUINotAvailableError(error) {
103
- return isPluginUINotAvailableError(error);
104
- },
105
- },
106
- logger: createPluginLogger(),
107
- system: createPluginSystem((target) => {
108
- runtime.notifications.push(String(target));
109
- }),
110
- ui: {
111
- async notify(message) {
112
- runtime.notifications.push(validatePluginNotifyMessage(message));
113
- },
114
- confirm: async (options) => pluginEnvConfirm(options),
115
- input: async (options) => pluginEnvInput(options),
116
- select: async (options) => pluginEnvSelection(options),
117
- },
118
- $: (strings, ...values) => runPluginShell(cwd, strings, values),
119
- experimental: createPluginExperimentalApi(runtime),
120
- };
121
- }
122
-
123
- export function createPluginContext(event = {}) {
124
- return {
125
- logger: createPluginLogger(),
126
- $: (strings, ...values) => runPluginShell(process.cwd(), strings, values),
127
- ui: {
128
- async notify(message) {
129
- validatePluginNotifyMessage(message);
130
- },
131
- confirm: async (options) => {
132
- validatePluginConfirmOptions(options);
133
- return false;
134
- },
135
- input: async (options) => {
136
- validatePluginInputOptions(options);
137
- return undefined;
138
- },
139
- select: async (options) => {
140
- validatePluginSelectOptions(options);
141
- return undefined;
142
- },
143
- },
144
- ai: createPluginAI(),
145
- system: createPluginSystem(),
146
- thread: createPluginThreadContext(event.thread?.id),
147
- };
148
- }
149
-
150
- export function createPluginCommandContext(cwd = process.cwd()) {
151
- const notifications = [];
152
- const context = {
153
- ui: {
154
- async notify(message) {
155
- notifications.push(validatePluginNotifyMessage(message));
156
- },
157
- confirm: async (options) => pluginEnvConfirm(options),
158
- input: async (options) => pluginEnvInput(options),
159
- select: async (options) => pluginEnvSelection(options),
160
- },
161
- system: createPluginSystem((target) => {
162
- notifications.push(String(target));
163
- }),
164
- ai: createPluginAI(),
165
- $: (strings, ...values) => runPluginShell(cwd, strings, values),
166
- thread: createPluginThreadContext(),
167
- };
168
- Object.defineProperty(context, 'notifications', {
169
- value: notifications,
170
- enumerable: false,
171
- });
172
- return context;
173
- }
174
-
175
- export function createPluginToolContext() {
176
- return {
177
- ui: {
178
- notify: async (message) => {
179
- validatePluginNotifyMessage(message);
180
- },
181
- confirm: async (options) => pluginEnvConfirm(options),
182
- input: async (options) => pluginEnvInput(options),
183
- select: async (options) => pluginEnvSelection(options),
184
- },
185
- logger: createPluginLogger(),
186
- };
187
- }
@@ -1,124 +0,0 @@
1
- import {
2
- SETTINGS_PREFIX,
3
- readEffectiveSettings,
4
- readSettings,
5
- readSettingsFile,
6
- writeSettings,
7
- writeSettingsFile,
8
- } from '../settings/load.mjs';
9
- import { findProjectRoot, workspaceSettingsFile } from '../settings/paths.mjs';
10
-
11
- export function createSubscription(dispose) {
12
- return {
13
- unsubscribe: dispose,
14
- };
15
- }
16
-
17
- export function createPluginConfigurationApi(cwd, runtime) {
18
- const observableSymbol = Symbol.observable ?? Symbol.for('observable');
19
- const api = {
20
- async get() {
21
- return pluginConfiguration(readEffectiveSettings());
22
- },
23
- async update(patch = {}, scope = 'workspace') {
24
- const normalizedPatch = normalizePluginConfigurationPatch(patch);
25
- const target = normalizePluginConfigurationTarget(scope);
26
- if (target === 'workspace') {
27
- const filePath = workspaceSettingsFile(findProjectRoot(cwd));
28
- await writeSettingsFile(filePath, { ...readSettingsFile(filePath), ...normalizedPatch });
29
- } else {
30
- await writeSettings({ ...readSettings(), ...normalizedPatch });
31
- }
32
- await notifyConfigurationSubscribers(runtime);
33
- },
34
- async delete(key, scope = 'workspace') {
35
- const normalizedKey = normalizePluginConfigurationKey(key);
36
- const target = normalizePluginConfigurationTarget(scope);
37
- if (target === 'workspace') {
38
- const filePath = workspaceSettingsFile(findProjectRoot(cwd));
39
- const settings = readSettingsFile(filePath);
40
- delete settings[normalizedKey];
41
- await writeSettingsFile(filePath, settings);
42
- } else {
43
- const settings = readSettings();
44
- delete settings[normalizedKey];
45
- await writeSettings(settings);
46
- }
47
- await notifyConfigurationSubscribers(runtime);
48
- },
49
- subscribe(handler) {
50
- validateObservableSubscriber(handler);
51
- runtime.configurationSubscribers.push(handler);
52
- return createSubscription(() => {
53
- runtime.configurationSubscribers = runtime.configurationSubscribers.filter((entry) => entry !== handler);
54
- });
55
- },
56
- pipe(op) {
57
- return op(api);
58
- },
59
- [observableSymbol]() {
60
- return api;
61
- },
62
- };
63
- return api;
64
- }
65
-
66
- function pluginConfiguration(settings = {}) {
67
- const config = { ...settings };
68
- for (const [key, value] of Object.entries(settings)) {
69
- if (key.startsWith(SETTINGS_PREFIX) && !Object.hasOwn(config, key.slice(SETTINGS_PREFIX.length))) {
70
- config[key.slice(SETTINGS_PREFIX.length)] = value;
71
- }
72
- }
73
- return config;
74
- }
75
-
76
- function normalizePluginConfigurationPatch(patch = {}) {
77
- if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
78
- throw new Error('plugin configuration update patch must be an object');
79
- }
80
- return Object.fromEntries(Object.entries(patch).map(([key, value]) => [
81
- normalizePluginConfigurationKey(key),
82
- value,
83
- ]));
84
- }
85
-
86
- function normalizePluginConfigurationKey(key) {
87
- if (typeof key !== 'string') {
88
- throw new Error('plugin configuration key must be a string');
89
- }
90
- const text = String(key);
91
- if (text.startsWith(SETTINGS_PREFIX)) return text;
92
- return `${SETTINGS_PREFIX}${text}`;
93
- }
94
-
95
- function normalizePluginConfigurationTarget(target = 'workspace') {
96
- if (target !== 'workspace' && target !== 'global') {
97
- throw new Error(`plugin configuration target must be workspace or global: ${String(target)}`);
98
- }
99
- return target;
100
- }
101
-
102
- async function notifyConfigurationSubscribers(runtime) {
103
- if (runtime.configurationSubscribers.length === 0) return;
104
- const config = pluginConfiguration(readEffectiveSettings());
105
- for (const subscriber of runtime.configurationSubscribers) {
106
- await notifyObservableSubscriber(subscriber, config);
107
- }
108
- }
109
-
110
- export async function notifyObservableSubscriber(subscriber, value) {
111
- if (typeof subscriber === 'function') {
112
- await subscriber(value);
113
- return;
114
- }
115
- if (typeof subscriber?.next === 'function') {
116
- await subscriber.next(value);
117
- }
118
- }
119
-
120
- export function validateObservableSubscriber(subscriber) {
121
- if (typeof subscriber === 'function') return;
122
- if (subscriber && typeof subscriber === 'object' && !Array.isArray(subscriber)) return;
123
- throw new Error('plugin observable subscriber must be a function or observer object');
124
- }
@@ -1,84 +0,0 @@
1
- import { existsSync, readdirSync } from 'node:fs';
2
- import path from 'node:path';
3
- import { pathToFileURL } from 'node:url';
4
- import { configDir, findProjectRoot } from '../settings/paths.mjs';
5
- import {
6
- createPluginApi,
7
- createPluginCommandContext,
8
- createPluginContext,
9
- createPluginToolContext,
10
- } from './api.mjs';
11
-
12
- export { createPluginCommandContext, createPluginToolContext };
13
-
14
- export function discoverPluginFiles(cwd) {
15
- const projectRoot = findProjectRoot(cwd);
16
- return [
17
- ...readPluginDir(path.join(projectRoot, '.coven-code', 'plugins'), 'project'),
18
- ...readPluginDir(path.join(configDir(), 'coven-code', 'plugins'), 'user'),
19
- ];
20
- }
21
-
22
- function readPluginDir(dir, source) {
23
- if (!existsSync(dir)) return [];
24
- return readdirSync(dir)
25
- .filter((entry) => entry.endsWith('.ts'))
26
- .sort((a, b) => a.localeCompare(b))
27
- .map((entry) => ({
28
- name: path.basename(entry, '.ts'),
29
- source,
30
- path: path.join(dir, entry),
31
- }));
32
- }
33
-
34
- export async function loadPlugins(cwd) {
35
- const runtime = {
36
- tools: [],
37
- commands: [],
38
- handlers: {},
39
- configurationSubscribers: [],
40
- notifications: [],
41
- statusItems: [],
42
- pluginSummaries: [],
43
- currentPlugin: undefined,
44
- };
45
- const seen = new Set();
46
- const api = createPluginApi({ cwd, runtime });
47
- for (const plugin of discoverPluginFiles(cwd)) {
48
- const module = await import(pathToFileURL(plugin.path).href);
49
- const summary = {
50
- name: plugin.name,
51
- source: plugin.source,
52
- path: plugin.path,
53
- tools: [],
54
- commands: [],
55
- events: [],
56
- };
57
- runtime.currentPlugin = summary;
58
- if (typeof module.default === 'function') await module.default(api);
59
- runtime.pluginSummaries.push(summary);
60
- runtime.currentPlugin = undefined;
61
- }
62
- runtime.tools = runtime.tools.filter((tool) => {
63
- if (!tool?.name || seen.has(tool.name)) return false;
64
- seen.add(tool.name);
65
- return true;
66
- });
67
- runtime.commands = runtime.commands.filter((command) => command?.name);
68
- return runtime;
69
- }
70
-
71
- export async function listPluginTools(cwd) {
72
- return (await loadPlugins(cwd)).tools;
73
- }
74
-
75
- export async function runPluginEventHandlers(handlers = [], event = {}, validateResult = () => {}) {
76
- let decision;
77
- for (const handler of handlers) {
78
- const result = await handler(event, createPluginContext(event));
79
- if (result) validateResult(result);
80
- if (!result || result.action === 'allow') continue;
81
- decision ??= result;
82
- }
83
- return decision ?? { action: 'allow' };
84
- }