@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.
- package/README.md +145 -0
- package/bin/coven-code-sdk.mjs +12 -0
- package/bin/coven-code.mjs +19 -0
- package/docs/CLI.md +192 -0
- package/docs/CONFIGURATION.md +107 -0
- package/docs/DEVELOPMENT.md +104 -0
- package/docs/DOGFOOD-PROTOCOL.md +263 -0
- package/docs/MCP-SKILLS-PLUGINS.md +127 -0
- package/docs/README.md +38 -0
- package/docs/RELEASE.md +33 -0
- package/docs/SDK.md +107 -0
- package/docs/superpowers/plans/2026-05-25-coven-code-panel-tui.md +904 -0
- package/docs/superpowers/plans/2026-05-25-coven-code-rebrand.md +670 -0
- package/docs/superpowers/specs/2026-05-25-coven-code-panel-tui-design.md +235 -0
- package/docs/superpowers/specs/2026-05-26-slash-first-tui-review.md +63 -0
- package/package.json +36 -0
- package/src/agent/lane.mjs +136 -0
- package/src/agent/local.mjs +95 -0
- package/src/cli/dispatch.mjs +66 -0
- package/src/cli/execute.mjs +588 -0
- package/src/cli/help.mjs +58 -0
- package/src/cli/interactive-core.mjs +302 -0
- package/src/cli/notifications.mjs +13 -0
- package/src/cli/parse.mjs +83 -0
- package/src/cli/reasoning.mjs +45 -0
- package/src/cli/refs.mjs +162 -0
- package/src/cli/repl.mjs +61 -0
- package/src/cli/slash-commands.mjs +357 -0
- package/src/cli/stream-json.mjs +116 -0
- package/src/cli/tui.mjs +757 -0
- package/src/commands/agents.mjs +53 -0
- package/src/commands/config.mjs +27 -0
- package/src/commands/ide.mjs +17 -0
- package/src/commands/login.mjs +84 -0
- package/src/commands/mcp.mjs +176 -0
- package/src/commands/permissions.mjs +328 -0
- package/src/commands/plugins.mjs +86 -0
- package/src/commands/review.mjs +74 -0
- package/src/commands/skill.mjs +23 -0
- package/src/commands/threads.mjs +165 -0
- package/src/commands/tools.mjs +77 -0
- package/src/commands/update.mjs +31 -0
- package/src/commands/usage.mjs +34 -0
- package/src/constants.mjs +46 -0
- package/src/main.mjs +87 -0
- package/src/mcp/discover.mjs +154 -0
- package/src/mcp/permissions.mjs +52 -0
- package/src/mcp/probe.mjs +424 -0
- package/src/mcp/registry.mjs +96 -0
- package/src/plugins/discover.mjs +880 -0
- package/src/sdk-install.mjs +187 -0
- package/src/sdk.mjs +314 -0
- package/src/settings/load.mjs +134 -0
- package/src/settings/paths.mjs +101 -0
- package/src/skills/builtin/building-skills/SKILL.md +20 -0
- package/src/skills/discover.mjs +95 -0
- package/src/threads/store.mjs +176 -0
- package/src/tools/builtin/bash.mjs +110 -0
- package/src/tools/builtin/create-file.mjs +66 -0
- package/src/tools/builtin/edit-file.mjs +76 -0
- package/src/tools/builtin/finder.mjs +73 -0
- package/src/tools/builtin/glob.mjs +74 -0
- package/src/tools/builtin/grep.mjs +82 -0
- package/src/tools/builtin/index.mjs +83 -0
- package/src/tools/builtin/librarian.mjs +97 -0
- package/src/tools/builtin/look-at.mjs +92 -0
- package/src/tools/builtin/mcp.mjs +51 -0
- package/src/tools/builtin/mermaid.mjs +59 -0
- package/src/tools/builtin/oracle.mjs +56 -0
- package/src/tools/builtin/painter.mjs +81 -0
- package/src/tools/builtin/plugin-tool.mjs +53 -0
- package/src/tools/builtin/read-mcp-resource.mjs +63 -0
- package/src/tools/builtin/read-web-page.mjs +72 -0
- package/src/tools/builtin/read.mjs +59 -0
- package/src/tools/builtin/runtime.mjs +215 -0
- package/src/tools/builtin/task.mjs +63 -0
- package/src/tools/builtin/toolbox-tool.mjs +57 -0
- package/src/tools/builtin/undo-edit.mjs +97 -0
- package/src/tools/builtin/web-search.mjs +128 -0
- package/src/tools/toolbox.mjs +273 -0
- package/src/util/fs.mjs +13 -0
- package/src/util/glob.mjs +46 -0
- package/src/util/html.mjs +21 -0
- package/src/util/media.mjs +13 -0
- package/src/util/shell.mjs +24 -0
- package/src/util/table.mjs +11 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { chmod, mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { VERSION } from './constants.mjs';
|
|
8
|
+
|
|
9
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
10
|
+
const covenCodeBin = path.join(repoRoot, 'bin', 'coven-code.mjs');
|
|
11
|
+
|
|
12
|
+
export async function runInstallCommand(args = []) {
|
|
13
|
+
const command = args[0];
|
|
14
|
+
if (!command || command === '--help' || command === '-h' || command === 'help') {
|
|
15
|
+
printUsage();
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
if (command !== 'install') {
|
|
19
|
+
console.error(`Unknown command: ${command}`);
|
|
20
|
+
printUsage();
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const parsed = parseInstallOptions(args.slice(1));
|
|
25
|
+
if (parsed.errorMessage) {
|
|
26
|
+
console.error(parsed.errorMessage);
|
|
27
|
+
printUsage();
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
|
30
|
+
if (parsed.showHelp) {
|
|
31
|
+
printUsage();
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
await installLocalCovenCode(parsed.options);
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function printUsage() {
|
|
40
|
+
console.log('Usage: coven-code-sdk install [--force]');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseInstallOptions(args = []) {
|
|
44
|
+
const options = { forceInstall: false };
|
|
45
|
+
for (const arg of args) {
|
|
46
|
+
if (arg === '--help' || arg === '-h') return { options, showHelp: true, errorMessage: null };
|
|
47
|
+
if (arg === '--force') {
|
|
48
|
+
options.forceInstall = true;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
return { options, showHelp: false, errorMessage: `Unknown option for install: ${arg}` };
|
|
52
|
+
}
|
|
53
|
+
return { options, showHelp: false, errorMessage: null };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function installLocalCovenCode(options = {}) {
|
|
57
|
+
if (!options.forceInstall) {
|
|
58
|
+
const installedCli = findPreManagedInstalledCli();
|
|
59
|
+
if (installedCli) {
|
|
60
|
+
console.log(`Coven Code CLI ${installedCli.version} already satisfies minimum ${VERSION} (${installedCli.source}).`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
console.log('Forcing SDK-managed install; skipping existing CLI detection.');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const target = sdkManagedCovenCodePath();
|
|
68
|
+
if (!options.forceInstall && existsSync(target)) {
|
|
69
|
+
const version = installedCovenCodeVersion(target);
|
|
70
|
+
if (version) {
|
|
71
|
+
console.log(`Coven Code CLI ${version} already satisfies minimum ${VERSION} (SDK_MANAGED).`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!options.forceInstall) {
|
|
77
|
+
const installedCli = findPostManagedInstalledCli();
|
|
78
|
+
if (installedCli) {
|
|
79
|
+
console.log(`Coven Code CLI ${installedCli.version} already satisfies minimum ${VERSION} (${installedCli.source}).`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
85
|
+
await writeFile(target, covenCodeWrapperSource(), 'utf8');
|
|
86
|
+
if (process.platform !== 'win32') await chmod(target, 0o755);
|
|
87
|
+
console.log(`Coven Code CLI ${VERSION} installed at ${target}.`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function findPreManagedInstalledCli() {
|
|
91
|
+
const cliPath = process.env.COVEN_CODE_CLI_PATH;
|
|
92
|
+
const envVersion = cliPath && existsSync(cliPath) ? installedCovenCodeVersion(cliPath) : undefined;
|
|
93
|
+
if (envVersion) return { source: 'COVEN_CODE_CLI_PATH', version: envVersion };
|
|
94
|
+
|
|
95
|
+
const localPackage = findLocalCovenCodePackage();
|
|
96
|
+
if (localPackage) return localPackage;
|
|
97
|
+
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function findPostManagedInstalledCli() {
|
|
102
|
+
const covenCodeHomePath = path.join(resolveCovenCodeHome(), 'bin', process.platform === 'win32' ? 'coven-code.cmd' : 'coven-code');
|
|
103
|
+
const covenCodeHomeVersion = existsSync(covenCodeHomePath) ? installedCovenCodeVersion(covenCodeHomePath) : undefined;
|
|
104
|
+
if (covenCodeHomeVersion) return { source: process.env.COVEN_CODE_HOME ? 'COVEN_CODE_HOME' : 'COVEN_CODE_HOME', version: covenCodeHomeVersion };
|
|
105
|
+
|
|
106
|
+
const pathCli = findCovenCodeOnPath();
|
|
107
|
+
if (pathCli) return pathCli;
|
|
108
|
+
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function findLocalCovenCodePackage() {
|
|
113
|
+
for (const packageName of ['@opencoven/coven-code']) {
|
|
114
|
+
const packageJsonPath = findNodeModulePackageJson(packageName);
|
|
115
|
+
if (!packageJsonPath) continue;
|
|
116
|
+
try {
|
|
117
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
118
|
+
const bin = typeof packageJson.bin?.['coven-code'] === 'string' ? packageJson.bin['coven-code'] : undefined;
|
|
119
|
+
if (!bin) continue;
|
|
120
|
+
const cliPath = path.resolve(path.dirname(packageJsonPath), bin);
|
|
121
|
+
const version = existsSync(cliPath) ? installedCovenCodeVersion(cliPath) : undefined;
|
|
122
|
+
if (version) return { source: 'LOCAL_NPM', version };
|
|
123
|
+
} catch {
|
|
124
|
+
// Try the next package candidate.
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function findNodeModulePackageJson(packageName) {
|
|
131
|
+
let current = process.cwd();
|
|
132
|
+
while (true) {
|
|
133
|
+
const candidate = path.join(current, 'node_modules', ...packageName.split('/'), 'package.json');
|
|
134
|
+
if (existsSync(candidate)) return candidate;
|
|
135
|
+
const parent = path.dirname(current);
|
|
136
|
+
if (parent === current) return undefined;
|
|
137
|
+
current = parent;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function findCovenCodeOnPath() {
|
|
142
|
+
for (const dir of process.env.PATH?.split(path.delimiter) ?? []) {
|
|
143
|
+
if (!dir) continue;
|
|
144
|
+
for (const name of covenCodeCommandNames()) {
|
|
145
|
+
const candidate = path.join(dir, name);
|
|
146
|
+
const version = existsSync(candidate) ? installedCovenCodeVersion(candidate) : undefined;
|
|
147
|
+
if (version) return { source: 'PATH', version };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function covenCodeCommandNames() {
|
|
154
|
+
return process.platform === 'win32' ? ['coven-code.exe', 'coven-code.cmd', 'coven-code'] : ['coven-code'];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function sdkManagedCovenCodePath() {
|
|
158
|
+
return path.join(resolveCovenCodeHome(), 'sdk', 'bin', process.platform === 'win32' ? 'coven-code.cmd' : 'coven-code');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function resolveCovenCodeHome() {
|
|
162
|
+
return process.env.COVEN_CODE_HOME || path.join(os.homedir(), '.coven-code');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function covenCodeWrapperSource() {
|
|
166
|
+
if (process.platform === 'win32') {
|
|
167
|
+
return `@echo off\r\n"${process.execPath}" "${covenCodeBin}" %*\r\n`;
|
|
168
|
+
}
|
|
169
|
+
return `#!/usr/bin/env node
|
|
170
|
+
import { spawnSync } from 'node:child_process';
|
|
171
|
+
|
|
172
|
+
const result = spawnSync(${JSON.stringify(process.execPath)}, [${JSON.stringify(covenCodeBin)}, ...process.argv.slice(2)], {
|
|
173
|
+
stdio: 'inherit',
|
|
174
|
+
});
|
|
175
|
+
if (result.error) {
|
|
176
|
+
console.error(result.error.stack || String(result.error));
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
process.exit(result.status ?? 0);
|
|
180
|
+
`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function installedCovenCodeVersion(command) {
|
|
184
|
+
const result = spawnSync(command, ['--version'], { encoding: 'utf8' });
|
|
185
|
+
if (result.status !== 0) return undefined;
|
|
186
|
+
return result.stdout.trim() || undefined;
|
|
187
|
+
}
|
package/src/sdk.mjs
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { appendFile, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import { createInterface } from 'node:readline';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { normalizeThreadVisibility, readThread, writeThread } from './threads/store.mjs';
|
|
10
|
+
import { parseJsonc } from './settings/load.mjs';
|
|
11
|
+
|
|
12
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
13
|
+
const covenCodeBin = path.join(repoRoot, 'bin', 'coven-code.mjs');
|
|
14
|
+
|
|
15
|
+
export const threads = {
|
|
16
|
+
async new(options = {}) {
|
|
17
|
+
return withEnv(options.env, async () => {
|
|
18
|
+
const now = new Date().toISOString();
|
|
19
|
+
const thread = {
|
|
20
|
+
id: `T-${randomUUID()}`,
|
|
21
|
+
title: '(empty thread)',
|
|
22
|
+
cwd: options.cwd ?? process.cwd(),
|
|
23
|
+
mode: options.mode ?? 'smart',
|
|
24
|
+
visibility: normalizeSdkThreadVisibility(options.visibility) ?? 'private',
|
|
25
|
+
labels: [],
|
|
26
|
+
archived: false,
|
|
27
|
+
createdAt: now,
|
|
28
|
+
updatedAt: now,
|
|
29
|
+
messages: [],
|
|
30
|
+
};
|
|
31
|
+
await writeThread(thread);
|
|
32
|
+
return thread.id;
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
async markdown({ threadId, env } = {}) {
|
|
37
|
+
if (!threadId) throw new Error('threads.markdown requires threadId');
|
|
38
|
+
return withEnv(env, async () => threadMarkdown(requireSdkThread(threadId)));
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export async function* execute({ prompt, options = {}, signal } = {}) {
|
|
43
|
+
if (prompt === undefined) throw new Error('execute requires a prompt');
|
|
44
|
+
const isStreamInput = isAsyncIterable(prompt);
|
|
45
|
+
const runSettings = await prepareRunSettings(options);
|
|
46
|
+
const args = executeArgs(prompt, { ...options, settingsFile: runSettings.settingsFile }, isStreamInput);
|
|
47
|
+
const covenCodeCommand = resolveCovenCodeCommand();
|
|
48
|
+
const started = Date.now();
|
|
49
|
+
await writeDebugLog(options, covenCodeCommand, args);
|
|
50
|
+
const child = spawn(covenCodeCommand.command, [...covenCodeCommand.args, ...args], {
|
|
51
|
+
cwd: options.cwd ?? process.cwd(),
|
|
52
|
+
env: { ...process.env, ...(options.env ?? {}) },
|
|
53
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
54
|
+
signal,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
let exitError;
|
|
58
|
+
const exitTask = waitForExit(child).catch((error) => {
|
|
59
|
+
exitError = error;
|
|
60
|
+
});
|
|
61
|
+
const stderr = [];
|
|
62
|
+
child.stderr.on('data', (chunk) => stderr.push(chunk));
|
|
63
|
+
let inputError;
|
|
64
|
+
const inputTask = (isStreamInput ? writeStreamInput(child, prompt, signal) : closeStdin(child))
|
|
65
|
+
.catch((error) => {
|
|
66
|
+
inputError = error;
|
|
67
|
+
});
|
|
68
|
+
const rl = createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
69
|
+
const closeChildStreams = () => {
|
|
70
|
+
rl.close();
|
|
71
|
+
child.stdout.destroy();
|
|
72
|
+
child.stderr.destroy();
|
|
73
|
+
};
|
|
74
|
+
signal?.addEventListener('abort', closeChildStreams, { once: true });
|
|
75
|
+
let sessionId = `T-${randomUUID()}`;
|
|
76
|
+
let emittedResult = false;
|
|
77
|
+
try {
|
|
78
|
+
for await (const line of rl) {
|
|
79
|
+
if (!line.trim()) continue;
|
|
80
|
+
const message = JSON.parse(line);
|
|
81
|
+
if (message.session_id) sessionId = message.session_id;
|
|
82
|
+
if (message.type === 'result') emittedResult = true;
|
|
83
|
+
yield message;
|
|
84
|
+
}
|
|
85
|
+
await inputTask;
|
|
86
|
+
if (inputError) throw inputError;
|
|
87
|
+
const status = await exitTask;
|
|
88
|
+
if (exitError) throw exitError;
|
|
89
|
+
if (status !== 0 && !emittedResult) {
|
|
90
|
+
yield {
|
|
91
|
+
type: 'result',
|
|
92
|
+
subtype: 'error_during_execution',
|
|
93
|
+
duration_ms: Date.now() - started,
|
|
94
|
+
is_error: true,
|
|
95
|
+
num_turns: 0,
|
|
96
|
+
error: Buffer.concat(stderr).toString('utf8').trim() || `coven-code exited with status ${status}`,
|
|
97
|
+
session_id: sessionId,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
} finally {
|
|
101
|
+
signal?.removeEventListener('abort', closeChildStreams);
|
|
102
|
+
rl.close();
|
|
103
|
+
await runSettings.cleanup();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function createUserMessage(text) {
|
|
108
|
+
return {
|
|
109
|
+
type: 'user',
|
|
110
|
+
message: {
|
|
111
|
+
role: 'user',
|
|
112
|
+
content: [{ type: 'text', text: String(text) }],
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function createPermission(tool, action, options = {}) {
|
|
118
|
+
if (action === 'delegate' && !options.to) {
|
|
119
|
+
throw new Error('delegate action requires "to" option');
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
tool,
|
|
123
|
+
action,
|
|
124
|
+
...(options.matches ? { matches: options.matches } : {}),
|
|
125
|
+
...(options.context ? { context: options.context } : {}),
|
|
126
|
+
...(options.to ? { to: options.to } : {}),
|
|
127
|
+
...(options.message ? { message: options.message } : {}),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function executeArgs(prompt, options, isStreamInput) {
|
|
132
|
+
const args = ['--execute'];
|
|
133
|
+
if (!isStreamInput) args.push(String(prompt));
|
|
134
|
+
args.push(options.thinking ? '--stream-json-thinking' : '--stream-json');
|
|
135
|
+
if (isStreamInput) args.push('--stream-json-input');
|
|
136
|
+
if (options.dangerouslyAllowAll) args.push('--dangerously-allow-all');
|
|
137
|
+
if (options.archive) args.push('--archive');
|
|
138
|
+
if (options.mode) args.push('--mode', options.mode);
|
|
139
|
+
if (options.reasoningEffort) args.push('--reasoning-effort', options.reasoningEffort);
|
|
140
|
+
const visibility = normalizeSdkThreadVisibility(options.visibility) ?? (options.continue ? undefined : 'workspace');
|
|
141
|
+
if (visibility) args.push('--visibility', visibility);
|
|
142
|
+
if (options.settingsFile) args.push('--settings-file', options.settingsFile);
|
|
143
|
+
if (options.continue) args.push('--continue', ...(typeof options.continue === 'string' ? [options.continue] : []));
|
|
144
|
+
if (options.toolbox) args.push('--toolbox', options.toolbox);
|
|
145
|
+
if (options.skills) args.push('--skills', options.skills);
|
|
146
|
+
if (options.mcpConfig) args.push('--mcp-config', typeof options.mcpConfig === 'string' ? options.mcpConfig : JSON.stringify(options.mcpConfig));
|
|
147
|
+
for (const label of options.labels ?? []) args.push('--label', label);
|
|
148
|
+
return args;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function normalizeSdkThreadVisibility(visibility) {
|
|
152
|
+
if (visibility === 'team') return 'workspace';
|
|
153
|
+
return normalizeThreadVisibility(visibility);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function writeStreamInput(child, prompt, signal) {
|
|
157
|
+
const iterator = prompt[Symbol.asyncIterator]();
|
|
158
|
+
try {
|
|
159
|
+
while (true) {
|
|
160
|
+
const { value: message, done } = await nextWithAbort(iterator, signal);
|
|
161
|
+
if (done) break;
|
|
162
|
+
if (child.stdin.destroyed) break;
|
|
163
|
+
child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
164
|
+
}
|
|
165
|
+
child.stdin.end();
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (!isAbortError(error)) throw error;
|
|
168
|
+
child.stdin.destroy();
|
|
169
|
+
try {
|
|
170
|
+
iterator.return?.().catch?.(() => {});
|
|
171
|
+
} catch {
|
|
172
|
+
// Best-effort generator cleanup; abort should not wait on a slow prompt source.
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function nextWithAbort(iterator, signal) {
|
|
178
|
+
if (!signal) return iterator.next();
|
|
179
|
+
if (signal.aborted) return Promise.reject(abortReason(signal));
|
|
180
|
+
return new Promise((resolve, reject) => {
|
|
181
|
+
const onAbort = () => reject(abortReason(signal));
|
|
182
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
183
|
+
iterator.next().then(resolve, reject).finally(() => {
|
|
184
|
+
signal.removeEventListener('abort', onAbort);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function abortReason(signal) {
|
|
190
|
+
return signal.reason instanceof Error ? signal.reason : new Error('aborted');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function isAbortError(error) {
|
|
194
|
+
return error?.name === 'AbortError' || error?.code === 'ABORT_ERR' || /abort/i.test(error?.message ?? '');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function closeStdin(child) {
|
|
198
|
+
child.stdin.end();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function waitForExit(child) {
|
|
202
|
+
return new Promise((resolve, reject) => {
|
|
203
|
+
child.on('error', reject);
|
|
204
|
+
child.on('close', (code) => resolve(code ?? 0));
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function isAsyncIterable(value) {
|
|
209
|
+
return value && typeof value[Symbol.asyncIterator] === 'function';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function requireSdkThread(threadId) {
|
|
213
|
+
const thread = readThread(threadId);
|
|
214
|
+
if (!thread) throw new Error(`Unknown thread: ${threadId}`);
|
|
215
|
+
return thread;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function threadMarkdown(thread) {
|
|
219
|
+
const lines = [
|
|
220
|
+
`# Thread ${thread.id}`,
|
|
221
|
+
'',
|
|
222
|
+
`Visibility: ${thread.visibility ?? 'private'}`,
|
|
223
|
+
`Status: ${thread.archived ? 'archived' : 'active'}`,
|
|
224
|
+
`CWD: ${thread.cwd}`,
|
|
225
|
+
'',
|
|
226
|
+
];
|
|
227
|
+
for (const message of thread.messages ?? []) {
|
|
228
|
+
lines.push(`## ${titleCaseRole(message.role)}`, '', String(message.content), '');
|
|
229
|
+
}
|
|
230
|
+
return `${lines.join('\n').trimEnd()}\n`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function titleCaseRole(role = '') {
|
|
234
|
+
return role ? `${role.slice(0, 1).toUpperCase()}${role.slice(1)}` : 'Message';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function withEnv(env = {}, fn) {
|
|
238
|
+
const previous = new Map();
|
|
239
|
+
for (const [key, value] of Object.entries(env ?? {})) {
|
|
240
|
+
previous.set(key, Object.hasOwn(process.env, key) ? process.env[key] : undefined);
|
|
241
|
+
process.env[key] = value;
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
return await fn();
|
|
245
|
+
} finally {
|
|
246
|
+
for (const [key, value] of previous) {
|
|
247
|
+
if (value === undefined) delete process.env[key];
|
|
248
|
+
else process.env[key] = value;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function prepareRunSettings(options = {}) {
|
|
254
|
+
if (!shouldWriteRunSettings(options)) return { settingsFile: options.settingsFile, cleanup: async () => {} };
|
|
255
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), 'coven-code-sdk-settings-'));
|
|
256
|
+
const settingsFile = path.join(dir, 'settings.json');
|
|
257
|
+
const baseSettings = options.settingsFile ? await readJsonSettings(sdkOptionsPath(options.settingsFile, options.cwd)) : {};
|
|
258
|
+
const settings = { ...baseSettings };
|
|
259
|
+
if (Array.isArray(options.permissions)) settings['covenCode.permissions'] = options.permissions;
|
|
260
|
+
if (Array.isArray(options.enabledTools)) settings['covenCode.tools.enable'] = options.enabledTools;
|
|
261
|
+
if (typeof options.systemPrompt === 'string') settings['covenCode.systemPrompt'] = options.systemPrompt;
|
|
262
|
+
if (typeof options.skills === 'string') settings['covenCode.skills.path'] = options.skills;
|
|
263
|
+
await writeFile(settingsFile, `${JSON.stringify(settings, null, 2)}\n`, 'utf8');
|
|
264
|
+
return {
|
|
265
|
+
settingsFile,
|
|
266
|
+
cleanup: async () => {
|
|
267
|
+
await rm(dir, { recursive: true, force: true });
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function shouldWriteRunSettings(options = {}) {
|
|
273
|
+
return Array.isArray(options.permissions)
|
|
274
|
+
|| Array.isArray(options.enabledTools)
|
|
275
|
+
|| typeof options.systemPrompt === 'string'
|
|
276
|
+
|| typeof options.skills === 'string';
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function readJsonSettings(filePath) {
|
|
280
|
+
try {
|
|
281
|
+
return parseJsonc(await readFile(filePath, 'utf8'));
|
|
282
|
+
} catch {
|
|
283
|
+
return {};
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function writeDebugLog(options = {}, covenCodeCommand = resolveCovenCodeCommand(), args = []) {
|
|
288
|
+
if (options.logLevel !== 'debug') return;
|
|
289
|
+
const line = `level=debug cwd=${options.cwd ?? process.cwd()} argv=${[covenCodeCommand.command, ...covenCodeCommand.args, ...args].map(JSON.stringify).join(' ')}\n`;
|
|
290
|
+
process.stderr.write(line);
|
|
291
|
+
if (!options.logFile) return;
|
|
292
|
+
const logFile = sdkOptionsPath(options.logFile, options.cwd);
|
|
293
|
+
await mkdir(path.dirname(logFile), { recursive: true });
|
|
294
|
+
await appendFile(logFile, line, 'utf8');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function sdkOptionsPath(filePath, cwd = process.cwd()) {
|
|
298
|
+
if (!filePath || path.isAbsolute(filePath)) return filePath;
|
|
299
|
+
return path.resolve(cwd, filePath);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function resolveCovenCodeCommand() {
|
|
303
|
+
const cliPath = process.env.COVEN_CODE_CLI_PATH;
|
|
304
|
+
if (cliPath && existsSync(cliPath)) {
|
|
305
|
+
return isNodeScriptPath(cliPath)
|
|
306
|
+
? { command: process.execPath, args: [cliPath] }
|
|
307
|
+
: { command: cliPath, args: [] };
|
|
308
|
+
}
|
|
309
|
+
return { command: process.execPath, args: [covenCodeBin] };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function isNodeScriptPath(filePath) {
|
|
313
|
+
return filePath.endsWith('.js') || filePath.endsWith('.mjs') || filePath.endsWith('.cjs');
|
|
314
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { findManagedSettingsFile, findWorkspaceSettingsFile, settingsFile } from './paths.mjs';
|
|
5
|
+
|
|
6
|
+
export const SETTINGS_PREFIX = 'covenCode.';
|
|
7
|
+
|
|
8
|
+
export function readSettings(parsed = {}) {
|
|
9
|
+
return readSettingsFile(settingsFile(parsed));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function readEffectiveSettings(parsed = {}, options = {}) {
|
|
13
|
+
const userSettings = readSettings(parsed);
|
|
14
|
+
const workspacePath = findWorkspaceSettingsFile(options.cwd ?? process.cwd());
|
|
15
|
+
const workspaceSettings = workspacePath ? readSettingsFile(workspacePath) : {};
|
|
16
|
+
return { ...userSettings, ...workspaceSettings, ...readManagedSettings() };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function readManagedSettings() {
|
|
20
|
+
return readSettingsFile(findManagedSettingsFile());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function writeSettings(settings, parsed = {}) {
|
|
24
|
+
await writeSettingsFile(settingsFile(parsed), settings);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function readSettingsFile(filePath) {
|
|
28
|
+
if (!filePath || !existsSync(filePath)) return {};
|
|
29
|
+
try {
|
|
30
|
+
return parseJsonc(readFileSync(filePath, 'utf8'));
|
|
31
|
+
} catch {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function writeSettingsFile(filePath, settings) {
|
|
37
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
38
|
+
await writeFile(filePath, `${JSON.stringify(settings, null, 2)}\n`, 'utf8');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function parseJsonc(text) {
|
|
42
|
+
return JSON.parse(removeJsonTrailingCommas(stripJsonComments(text)));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function stripJsonComments(text) {
|
|
46
|
+
let output = '';
|
|
47
|
+
let inString = false;
|
|
48
|
+
let escaped = false;
|
|
49
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
50
|
+
const char = text[index];
|
|
51
|
+
const next = text[index + 1];
|
|
52
|
+
|
|
53
|
+
if (inString) {
|
|
54
|
+
output += char;
|
|
55
|
+
if (escaped) escaped = false;
|
|
56
|
+
else if (char === '\\') escaped = true;
|
|
57
|
+
else if (char === '"') inString = false;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (char === '"') {
|
|
62
|
+
inString = true;
|
|
63
|
+
output += char;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (char === '/' && next === '/') {
|
|
68
|
+
while (index < text.length && text[index] !== '\n') index += 1;
|
|
69
|
+
if (index < text.length) output += text[index];
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (char === '/' && next === '*') {
|
|
74
|
+
index += 2;
|
|
75
|
+
while (index < text.length && !(text[index] === '*' && text[index + 1] === '/')) index += 1;
|
|
76
|
+
index += 1;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
output += char;
|
|
81
|
+
}
|
|
82
|
+
return output;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function removeJsonTrailingCommas(text) {
|
|
86
|
+
let output = '';
|
|
87
|
+
let inString = false;
|
|
88
|
+
let escaped = false;
|
|
89
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
90
|
+
const char = text[index];
|
|
91
|
+
|
|
92
|
+
if (inString) {
|
|
93
|
+
output += char;
|
|
94
|
+
if (escaped) escaped = false;
|
|
95
|
+
else if (char === '\\') escaped = true;
|
|
96
|
+
else if (char === '"') inString = false;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (char === '"') {
|
|
101
|
+
inString = true;
|
|
102
|
+
output += char;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (char === ',') {
|
|
107
|
+
let lookahead = index + 1;
|
|
108
|
+
while (/\s/.test(text[lookahead] ?? '')) lookahead += 1;
|
|
109
|
+
if (text[lookahead] === '}' || text[lookahead] === ']') continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
output += char;
|
|
113
|
+
}
|
|
114
|
+
return output;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function readFrontmatter(text) {
|
|
118
|
+
const metadata = {};
|
|
119
|
+
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
120
|
+
if (!match) return metadata;
|
|
121
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
122
|
+
const entry = line.match(/^([A-Za-z0-9_.-]+):\s*(.*)$/);
|
|
123
|
+
if (entry) metadata[entry[1]] = entry[2].replace(/^["']|["']$/g, '');
|
|
124
|
+
}
|
|
125
|
+
return metadata;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function splitListValue(value) {
|
|
129
|
+
const text = String(value).trim();
|
|
130
|
+
if (text.startsWith('[') && text.endsWith(']')) {
|
|
131
|
+
return text.slice(1, -1).split(',').map((entry) => entry.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
|
|
132
|
+
}
|
|
133
|
+
return text.split(/[,\s]+/).map((entry) => entry.trim()).filter(Boolean);
|
|
134
|
+
}
|