@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,302 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { appendFile, mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { AGENT_MODES, CLI_NAME, CONFIG_SUBDIR, REPL_HISTORY_LIMIT } from '../constants.mjs';
|
|
7
|
+
import { configDir } from '../settings/paths.mjs';
|
|
8
|
+
import { shellQuote, splitShellWords } from '../util/shell.mjs';
|
|
9
|
+
import { latestActiveThread, requireThread } from '../threads/store.mjs';
|
|
10
|
+
import { runCommand } from './dispatch.mjs';
|
|
11
|
+
import { runExecute } from './execute.mjs';
|
|
12
|
+
import {
|
|
13
|
+
coerceReasoningEffortForMode,
|
|
14
|
+
nextReasoningEffortForMode,
|
|
15
|
+
reasoningEffortForMode,
|
|
16
|
+
} from './reasoning.mjs';
|
|
17
|
+
import {
|
|
18
|
+
buildSlashCommandCatalog,
|
|
19
|
+
findSlashCommand,
|
|
20
|
+
formatSlashCommandDetails,
|
|
21
|
+
formatSlashHelpLines,
|
|
22
|
+
skillPromptFromSlashCommand,
|
|
23
|
+
} from './slash-commands.mjs';
|
|
24
|
+
|
|
25
|
+
export function createInteractiveSession(parsed, options = {}) {
|
|
26
|
+
return {
|
|
27
|
+
parsed,
|
|
28
|
+
thread: options.thread,
|
|
29
|
+
cwd: options.cwd ?? process.cwd(),
|
|
30
|
+
queuedMessages: [],
|
|
31
|
+
commandRunner: options.commandRunner ?? runCommand,
|
|
32
|
+
executeRunner: options.executeRunner ?? runExecute,
|
|
33
|
+
editorReader: options.editorReader ?? readEditorPrompt,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function handleInteractiveInput(session, text) {
|
|
38
|
+
if (!text) return { kind: 'empty', lines: [] };
|
|
39
|
+
if (text === '/exit' || text === '/quit') return { kind: 'exit', lines: [] };
|
|
40
|
+
if (text === '/help') return { kind: 'help', lines: await sessionSlashHelpLines(session) };
|
|
41
|
+
if (!text.startsWith('/')) {
|
|
42
|
+
session.thread = await runInteractiveTurn(session.parsed, text, session.thread, session.executeRunner);
|
|
43
|
+
while (session.queuedMessages.length > 0) {
|
|
44
|
+
session.thread = await runInteractiveTurn(session.parsed, session.queuedMessages.shift(), session.thread, session.executeRunner);
|
|
45
|
+
}
|
|
46
|
+
return { kind: 'turn', lines: [] };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const tokens = splitShellWords(text.slice(1));
|
|
50
|
+
const [cmd, ...rest] = tokens;
|
|
51
|
+
const catalog = await safeSlashCommandCatalog(session);
|
|
52
|
+
if (!cmd) return { kind: 'help', lines: formatCatalogHelpLines(catalog) };
|
|
53
|
+
const catalogEntry = findSlashCommand(catalog, cmd);
|
|
54
|
+
|
|
55
|
+
if (catalogEntry?.source === 'skill') {
|
|
56
|
+
if (rest.length === 0) {
|
|
57
|
+
return { kind: 'command', lines: formatSlashCommandDetails(catalogEntry) };
|
|
58
|
+
}
|
|
59
|
+
await submitPromptAndQueue(session, skillPromptFromSlashCommand(catalogEntry, rest.join(' ')));
|
|
60
|
+
return { kind: 'turn', lines: [] };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (catalogEntry?.source === 'plugin' && catalogEntry.availability?.type === 'disabled') {
|
|
64
|
+
return {
|
|
65
|
+
kind: 'error',
|
|
66
|
+
lines: [`${CLI_NAME}: ${catalogEntry.availability.reason ?? `Plugin command disabled: ${catalogEntry.name}`}`],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (cmd === 'mode') {
|
|
71
|
+
const nextMode = rest[0];
|
|
72
|
+
if (!nextMode) return { kind: 'command', lines: [`mode: ${session.parsed.mode}`] };
|
|
73
|
+
if (!AGENT_MODES.includes(nextMode)) return { kind: 'error', lines: [`${CLI_NAME}: Unknown mode: ${nextMode}`] };
|
|
74
|
+
session.parsed.mode = nextMode;
|
|
75
|
+
session.parsed.reasoningEffort = coerceReasoningEffortForMode(session.parsed.mode, session.parsed.reasoningEffort);
|
|
76
|
+
return { kind: 'command', lines: [`mode: ${session.parsed.mode}`, `reasoning effort: ${session.parsed.reasoningEffort}`] };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (cmd === 'reasoning') {
|
|
80
|
+
try {
|
|
81
|
+
const nextEffort = rest[0] === 'next'
|
|
82
|
+
? nextReasoningEffortForMode(session.parsed.mode, session.parsed.reasoningEffort)
|
|
83
|
+
: rest[0];
|
|
84
|
+
session.parsed.reasoningEffort = reasoningEffortForMode(session.parsed.mode, nextEffort ?? session.parsed.reasoningEffort);
|
|
85
|
+
return { kind: 'command', lines: [`reasoning effort: ${session.parsed.reasoningEffort}`] };
|
|
86
|
+
} catch (error) {
|
|
87
|
+
return { kind: 'error', lines: [`${CLI_NAME}: ${error?.message ?? error}`] };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (cmd === 'queue') {
|
|
92
|
+
const queued = rest.join(' ').trim();
|
|
93
|
+
if (!queued) return { kind: 'error', lines: [`${CLI_NAME}: /queue requires a prompt`] };
|
|
94
|
+
session.queuedMessages.push(queued);
|
|
95
|
+
return { kind: 'command', lines: [`queued: ${queued}`] };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (cmd === 'new') {
|
|
99
|
+
session.thread = undefined;
|
|
100
|
+
return { kind: 'command', lines: ['new thread'] };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (cmd === 'continue') {
|
|
104
|
+
try {
|
|
105
|
+
session.thread = interactiveContinuationThread(rest[0]);
|
|
106
|
+
return { kind: 'command', lines: [`continued: ${session.thread.id}`] };
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return { kind: 'error', lines: [`${CLI_NAME}: ${error?.message ?? error}`] };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (cmd === `${CLI_NAME}:` && rest.join(' ') === 'help') return { kind: 'help', lines: await sessionSlashHelpLines(session) };
|
|
113
|
+
|
|
114
|
+
if (cmd === 'editor') {
|
|
115
|
+
const edited = await session.editorReader();
|
|
116
|
+
if (edited) await submitPromptAndQueue(session, edited);
|
|
117
|
+
return { kind: 'command', lines: [] };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (cmd === 'edit') {
|
|
121
|
+
try {
|
|
122
|
+
if (!session.thread) throw new Error('No current thread to edit');
|
|
123
|
+
const editTarget = editablePreviousPrompt(session.thread);
|
|
124
|
+
const edited = await session.editorReader(editTarget.prompt);
|
|
125
|
+
if (edited) {
|
|
126
|
+
session.thread.messages = session.thread.messages.slice(0, editTarget.index);
|
|
127
|
+
if (session.thread.messages.length === 0) {
|
|
128
|
+
session.thread.title = edited.split(/\r?\n/).find(Boolean)?.slice(0, 120) || '(empty prompt)';
|
|
129
|
+
}
|
|
130
|
+
await submitPromptAndQueue(session, edited);
|
|
131
|
+
}
|
|
132
|
+
return { kind: 'command', lines: [] };
|
|
133
|
+
} catch (error) {
|
|
134
|
+
return { kind: 'error', lines: [`${CLI_NAME}: ${error?.message ?? error}`] };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (cmd === 'thread:' && rest.join(' ') === 'archive and quit') {
|
|
139
|
+
try {
|
|
140
|
+
if (!session.thread) throw new Error('No current thread to archive');
|
|
141
|
+
await session.commandRunner('threads', ['archive', session.thread.id], session.parsed, '');
|
|
142
|
+
return { kind: 'exit', lines: [] };
|
|
143
|
+
} catch (error) {
|
|
144
|
+
return { kind: 'error', lines: [`${CLI_NAME}: ${error?.message ?? error}`] };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (cmd === 'thread:' && rest[0] === 'set' && rest[1] === 'visibility') {
|
|
149
|
+
try {
|
|
150
|
+
if (!session.thread) throw new Error('No current thread to update');
|
|
151
|
+
await session.commandRunner('threads', ['visibility', session.thread.id, ...rest.slice(2)], session.parsed, '');
|
|
152
|
+
return { kind: 'command', lines: [] };
|
|
153
|
+
} catch (error) {
|
|
154
|
+
return { kind: 'error', lines: [`${CLI_NAME}: ${error?.message ?? error}`] };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (cmd === 'feedback:' && rest.join(' ') === 'send report with diagnostics') {
|
|
159
|
+
try {
|
|
160
|
+
if (!session.thread) throw new Error('No current thread to report');
|
|
161
|
+
await session.commandRunner('threads', ['report', session.thread.id], session.parsed, '');
|
|
162
|
+
return { kind: 'command', lines: [] };
|
|
163
|
+
} catch (error) {
|
|
164
|
+
return { kind: 'error', lines: [`${CLI_NAME}: ${error?.message ?? error}`] };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
if (cmd === 'skill:') await session.commandRunner('skill', rest, session.parsed, '');
|
|
170
|
+
else if (cmd === 'plugins:') await session.commandRunner('plugins', rest, session.parsed, '');
|
|
171
|
+
else await session.commandRunner(cmd, rest, session.parsed, '');
|
|
172
|
+
return { kind: 'command', lines: [] };
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (String(error?.message ?? '').startsWith('Unknown command:')) {
|
|
175
|
+
try {
|
|
176
|
+
await session.commandRunner('plugins', ['run', cmd], session.parsed, '');
|
|
177
|
+
return { kind: 'command', lines: [] };
|
|
178
|
+
} catch (pluginError) {
|
|
179
|
+
if (!String(pluginError?.message ?? '').startsWith('Unknown plugin command:')) throw pluginError;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return { kind: 'error', lines: [`${CLI_NAME}: ${error?.message ?? error}`] };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function submitPromptAndQueue(session, text) {
|
|
187
|
+
session.thread = await runInteractiveTurn(session.parsed, text, session.thread, session.executeRunner);
|
|
188
|
+
while (session.queuedMessages.length > 0) {
|
|
189
|
+
session.thread = await runInteractiveTurn(session.parsed, session.queuedMessages.shift(), session.thread, session.executeRunner);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function slashHelpLines() {
|
|
194
|
+
return formatSlashHelpLines();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function printSlashHelp() {
|
|
198
|
+
for (const line of slashHelpLines()) console.log(line);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function readEditorPrompt(initialText = '') {
|
|
202
|
+
const editor = process.env.EDITOR || process.env.VISUAL;
|
|
203
|
+
if (!editor) {
|
|
204
|
+
console.error(`${CLI_NAME}: /editor requires $EDITOR or $VISUAL`);
|
|
205
|
+
return '';
|
|
206
|
+
}
|
|
207
|
+
const file = path.join(tmpdir(), `${CONFIG_SUBDIR}-prompt-${process.pid}-${Date.now()}.md`);
|
|
208
|
+
try {
|
|
209
|
+
await writeFile(file, initialText);
|
|
210
|
+
const result = spawnSync(`${editor} ${shellQuote(file)}`, {
|
|
211
|
+
stdio: 'inherit',
|
|
212
|
+
shell: true,
|
|
213
|
+
});
|
|
214
|
+
if (result.error) {
|
|
215
|
+
console.error(`${CLI_NAME}: Unable to run editor: ${result.error.message}`);
|
|
216
|
+
return '';
|
|
217
|
+
}
|
|
218
|
+
if ((result.status ?? 0) !== 0) {
|
|
219
|
+
console.error(`${CLI_NAME}: Editor exited with status ${result.status}`);
|
|
220
|
+
return '';
|
|
221
|
+
}
|
|
222
|
+
return (await readFile(file, 'utf8')).trim();
|
|
223
|
+
} finally {
|
|
224
|
+
await unlink(file).catch(() => {});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function editablePreviousPrompt(thread) {
|
|
229
|
+
const index = (thread.messages ?? []).findLastIndex((message) => (
|
|
230
|
+
message.role === 'user' && typeof message.content === 'string'
|
|
231
|
+
));
|
|
232
|
+
if (index === -1) throw new Error('No previous user prompt to edit');
|
|
233
|
+
return { index, prompt: thread.messages[index].content };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function interactiveContinuationThread(threadId) {
|
|
237
|
+
if (threadId) return requireThread(threadId);
|
|
238
|
+
const thread = latestActiveThread();
|
|
239
|
+
if (!thread) throw new Error('No active thread to continue');
|
|
240
|
+
return thread;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export async function runInteractiveTurn(parsed, text, thread, executeRunner = runExecute) {
|
|
244
|
+
try {
|
|
245
|
+
return await executeRunner(
|
|
246
|
+
{ ...parsed, execute: true, prompt: text, streamJson: false, streamJsonThinking: false, streamJsonInput: false },
|
|
247
|
+
'',
|
|
248
|
+
{ thread },
|
|
249
|
+
);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
console.error(`${CLI_NAME}: ${error?.message ?? error}`);
|
|
252
|
+
return thread;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function replHistoryFile() {
|
|
257
|
+
return process.env.COVEN_CODE_REPL_HISTORY_FILE
|
|
258
|
+
|| path.join(configDir(), CONFIG_SUBDIR, 'repl_history');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function loadReplHistory() {
|
|
262
|
+
if (process.env.COVEN_CODE_REPL_HISTORY === '0') return [];
|
|
263
|
+
try {
|
|
264
|
+
return readFileSync(replHistoryFile(), 'utf8')
|
|
265
|
+
.split(/\r?\n/)
|
|
266
|
+
.filter(Boolean)
|
|
267
|
+
.slice(-REPL_HISTORY_LIMIT)
|
|
268
|
+
.reverse();
|
|
269
|
+
} catch {
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export async function appendReplHistory(line) {
|
|
275
|
+
if (process.env.COVEN_CODE_REPL_HISTORY === '0') return;
|
|
276
|
+
try {
|
|
277
|
+
const file = replHistoryFile();
|
|
278
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
279
|
+
await appendFile(file, `${line}\n`);
|
|
280
|
+
} catch {
|
|
281
|
+
// history must never break the REPL
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function safeSlashCommandCatalog(session) {
|
|
286
|
+
try {
|
|
287
|
+
return await buildSlashCommandCatalog({
|
|
288
|
+
parsed: session.parsed,
|
|
289
|
+
cwd: session.cwd ?? process.cwd(),
|
|
290
|
+
});
|
|
291
|
+
} catch {
|
|
292
|
+
return [];
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function sessionSlashHelpLines(session) {
|
|
297
|
+
return formatCatalogHelpLines(await safeSlashCommandCatalog(session));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function formatCatalogHelpLines(catalog) {
|
|
301
|
+
return catalog.length > 0 ? formatSlashHelpLines(catalog) : slashHelpLines();
|
|
302
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { readEffectiveSettings } from '../settings/load.mjs';
|
|
2
|
+
|
|
3
|
+
export function notifyAgentComplete(parsed = {}) {
|
|
4
|
+
if (readEffectiveSettings(parsed)['covenCode.notifications.enabled'] === false) return;
|
|
5
|
+
if (!shouldUseTerminalBell()) return;
|
|
6
|
+
process.stderr.write('\x07');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function shouldUseTerminalBell() {
|
|
10
|
+
return process.env.COVEN_CODE_FORCE_BEL === '1'
|
|
11
|
+
|| process.env.COVEN_CODE_FORCE_BEL === 'true'
|
|
12
|
+
|| Boolean(process.env.SSH_TTY || process.env.SSH_CONNECTION);
|
|
13
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export class UsageError extends Error {}
|
|
2
|
+
|
|
3
|
+
export function parseGlobalArgs(args) {
|
|
4
|
+
const parsed = {
|
|
5
|
+
execute: false,
|
|
6
|
+
prompt: '',
|
|
7
|
+
streamJson: false,
|
|
8
|
+
streamJsonThinking: false,
|
|
9
|
+
streamJsonInput: false,
|
|
10
|
+
dangerouslyAllowAll: false,
|
|
11
|
+
help: false,
|
|
12
|
+
version: false,
|
|
13
|
+
mode: 'smart',
|
|
14
|
+
reasoningEffort: undefined,
|
|
15
|
+
mcpConfig: undefined,
|
|
16
|
+
settingsFile: undefined,
|
|
17
|
+
labels: [],
|
|
18
|
+
visibility: undefined,
|
|
19
|
+
archive: false,
|
|
20
|
+
continueThread: false,
|
|
21
|
+
toolbox: undefined,
|
|
22
|
+
skills: undefined,
|
|
23
|
+
ide: undefined,
|
|
24
|
+
positionals: [],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
28
|
+
const arg = args[index];
|
|
29
|
+
if (arg === '--help' || arg === '-h') parsed.help = true;
|
|
30
|
+
else if (arg === '--version' || arg === '-v') parsed.version = true;
|
|
31
|
+
else if (arg === '--execute' || arg === '-x') {
|
|
32
|
+
parsed.execute = true;
|
|
33
|
+
const next = args[index + 1];
|
|
34
|
+
if (next && !next.startsWith('-')) {
|
|
35
|
+
parsed.prompt = next;
|
|
36
|
+
index += 1;
|
|
37
|
+
}
|
|
38
|
+
} else if (arg === '--stream-json') parsed.streamJson = true;
|
|
39
|
+
else if (arg === '--stream-json-thinking') {
|
|
40
|
+
parsed.streamJson = true;
|
|
41
|
+
parsed.streamJsonThinking = true;
|
|
42
|
+
} else if (arg === '--stream-json-input') parsed.streamJsonInput = true;
|
|
43
|
+
else if (arg === '--dangerously-allow-all') parsed.dangerouslyAllowAll = true;
|
|
44
|
+
else if (arg === '--mcp-config') {
|
|
45
|
+
parsed.mcpConfig = args[index + 1] ?? '';
|
|
46
|
+
index += 1;
|
|
47
|
+
} else if (arg === '--settings-file') {
|
|
48
|
+
parsed.settingsFile = args[index + 1] ?? '';
|
|
49
|
+
index += 1;
|
|
50
|
+
} else if (arg === '--label') {
|
|
51
|
+
parsed.labels.push(args[index + 1] ?? '');
|
|
52
|
+
index += 1;
|
|
53
|
+
} else if (arg === '--visibility') {
|
|
54
|
+
parsed.visibility = args[index + 1] ?? '';
|
|
55
|
+
index += 1;
|
|
56
|
+
} else if (arg === '--archive') {
|
|
57
|
+
parsed.archive = true;
|
|
58
|
+
} else if (arg === '--continue') {
|
|
59
|
+
const next = args[index + 1];
|
|
60
|
+
if (next && /^T-[A-Za-z0-9-]+$/.test(next)) {
|
|
61
|
+
parsed.continueThread = next;
|
|
62
|
+
index += 1;
|
|
63
|
+
} else {
|
|
64
|
+
parsed.continueThread = true;
|
|
65
|
+
}
|
|
66
|
+
} else if (arg === '--toolbox') {
|
|
67
|
+
parsed.toolbox = args[index + 1] ?? '';
|
|
68
|
+
index += 1;
|
|
69
|
+
} else if (arg === '--skills') {
|
|
70
|
+
parsed.skills = args[index + 1] ?? '';
|
|
71
|
+
index += 1;
|
|
72
|
+
} else if (arg === '--mode') {
|
|
73
|
+
parsed.mode = args[index + 1] ?? parsed.mode;
|
|
74
|
+
index += 1;
|
|
75
|
+
} else if (arg === '--reasoning-effort') {
|
|
76
|
+
parsed.reasoningEffort = args[index + 1] ?? '';
|
|
77
|
+
index += 1;
|
|
78
|
+
} else if (arg === '--jetbrains') parsed.ide = 'jetbrains';
|
|
79
|
+
else parsed.positionals.push(arg);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return parsed;
|
|
83
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { UsageError } from './parse.mjs';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_REASONING_EFFORT_BY_MODE = {
|
|
4
|
+
smart: 'high',
|
|
5
|
+
deep: 'high',
|
|
6
|
+
rush: 'minimal',
|
|
7
|
+
large: 'high',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const REASONING_EFFORTS_BY_MODE = {
|
|
11
|
+
smart: ['high', 'xhigh', 'max'],
|
|
12
|
+
deep: ['high', 'low', 'medium', 'xhigh'],
|
|
13
|
+
rush: ['minimal'],
|
|
14
|
+
large: ['high', 'xhigh', 'max'],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function reasoningEffortForMode(mode, requested) {
|
|
18
|
+
const efforts = reasoningEffortsForMode(mode);
|
|
19
|
+
if (requested === undefined || requested === null || requested === '') return defaultReasoningEffortForMode(mode);
|
|
20
|
+
if (efforts.includes(requested)) return requested;
|
|
21
|
+
throw new UsageError(`reasoning effort for ${mode} must be one of: ${efforts.join(', ')}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function coerceReasoningEffortForMode(mode, requested) {
|
|
25
|
+
try {
|
|
26
|
+
return reasoningEffortForMode(mode, requested);
|
|
27
|
+
} catch {
|
|
28
|
+
return defaultReasoningEffortForMode(mode);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function nextReasoningEffortForMode(mode, requested) {
|
|
33
|
+
const efforts = reasoningEffortsForMode(mode);
|
|
34
|
+
const current = coerceReasoningEffortForMode(mode, requested);
|
|
35
|
+
const index = efforts.indexOf(current);
|
|
36
|
+
return efforts[(index + 1) % efforts.length];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function defaultReasoningEffortForMode(mode) {
|
|
40
|
+
return DEFAULT_REASONING_EFFORT_BY_MODE[mode] ?? DEFAULT_REASONING_EFFORT_BY_MODE.smart;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function reasoningEffortsForMode(mode) {
|
|
44
|
+
return REASONING_EFFORTS_BY_MODE[mode] ?? REASONING_EFFORTS_BY_MODE.smart;
|
|
45
|
+
}
|
package/src/cli/refs.mjs
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { FILE_MENTION_MAX_LINES, FILE_MENTION_MAX_LINE_LENGTH, THREAD_URL_BASE } from '../constants.mjs';
|
|
5
|
+
import { globToRegex, hasGlob, walkFiles } from '../util/glob.mjs';
|
|
6
|
+
import { listThreads, readThread, threadSearchText } from '../threads/store.mjs';
|
|
7
|
+
import { readEffectiveSettings } from '../settings/load.mjs';
|
|
8
|
+
|
|
9
|
+
export function expandFileReferences(prompt, options = {}) {
|
|
10
|
+
return prompt.replace(/@(?!(?:T-|@))((?:~|\/|\.{1,2}\/)?[A-Za-z0-9_./*?-]*[A-Za-z0-9_*-])/g, (match, rawPath) => {
|
|
11
|
+
const blocks = mentionedFiles(rawPath, options)
|
|
12
|
+
.map((filePath) => fileMentionBlock(filePath, options))
|
|
13
|
+
.filter(Boolean);
|
|
14
|
+
if (blocks.length === 0) return match;
|
|
15
|
+
return blocks.join('\n');
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function fileMentionBlock(filePath, options = {}) {
|
|
20
|
+
const image = imageMentionBlock(filePath);
|
|
21
|
+
if (image) return image;
|
|
22
|
+
const content = readMentionedTextFile(filePath);
|
|
23
|
+
if (content === undefined) return undefined;
|
|
24
|
+
if (options.includeFile && !options.includeFile(filePath, content)) return undefined;
|
|
25
|
+
return `[file:${filePath}]\n${content}\n[/file]`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function imageMentionBlock(filePath) {
|
|
29
|
+
const mediaType = imageMediaType(filePath);
|
|
30
|
+
if (!mediaType) return undefined;
|
|
31
|
+
const bytes = readFileSync(filePath).byteLength;
|
|
32
|
+
return `[image:${filePath}]\nmedia_type: ${mediaType}\nbytes: ${bytes}\n[/image]`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function imageMediaType(filePath) {
|
|
36
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
37
|
+
if (ext === '.png') return 'image/png';
|
|
38
|
+
if (ext === '.jpg' || ext === '.jpeg') return 'image/jpeg';
|
|
39
|
+
if (ext === '.gif') return 'image/gif';
|
|
40
|
+
if (ext === '.webp') return 'image/webp';
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readMentionedTextFile(filePath) {
|
|
45
|
+
const raw = readFileSync(filePath);
|
|
46
|
+
if (raw.includes(0)) return undefined;
|
|
47
|
+
const text = raw.toString('utf8');
|
|
48
|
+
return text
|
|
49
|
+
.split(/\r?\n/)
|
|
50
|
+
.slice(0, FILE_MENTION_MAX_LINES)
|
|
51
|
+
.map((line) => line.slice(0, FILE_MENTION_MAX_LINE_LENGTH))
|
|
52
|
+
.join('\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function mentionedFiles(rawPath, options = {}) {
|
|
56
|
+
if (!hasGlob(rawPath)) {
|
|
57
|
+
const filePath = resolveMentionedPath(rawPath, options);
|
|
58
|
+
return existsSync(filePath) && statSync(filePath).isFile() ? [filePath] : [];
|
|
59
|
+
}
|
|
60
|
+
const root = globSearchRoot(rawPath, options);
|
|
61
|
+
const pattern = rawPath.startsWith('~/')
|
|
62
|
+
? path.join(os.homedir(), rawPath.slice(2))
|
|
63
|
+
: path.resolve(options.baseDir ?? process.cwd(), rawPath);
|
|
64
|
+
const re = globToRegex(path.normalize(pattern));
|
|
65
|
+
const ignored = gitignoredFileMatcher(options.baseDir ?? process.cwd());
|
|
66
|
+
const alwaysIncluded = alwaysIncludedFileMatcher(options.baseDir ?? process.cwd(), options.parsed);
|
|
67
|
+
return walkFiles(root)
|
|
68
|
+
.filter((filePath) => re.test(path.normalize(filePath)))
|
|
69
|
+
.filter((filePath) => !ignored(filePath) || alwaysIncluded(filePath))
|
|
70
|
+
.sort((a, b) => a.localeCompare(b));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function resolveMentionedPath(rawPath, options = {}) {
|
|
74
|
+
if (rawPath.startsWith('~/')) return path.join(os.homedir(), rawPath.slice(2));
|
|
75
|
+
if (path.isAbsolute(rawPath)) return rawPath;
|
|
76
|
+
return path.resolve(options.baseDir ?? process.cwd(), rawPath);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function globSearchRoot(rawPath, options = {}) {
|
|
80
|
+
const pattern = resolveMentionedPath(rawPath, options);
|
|
81
|
+
const parts = pattern.split(path.sep);
|
|
82
|
+
const stableParts = [];
|
|
83
|
+
for (const part of parts) {
|
|
84
|
+
if (hasGlob(part)) break;
|
|
85
|
+
stableParts.push(part);
|
|
86
|
+
}
|
|
87
|
+
const root = stableParts.join(path.sep) || path.sep;
|
|
88
|
+
return existsSync(root) && statSync(root).isDirectory() ? root : path.dirname(root);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function gitignoredFileMatcher(baseDir) {
|
|
92
|
+
const gitignorePath = findUp('.gitignore', baseDir);
|
|
93
|
+
if (!gitignorePath) return () => false;
|
|
94
|
+
const root = path.dirname(gitignorePath);
|
|
95
|
+
const patterns = readFileSync(gitignorePath, 'utf8')
|
|
96
|
+
.split(/\r?\n/)
|
|
97
|
+
.map((line) => line.trim())
|
|
98
|
+
.filter((line) => line && !line.startsWith('#') && !line.startsWith('!'))
|
|
99
|
+
.map((line) => gitignorePatternRegex(root, line));
|
|
100
|
+
return (filePath) => patterns.some((pattern) => pattern.test(path.normalize(filePath)));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function alwaysIncludedFileMatcher(baseDir, parsed = {}) {
|
|
104
|
+
const settings = readEffectiveSettings(parsed);
|
|
105
|
+
const patterns = Array.isArray(settings['covenCode.fuzzy.alwaysIncludePaths'])
|
|
106
|
+
? settings['covenCode.fuzzy.alwaysIncludePaths']
|
|
107
|
+
: [];
|
|
108
|
+
const root = path.dirname(findUp('.gitignore', baseDir) ?? path.join(path.resolve(baseDir), '.gitignore'));
|
|
109
|
+
const regexes = patterns
|
|
110
|
+
.filter((pattern) => typeof pattern === 'string' && pattern.trim())
|
|
111
|
+
.map((pattern) => globToRegex(path.normalize(path.resolve(root, pattern))));
|
|
112
|
+
return (filePath) => regexes.some((pattern) => pattern.test(path.normalize(filePath)));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function gitignorePatternRegex(root, pattern) {
|
|
116
|
+
const directoryOnly = pattern.endsWith('/');
|
|
117
|
+
const cleanPattern = pattern.replace(/\/+$/, '');
|
|
118
|
+
const absolutePattern = path.resolve(root, cleanPattern);
|
|
119
|
+
const source = directoryOnly
|
|
120
|
+
? `${globToRegex(path.normalize(absolutePattern)).source.slice(1, -1)}(?:${escapePathSep()}.*)?`
|
|
121
|
+
: globToRegex(path.normalize(absolutePattern)).source.slice(1, -1);
|
|
122
|
+
return new RegExp(`^${source}$`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function escapePathSep() {
|
|
126
|
+
return path.sep === '\\' ? '\\\\' : '/';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function findUp(name, cwd) {
|
|
130
|
+
let current = path.resolve(cwd);
|
|
131
|
+
while (true) {
|
|
132
|
+
const candidate = path.join(current, name);
|
|
133
|
+
if (existsSync(candidate)) return candidate;
|
|
134
|
+
const parent = path.dirname(current);
|
|
135
|
+
if (parent === current || current === os.homedir()) return undefined;
|
|
136
|
+
current = parent;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function expandThreadReferences(prompt) {
|
|
141
|
+
const threadUrlPattern = THREAD_URL_BASE.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
142
|
+
const explicitReferences = prompt.replace(new RegExp(`(?:@|${threadUrlPattern}/)(T-[A-Za-z0-9-]+)`, 'g'), (match, threadId) => {
|
|
143
|
+
const thread = readThread(threadId);
|
|
144
|
+
if (!thread) return match;
|
|
145
|
+
return threadMentionBlock(thread);
|
|
146
|
+
});
|
|
147
|
+
return explicitReferences.replace(/@@([A-Za-z0-9_.:/-]+)/g, (match, rawQuery) => {
|
|
148
|
+
const thread = findMentionedThread(rawQuery);
|
|
149
|
+
return thread ? threadMentionBlock(thread) : match;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function findMentionedThread(rawQuery) {
|
|
154
|
+
const query = String(rawQuery ?? '').trim().toLowerCase();
|
|
155
|
+
if (!query) return undefined;
|
|
156
|
+
return listThreads()
|
|
157
|
+
.filter((thread) => threadSearchText(thread).toLowerCase().includes(query))[0];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function threadMentionBlock(thread) {
|
|
161
|
+
return `[thread:${thread.id}]\n${thread.messages.map((message) => `${message.role}: ${message.content}`).join('\n')}\n[/thread]`;
|
|
162
|
+
}
|
package/src/cli/repl.mjs
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as readline from 'node:readline/promises';
|
|
2
|
+
import { CLI_NAME, REPL_HISTORY_LIMIT, VERSION } from '../constants.mjs';
|
|
3
|
+
import {
|
|
4
|
+
appendReplHistory,
|
|
5
|
+
createInteractiveSession,
|
|
6
|
+
handleInteractiveInput,
|
|
7
|
+
loadReplHistory,
|
|
8
|
+
} from './interactive-core.mjs';
|
|
9
|
+
|
|
10
|
+
export async function runInteractive(parsed, initialInput = '') {
|
|
11
|
+
const rl = readline.createInterface({
|
|
12
|
+
input: process.stdin,
|
|
13
|
+
output: process.stdout,
|
|
14
|
+
terminal: true,
|
|
15
|
+
historySize: REPL_HISTORY_LIMIT,
|
|
16
|
+
removeHistoryDuplicates: true,
|
|
17
|
+
history: loadReplHistory(),
|
|
18
|
+
});
|
|
19
|
+
const session = createInteractiveSession(parsed);
|
|
20
|
+
let buffer = [];
|
|
21
|
+
console.log(`${CLI_NAME} ${VERSION} — interactive mode. Type /exit or press Ctrl-D to quit, /help for slash commands.`);
|
|
22
|
+
if (initialInput.trim()) {
|
|
23
|
+
await handleInteractiveInput(session, initialInput.trim());
|
|
24
|
+
if (!process.stdin.isTTY) {
|
|
25
|
+
rl.close();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
rl.setPrompt('> ');
|
|
30
|
+
rl.prompt();
|
|
31
|
+
try {
|
|
32
|
+
for await (const line of rl) {
|
|
33
|
+
if (line.trim()) await appendReplHistory(line);
|
|
34
|
+
if (line.endsWith('\\')) {
|
|
35
|
+
buffer.push(line.slice(0, -1));
|
|
36
|
+
rl.setPrompt('… ');
|
|
37
|
+
rl.prompt();
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
buffer.push(line);
|
|
41
|
+
const text = buffer.join('\n').trim();
|
|
42
|
+
buffer = [];
|
|
43
|
+
rl.setPrompt('> ');
|
|
44
|
+
if (!text) {
|
|
45
|
+
rl.prompt();
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const result = await handleInteractiveInput(session, text);
|
|
49
|
+
if (result.lines.length > 0) {
|
|
50
|
+
for (const outputLine of result.lines) {
|
|
51
|
+
if (result.kind === 'error') console.error(outputLine);
|
|
52
|
+
else console.log(outputLine);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (result.kind === 'exit') break;
|
|
56
|
+
rl.prompt();
|
|
57
|
+
}
|
|
58
|
+
} finally {
|
|
59
|
+
rl.close();
|
|
60
|
+
}
|
|
61
|
+
}
|