@opencoven/coven-code 0.0.1 → 0.0.2
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 +2 -1
- package/docs/CLI.md +65 -1
- package/docs/DEMO.md +450 -0
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/README.md +1 -0
- package/package.json +7 -6
- package/src/agent/{local.mjs → fixture.mjs} +1 -1
- package/src/cli/execute.mjs +6 -4
- package/src/cli/interactive-core.mjs +5 -279
- package/src/cli/interactive-io.mjs +101 -0
- package/src/cli/interactive-slash.mjs +184 -0
- package/src/cli/repl.mjs +1 -2
- package/src/cli/tui-actions.mjs +72 -0
- package/src/cli/tui-blessed.mjs +198 -0
- package/src/cli/tui-keys.mjs +80 -0
- package/src/cli/tui-lane.mjs +73 -0
- package/src/cli/tui-render.mjs +169 -0
- package/src/cli/tui-submit.mjs +82 -0
- package/src/cli/tui.mjs +30 -613
- package/src/commands/permissions-eval.mjs +122 -0
- package/src/commands/permissions-rules.mjs +53 -0
- package/src/commands/permissions-text.mjs +112 -0
- package/src/commands/permissions.mjs +15 -281
- package/src/commands/usage.mjs +1 -1
- package/src/constants.mjs +7 -1
- package/src/mcp/local.mjs +55 -0
- package/src/mcp/parsers.mjs +46 -0
- package/src/mcp/probe.mjs +12 -351
- package/src/mcp/remote-oauth.mjs +55 -0
- package/src/mcp/remote-session.mjs +54 -0
- package/src/mcp/remote-sse.mjs +82 -0
- package/src/mcp/remote.mjs +74 -0
- package/src/plugins/api.mjs +187 -0
- package/src/plugins/configuration.mjs +124 -0
- package/src/plugins/discover.mjs +8 -804
- package/src/plugins/helpers.mjs +187 -0
- package/src/plugins/subsystems.mjs +198 -0
- package/src/plugins/validators.mjs +142 -0
- package/src/sdk-execute.mjs +82 -0
- package/src/sdk-settings.mjs +88 -0
- package/src/sdk.mjs +13 -164
- package/src/tools/builtin/oracle.mjs +2 -2
- package/src/tools/builtin/runtime-content.mjs +31 -0
- package/src/tools/builtin/runtime-decisions.mjs +115 -0
- package/src/tools/builtin/runtime.mjs +18 -148
- package/src/tools/builtin/task.mjs +2 -2
|
@@ -1,26 +1,7 @@
|
|
|
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
1
|
import { runCommand } from './dispatch.mjs';
|
|
11
2
|
import { runExecute } from './execute.mjs';
|
|
12
|
-
import {
|
|
13
|
-
|
|
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';
|
|
3
|
+
import { readEditorPrompt, submitPromptAndQueue } from './interactive-io.mjs';
|
|
4
|
+
import { handleSlashCommand, sessionSlashHelpLines } from './interactive-slash.mjs';
|
|
24
5
|
|
|
25
6
|
export function createInteractiveSession(parsed, options = {}) {
|
|
26
7
|
return {
|
|
@@ -28,6 +9,7 @@ export function createInteractiveSession(parsed, options = {}) {
|
|
|
28
9
|
thread: options.thread,
|
|
29
10
|
cwd: options.cwd ?? process.cwd(),
|
|
30
11
|
queuedMessages: [],
|
|
12
|
+
silent: options.silent ?? false,
|
|
31
13
|
commandRunner: options.commandRunner ?? runCommand,
|
|
32
14
|
executeRunner: options.executeRunner ?? runExecute,
|
|
33
15
|
editorReader: options.editorReader ?? readEditorPrompt,
|
|
@@ -39,264 +21,8 @@ export async function handleInteractiveInput(session, text) {
|
|
|
39
21
|
if (text === '/exit' || text === '/quit') return { kind: 'exit', lines: [] };
|
|
40
22
|
if (text === '/help') return { kind: 'help', lines: await sessionSlashHelpLines(session) };
|
|
41
23
|
if (!text.startsWith('/')) {
|
|
42
|
-
|
|
43
|
-
while (session.queuedMessages.length > 0) {
|
|
44
|
-
session.thread = await runInteractiveTurn(session.parsed, session.queuedMessages.shift(), session.thread, session.executeRunner);
|
|
45
|
-
}
|
|
24
|
+
await submitPromptAndQueue(session, text);
|
|
46
25
|
return { kind: 'turn', lines: [] };
|
|
47
26
|
}
|
|
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();
|
|
27
|
+
return handleSlashCommand(session, text);
|
|
302
28
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
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 { CLI_NAME, CONFIG_SUBDIR, REPL_HISTORY_LIMIT } from '../constants.mjs';
|
|
7
|
+
import { configDir } from '../settings/paths.mjs';
|
|
8
|
+
import { shellQuote } from '../util/shell.mjs';
|
|
9
|
+
import { latestActiveThread, requireThread } from '../threads/store.mjs';
|
|
10
|
+
import { runExecute } from './execute.mjs';
|
|
11
|
+
|
|
12
|
+
export async function runInteractiveTurn(parsed, text, thread, executeRunner = runExecute, options = {}) {
|
|
13
|
+
try {
|
|
14
|
+
return await executeRunner(
|
|
15
|
+
{ ...parsed, execute: true, prompt: text, streamJson: false, streamJsonThinking: false, streamJsonInput: false },
|
|
16
|
+
'',
|
|
17
|
+
{ thread, ...options },
|
|
18
|
+
);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error(`${CLI_NAME}: ${error?.message ?? error}`);
|
|
21
|
+
return thread;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function submitPromptAndQueue(session, text) {
|
|
26
|
+
session.thread = await runInteractiveTurn(session.parsed, text, session.thread, session.executeRunner, { silent: session.silent });
|
|
27
|
+
while (session.queuedMessages.length > 0) {
|
|
28
|
+
session.thread = await runInteractiveTurn(session.parsed, session.queuedMessages.shift(), session.thread, session.executeRunner, { silent: session.silent });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function readEditorPrompt(initialText = '') {
|
|
33
|
+
const editor = process.env.EDITOR || process.env.VISUAL;
|
|
34
|
+
if (!editor) {
|
|
35
|
+
console.error(`${CLI_NAME}: /editor requires $EDITOR or $VISUAL`);
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
const file = path.join(tmpdir(), `${CONFIG_SUBDIR}-prompt-${process.pid}-${Date.now()}.md`);
|
|
39
|
+
try {
|
|
40
|
+
await writeFile(file, initialText);
|
|
41
|
+
const result = spawnSync(`${editor} ${shellQuote(file)}`, {
|
|
42
|
+
stdio: 'inherit',
|
|
43
|
+
shell: true,
|
|
44
|
+
});
|
|
45
|
+
if (result.error) {
|
|
46
|
+
console.error(`${CLI_NAME}: Unable to run editor: ${result.error.message}`);
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
49
|
+
if ((result.status ?? 0) !== 0) {
|
|
50
|
+
console.error(`${CLI_NAME}: Editor exited with status ${result.status}`);
|
|
51
|
+
return '';
|
|
52
|
+
}
|
|
53
|
+
return (await readFile(file, 'utf8')).trim();
|
|
54
|
+
} finally {
|
|
55
|
+
await unlink(file).catch(() => {});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function editablePreviousPrompt(thread) {
|
|
60
|
+
const index = (thread.messages ?? []).findLastIndex((message) => (
|
|
61
|
+
message.role === 'user' && typeof message.content === 'string'
|
|
62
|
+
));
|
|
63
|
+
if (index === -1) throw new Error('No previous user prompt to edit');
|
|
64
|
+
return { index, prompt: thread.messages[index].content };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function interactiveContinuationThread(threadId) {
|
|
68
|
+
if (threadId) return requireThread(threadId);
|
|
69
|
+
const thread = latestActiveThread();
|
|
70
|
+
if (!thread) throw new Error('No active thread to continue');
|
|
71
|
+
return thread;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function replHistoryFile() {
|
|
75
|
+
return process.env.COVEN_CODE_REPL_HISTORY_FILE
|
|
76
|
+
|| path.join(configDir(), CONFIG_SUBDIR, 'repl_history');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function loadReplHistory() {
|
|
80
|
+
if (process.env.COVEN_CODE_REPL_HISTORY === '0') return [];
|
|
81
|
+
try {
|
|
82
|
+
return readFileSync(replHistoryFile(), 'utf8')
|
|
83
|
+
.split(/\r?\n/)
|
|
84
|
+
.filter(Boolean)
|
|
85
|
+
.slice(-REPL_HISTORY_LIMIT)
|
|
86
|
+
.reverse();
|
|
87
|
+
} catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function appendReplHistory(line) {
|
|
93
|
+
if (process.env.COVEN_CODE_REPL_HISTORY === '0') return;
|
|
94
|
+
try {
|
|
95
|
+
const file = replHistoryFile();
|
|
96
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
97
|
+
await appendFile(file, `${line}\n`);
|
|
98
|
+
} catch {
|
|
99
|
+
// history must never break the REPL
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { AGENT_MODES, CLI_NAME } from '../constants.mjs';
|
|
2
|
+
import { splitShellWords } from '../util/shell.mjs';
|
|
3
|
+
import {
|
|
4
|
+
coerceReasoningEffortForMode,
|
|
5
|
+
nextReasoningEffortForMode,
|
|
6
|
+
reasoningEffortForMode,
|
|
7
|
+
} from './reasoning.mjs';
|
|
8
|
+
import {
|
|
9
|
+
buildSlashCommandCatalog,
|
|
10
|
+
findSlashCommand,
|
|
11
|
+
formatSlashCommandDetails,
|
|
12
|
+
formatSlashHelpLines,
|
|
13
|
+
skillPromptFromSlashCommand,
|
|
14
|
+
} from './slash-commands.mjs';
|
|
15
|
+
import {
|
|
16
|
+
editablePreviousPrompt,
|
|
17
|
+
interactiveContinuationThread,
|
|
18
|
+
submitPromptAndQueue,
|
|
19
|
+
} from './interactive-io.mjs';
|
|
20
|
+
|
|
21
|
+
export async function handleSlashCommand(session, text) {
|
|
22
|
+
const tokens = splitShellWords(text.slice(1));
|
|
23
|
+
const [cmd, ...rest] = tokens;
|
|
24
|
+
const catalog = await safeSlashCommandCatalog(session);
|
|
25
|
+
if (!cmd) return { kind: 'help', lines: formatCatalogHelpLines(catalog) };
|
|
26
|
+
const catalogEntry = findSlashCommand(catalog, cmd);
|
|
27
|
+
|
|
28
|
+
if (catalogEntry?.source === 'skill') {
|
|
29
|
+
if (rest.length === 0) {
|
|
30
|
+
return { kind: 'command', lines: formatSlashCommandDetails(catalogEntry) };
|
|
31
|
+
}
|
|
32
|
+
await submitPromptAndQueue(session, skillPromptFromSlashCommand(catalogEntry, rest.join(' ')));
|
|
33
|
+
return { kind: 'turn', lines: [] };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (catalogEntry?.source === 'plugin' && catalogEntry.availability?.type === 'disabled') {
|
|
37
|
+
return {
|
|
38
|
+
kind: 'error',
|
|
39
|
+
lines: [`${CLI_NAME}: ${catalogEntry.availability.reason ?? `Plugin command disabled: ${catalogEntry.name}`}`],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (cmd === 'mode') {
|
|
44
|
+
const nextMode = rest[0];
|
|
45
|
+
if (!nextMode) return { kind: 'command', lines: [`mode: ${session.parsed.mode}`] };
|
|
46
|
+
if (!AGENT_MODES.includes(nextMode)) return { kind: 'error', lines: [`${CLI_NAME}: Unknown mode: ${nextMode}`] };
|
|
47
|
+
session.parsed.mode = nextMode;
|
|
48
|
+
session.parsed.reasoningEffort = coerceReasoningEffortForMode(session.parsed.mode, session.parsed.reasoningEffort);
|
|
49
|
+
return { kind: 'command', lines: [`mode: ${session.parsed.mode}`, `reasoning effort: ${session.parsed.reasoningEffort}`] };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (cmd === 'reasoning') {
|
|
53
|
+
try {
|
|
54
|
+
const nextEffort = rest[0] === 'next'
|
|
55
|
+
? nextReasoningEffortForMode(session.parsed.mode, session.parsed.reasoningEffort)
|
|
56
|
+
: rest[0];
|
|
57
|
+
session.parsed.reasoningEffort = reasoningEffortForMode(session.parsed.mode, nextEffort ?? session.parsed.reasoningEffort);
|
|
58
|
+
return { kind: 'command', lines: [`reasoning effort: ${session.parsed.reasoningEffort}`] };
|
|
59
|
+
} catch (error) {
|
|
60
|
+
return { kind: 'error', lines: [`${CLI_NAME}: ${error?.message ?? error}`] };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (cmd === 'queue') {
|
|
65
|
+
const queued = rest.join(' ').trim();
|
|
66
|
+
if (!queued) return { kind: 'error', lines: [`${CLI_NAME}: /queue requires a prompt`] };
|
|
67
|
+
session.queuedMessages.push(queued);
|
|
68
|
+
return { kind: 'command', lines: [`queued: ${queued}`] };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (cmd === 'new') {
|
|
72
|
+
session.thread = undefined;
|
|
73
|
+
return { kind: 'command', lines: ['new thread'] };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (cmd === 'continue') {
|
|
77
|
+
try {
|
|
78
|
+
session.thread = interactiveContinuationThread(rest[0]);
|
|
79
|
+
return { kind: 'command', lines: [`continued: ${session.thread.id}`] };
|
|
80
|
+
} catch (error) {
|
|
81
|
+
return { kind: 'error', lines: [`${CLI_NAME}: ${error?.message ?? error}`] };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (cmd === `${CLI_NAME}:` && rest.join(' ') === 'help') return { kind: 'help', lines: await sessionSlashHelpLines(session) };
|
|
86
|
+
|
|
87
|
+
if (cmd === 'editor') {
|
|
88
|
+
const edited = await session.editorReader();
|
|
89
|
+
if (edited) await submitPromptAndQueue(session, edited);
|
|
90
|
+
return { kind: 'command', lines: [] };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (cmd === 'edit') {
|
|
94
|
+
try {
|
|
95
|
+
if (!session.thread) throw new Error('No current thread to edit');
|
|
96
|
+
const editTarget = editablePreviousPrompt(session.thread);
|
|
97
|
+
const edited = await session.editorReader(editTarget.prompt);
|
|
98
|
+
if (edited) {
|
|
99
|
+
session.thread.messages = session.thread.messages.slice(0, editTarget.index);
|
|
100
|
+
if (session.thread.messages.length === 0) {
|
|
101
|
+
session.thread.title = edited.split(/\r?\n/).find(Boolean)?.slice(0, 120) || '(empty prompt)';
|
|
102
|
+
}
|
|
103
|
+
await submitPromptAndQueue(session, edited);
|
|
104
|
+
}
|
|
105
|
+
return { kind: 'command', lines: [] };
|
|
106
|
+
} catch (error) {
|
|
107
|
+
return { kind: 'error', lines: [`${CLI_NAME}: ${error?.message ?? error}`] };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (cmd === 'thread:' && rest.join(' ') === 'archive and quit') {
|
|
112
|
+
try {
|
|
113
|
+
if (!session.thread) throw new Error('No current thread to archive');
|
|
114
|
+
await session.commandRunner('threads', ['archive', session.thread.id], session.parsed, '');
|
|
115
|
+
return { kind: 'exit', lines: [] };
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return { kind: 'error', lines: [`${CLI_NAME}: ${error?.message ?? error}`] };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (cmd === 'thread:' && rest[0] === 'set' && rest[1] === 'visibility') {
|
|
122
|
+
try {
|
|
123
|
+
if (!session.thread) throw new Error('No current thread to update');
|
|
124
|
+
await session.commandRunner('threads', ['visibility', session.thread.id, ...rest.slice(2)], session.parsed, '');
|
|
125
|
+
return { kind: 'command', lines: [] };
|
|
126
|
+
} catch (error) {
|
|
127
|
+
return { kind: 'error', lines: [`${CLI_NAME}: ${error?.message ?? error}`] };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (cmd === 'feedback:' && rest.join(' ') === 'send report with diagnostics') {
|
|
132
|
+
try {
|
|
133
|
+
if (!session.thread) throw new Error('No current thread to report');
|
|
134
|
+
await session.commandRunner('threads', ['report', session.thread.id], session.parsed, '');
|
|
135
|
+
return { kind: 'command', lines: [] };
|
|
136
|
+
} catch (error) {
|
|
137
|
+
return { kind: 'error', lines: [`${CLI_NAME}: ${error?.message ?? error}`] };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
if (cmd === 'skill:') await session.commandRunner('skill', rest, session.parsed, '');
|
|
143
|
+
else if (cmd === 'plugins:') await session.commandRunner('plugins', rest, session.parsed, '');
|
|
144
|
+
else await session.commandRunner(cmd, rest, session.parsed, '');
|
|
145
|
+
return { kind: 'command', lines: [] };
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (String(error?.message ?? '').startsWith('Unknown command:')) {
|
|
148
|
+
try {
|
|
149
|
+
await session.commandRunner('plugins', ['run', cmd], session.parsed, '');
|
|
150
|
+
return { kind: 'command', lines: [] };
|
|
151
|
+
} catch (pluginError) {
|
|
152
|
+
if (!String(pluginError?.message ?? '').startsWith('Unknown plugin command:')) throw pluginError;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return { kind: 'error', lines: [`${CLI_NAME}: ${error?.message ?? error}`] };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function slashHelpLines() {
|
|
160
|
+
return formatSlashHelpLines();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function printSlashHelp() {
|
|
164
|
+
for (const line of slashHelpLines()) console.log(line);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function safeSlashCommandCatalog(session) {
|
|
168
|
+
try {
|
|
169
|
+
return await buildSlashCommandCatalog({
|
|
170
|
+
parsed: session.parsed,
|
|
171
|
+
cwd: session.cwd ?? process.cwd(),
|
|
172
|
+
});
|
|
173
|
+
} catch {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function sessionSlashHelpLines(session) {
|
|
179
|
+
return formatCatalogHelpLines(await safeSlashCommandCatalog(session));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function formatCatalogHelpLines(catalog) {
|
|
183
|
+
return catalog.length > 0 ? formatSlashHelpLines(catalog) : slashHelpLines();
|
|
184
|
+
}
|
package/src/cli/repl.mjs
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import * as readline from 'node:readline/promises';
|
|
2
2
|
import { CLI_NAME, REPL_HISTORY_LIMIT, VERSION } from '../constants.mjs';
|
|
3
3
|
import {
|
|
4
|
-
appendReplHistory,
|
|
5
4
|
createInteractiveSession,
|
|
6
5
|
handleInteractiveInput,
|
|
7
|
-
loadReplHistory,
|
|
8
6
|
} from './interactive-core.mjs';
|
|
7
|
+
import { appendReplHistory, loadReplHistory } from './interactive-io.mjs';
|
|
9
8
|
|
|
10
9
|
export async function runInteractive(parsed, initialInput = '') {
|
|
11
10
|
const rl = readline.createInterface({
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildSlashCommandCatalog,
|
|
3
|
+
buildStaticSlashCommandCatalog,
|
|
4
|
+
filterSlashCommands,
|
|
5
|
+
} from './slash-commands.mjs';
|
|
6
|
+
import { currentSlashMatches } from './tui-render.mjs';
|
|
7
|
+
import { submitTuiText } from './tui-submit.mjs';
|
|
8
|
+
|
|
9
|
+
export function insertComposerText(model, text) {
|
|
10
|
+
model.composer = `${model.composer.slice(0, model.composerCursor)}${text}${model.composer.slice(model.composerCursor)}`;
|
|
11
|
+
model.composerCursor += text.length;
|
|
12
|
+
updateSlashState(model);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function deleteComposerText(model, kind) {
|
|
16
|
+
if (kind === 'delete') {
|
|
17
|
+
if (model.composerCursor >= model.composer.length) return;
|
|
18
|
+
model.composer = `${model.composer.slice(0, model.composerCursor)}${model.composer.slice(model.composerCursor + 1)}`;
|
|
19
|
+
} else {
|
|
20
|
+
if (model.composerCursor <= 0) return;
|
|
21
|
+
model.composer = `${model.composer.slice(0, model.composerCursor - 1)}${model.composer.slice(model.composerCursor)}`;
|
|
22
|
+
model.composerCursor -= 1;
|
|
23
|
+
}
|
|
24
|
+
updateSlashState(model);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function updateSlashState(model) {
|
|
28
|
+
const beforeCursor = model.composer.slice(0, model.composerCursor);
|
|
29
|
+
const active = beforeCursor.startsWith('/') && !/\s/.test(beforeCursor.slice(1));
|
|
30
|
+
if (!active) {
|
|
31
|
+
closeSlashMenu(model);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
model.slashOpen = true;
|
|
35
|
+
model.slashQuery = beforeCursor.replace(/^\/+/, '');
|
|
36
|
+
model.slashMatches = filterSlashCommands(model.slashCatalog, beforeCursor);
|
|
37
|
+
if (model.slashMatches.length === 0) model.slashIndex = 0;
|
|
38
|
+
else model.slashIndex = Math.min(model.slashIndex, model.slashMatches.length - 1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function closeSlashMenu(model) {
|
|
42
|
+
model.slashOpen = false;
|
|
43
|
+
model.slashQuery = '';
|
|
44
|
+
model.slashMatches = filterSlashCommands(model.slashCatalog, '');
|
|
45
|
+
model.slashIndex = 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function completeSlashSelection(model) {
|
|
49
|
+
const selected = currentSlashMatches(model)[model.slashIndex];
|
|
50
|
+
if (!selected) return;
|
|
51
|
+
model.composer = `${selected.command} `;
|
|
52
|
+
model.composerCursor = model.composer.length;
|
|
53
|
+
closeSlashMenu(model);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function acceptSlashSelection(model, session) {
|
|
57
|
+
const selected = currentSlashMatches(model)[model.slashIndex];
|
|
58
|
+
if (!selected) return;
|
|
59
|
+
const command = selected.command;
|
|
60
|
+
model.composer = '';
|
|
61
|
+
model.composerCursor = 0;
|
|
62
|
+
closeSlashMenu(model);
|
|
63
|
+
await submitTuiText(model, session, command);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function safeBuildSlashCommandCatalog(parsed) {
|
|
67
|
+
try {
|
|
68
|
+
return await buildSlashCommandCatalog({ parsed, cwd: process.cwd() });
|
|
69
|
+
} catch {
|
|
70
|
+
return buildStaticSlashCommandCatalog();
|
|
71
|
+
}
|
|
72
|
+
}
|