markov-cli 1.0.10 → 1.0.12
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/.env.example +12 -0
- package/bin/markov.js +15 -0
- package/package.json +1 -1
- package/src/agent/agentLoop.js +131 -0
- package/src/agent/context.js +102 -0
- package/src/auth.js +46 -0
- package/src/claude.js +318 -0
- package/src/commands/setup.js +72 -0
- package/src/editor/codeBlockEdits.js +27 -0
- package/src/files.js +1 -1
- package/src/input.js +67 -13
- package/src/interactive.js +348 -599
- package/src/ollama.js +173 -6
- package/src/openai.js +258 -0
- package/src/tools.js +151 -35
- package/src/ui/formatting.js +125 -0
- package/src/ui/prompts.js +116 -0
- package/src/ui/spinner.js +40 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { mkdirSync, writeFileSync } from 'fs';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import { execCommand } from '../tools.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Run a list of setup steps (mkdir / cd / run / write) in sequence.
|
|
8
|
+
* Returns true if all steps succeeded, false if any failed.
|
|
9
|
+
*/
|
|
10
|
+
export async function runSetupSteps(steps) {
|
|
11
|
+
for (const step of steps) {
|
|
12
|
+
if (step.type === 'mkdir') {
|
|
13
|
+
process.stdout.write(chalk.dim(` mkdir: ${step.path}\n`));
|
|
14
|
+
try {
|
|
15
|
+
mkdirSync(resolve(process.cwd(), step.path), { recursive: true });
|
|
16
|
+
console.log(chalk.green(` ✓ created ${step.path}\n`));
|
|
17
|
+
} catch (err) {
|
|
18
|
+
console.log(chalk.red(` ✗ ${err.message}\n`));
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
} else if (step.type === 'cd') {
|
|
22
|
+
try {
|
|
23
|
+
process.chdir(resolve(process.cwd(), step.path));
|
|
24
|
+
console.log(chalk.dim(` 📁 ${process.cwd()}\n`));
|
|
25
|
+
} catch (err) {
|
|
26
|
+
console.log(chalk.red(` ✗ no such directory: ${step.path}\n`));
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
} else if (step.type === 'run') {
|
|
30
|
+
process.stdout.write(chalk.dim(` running: ${step.cmd}\n`));
|
|
31
|
+
const { stdout, stderr, exitCode } = await execCommand(step.cmd);
|
|
32
|
+
const output = [stdout, stderr].filter(Boolean).join('\n').trim();
|
|
33
|
+
if (output) console.log(chalk.dim(output));
|
|
34
|
+
if (exitCode !== 0) {
|
|
35
|
+
console.log(chalk.red(` ✗ command failed (exit ${exitCode})\n`));
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
console.log(chalk.green(` ✓ done\n`));
|
|
39
|
+
} else if (step.type === 'write') {
|
|
40
|
+
process.stdout.write(chalk.dim(` write: ${step.path}\n`));
|
|
41
|
+
try {
|
|
42
|
+
const abs = resolve(process.cwd(), step.path);
|
|
43
|
+
const dir = abs.split('/').slice(0, -1).join('/');
|
|
44
|
+
mkdirSync(dir, { recursive: true });
|
|
45
|
+
writeFileSync(abs, step.content);
|
|
46
|
+
console.log(chalk.green(` ✓ wrote ${step.path}\n`));
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.log(chalk.red(` ✗ ${err.message}\n`));
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Next.js setup steps. */
|
|
57
|
+
export const NEXTJS_STEPS = [
|
|
58
|
+
{ type: 'run', cmd: 'npx create-next-app@latest . --typescript --tailwind --eslint --no-git' },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
/** TanStack Start setup steps (Vite + TanStack Router, type-safe full-stack). Uses @tanstack/cli (create-start is deprecated). */
|
|
62
|
+
export const TANSTACK_STEPS = [
|
|
63
|
+
{
|
|
64
|
+
type: 'run',
|
|
65
|
+
cmd: 'npx @tanstack/cli create . --package-manager npm --toolchain eslint',
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
/** Laravel setup steps. */
|
|
70
|
+
export const LARAVEL_STEPS = [
|
|
71
|
+
{ type: 'run', cmd: 'composer create-project laravel/laravel .' },
|
|
72
|
+
];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { parseEdits, renderDiff, applyEdit } from '../editor.js';
|
|
3
|
+
import { confirm } from '../ui/prompts.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Scan AI response text for fenced code blocks that reference files.
|
|
7
|
+
* Show a diff preview for each and apply on user confirmation.
|
|
8
|
+
* @param {string} responseText - The AI's response content
|
|
9
|
+
* @param {string[]} loadedFiles - Files attached via @ref
|
|
10
|
+
*/
|
|
11
|
+
export async function applyCodeBlockEdits(responseText, loadedFiles = []) {
|
|
12
|
+
const edits = parseEdits(responseText, loadedFiles);
|
|
13
|
+
if (edits.length === 0) return;
|
|
14
|
+
|
|
15
|
+
console.log(chalk.dim(`\n📝 Detected ${edits.length} file edit(s) in response:\n`));
|
|
16
|
+
|
|
17
|
+
for (const edit of edits) {
|
|
18
|
+
renderDiff(edit.filepath, edit.content);
|
|
19
|
+
const ok = await confirm(chalk.bold(`Apply changes to ${chalk.cyan(edit.filepath)}? [y/N] `));
|
|
20
|
+
if (ok) {
|
|
21
|
+
applyEdit(edit.filepath, edit.content);
|
|
22
|
+
console.log(chalk.green(` ✓ ${edit.filepath} updated\n`));
|
|
23
|
+
} else {
|
|
24
|
+
console.log(chalk.dim(` ✗ skipped ${edit.filepath}\n`));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/files.js
CHANGED
|
@@ -76,7 +76,7 @@ export function resolveFileRefs(input, cwd = process.cwd()) {
|
|
|
76
76
|
|
|
77
77
|
const contextBlock = blocks.length > 0
|
|
78
78
|
? `The user referenced the following file(s) as context:\n\n${blocks.join('\n\n')}\n\n` +
|
|
79
|
-
`ATTACHED FILES RULE: The user attached the file(s) above. If they ask you to edit, add, fix, or create anything in these files, you MUST use
|
|
79
|
+
`ATTACHED FILES RULE: The user attached the file(s) above. If they ask you to edit, add, fix, or create anything in these files, you MUST use run_terminal_command (e.g. echo, cat, tee, heredocs) or search_replace for small edits — do not output the new or modified file content in your reply (no code blocks with file content). Use the exact paths shown above (e.g. ${loaded[0] ?? 'path'}).\n\n`
|
|
80
80
|
: '';
|
|
81
81
|
|
|
82
82
|
return { content: contextBlock + input, loaded, failed };
|
package/src/input.js
CHANGED
|
@@ -21,18 +21,25 @@ function statusBar() {
|
|
|
21
21
|
return STATUS_LEFT + ' '.repeat(gap) + right;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
const INPUT_HISTORY_MAX = 100;
|
|
25
|
+
|
|
24
26
|
/**
|
|
25
|
-
* Show an interactive raw-mode prompt that supports @file autocomplete.
|
|
27
|
+
* Show an interactive raw-mode prompt that supports @file autocomplete and Up/Down input history.
|
|
26
28
|
* Returns a Promise<string|null> — null means the prompt was cancelled (Ctrl+Q).
|
|
29
|
+
* @param {string} _promptStr
|
|
30
|
+
* @param {string[]} allFiles
|
|
31
|
+
* @param {string[]} inputHistory - Mutable array of previous inputs; newest last. Up/Down navigate this; Enter pushes current input.
|
|
27
32
|
*/
|
|
28
|
-
export function chatPrompt(_promptStr, allFiles) {
|
|
33
|
+
export function chatPrompt(_promptStr, allFiles, inputHistory = []) {
|
|
29
34
|
return new Promise((resolve) => {
|
|
30
35
|
const stdin = process.stdin;
|
|
31
36
|
const stdout = process.stdout;
|
|
32
37
|
|
|
33
38
|
let buffer = '';
|
|
39
|
+
let cursorPos = 0; // 0..buffer.length; position of cursor in buffer
|
|
34
40
|
let pickerFiles = [];
|
|
35
41
|
let pickerIndex = 0;
|
|
42
|
+
let historyIndex = -1; // -1 = not navigating history
|
|
36
43
|
let cursorLineOffset = 0; // lines from top of drawn block to where cursor sits
|
|
37
44
|
|
|
38
45
|
const getAtPos = () => {
|
|
@@ -96,9 +103,12 @@ export function chatPrompt(_promptStr, allFiles) {
|
|
|
96
103
|
const inputVisualLines = Math.max(1, Math.ceil(inputVisualLen / w));
|
|
97
104
|
cursorLineOffset = inputVisualLines;
|
|
98
105
|
|
|
99
|
-
// Position cursor
|
|
100
|
-
const
|
|
101
|
-
const
|
|
106
|
+
// Position cursor: beforeCursorLen is the character offset where the cursor sits.
|
|
107
|
+
const prefixLen = visibleLen(chalk.cyan(PREFIX));
|
|
108
|
+
const beforeCursorLen = prefixLen + visibleLen(buffer.slice(0, cursorPos));
|
|
109
|
+
const lineIdx = Math.floor(beforeCursorLen / w);
|
|
110
|
+
const col = (beforeCursorLen % w) + 1;
|
|
111
|
+
if (lineIdx > 0) stdout.write(`\x1b[${lineIdx}B`);
|
|
102
112
|
stdout.write(`\x1b[${col}G`);
|
|
103
113
|
};
|
|
104
114
|
|
|
@@ -147,12 +157,17 @@ export function chatPrompt(_promptStr, allFiles) {
|
|
|
147
157
|
const atPos = getAtPos();
|
|
148
158
|
if (atPos !== -1) {
|
|
149
159
|
buffer = buffer.slice(0, atPos) + '@' + pickerFiles[pickerIndex] + ' ';
|
|
160
|
+
cursorPos = buffer.length;
|
|
150
161
|
}
|
|
151
162
|
pickerFiles = [];
|
|
152
163
|
pickerIndex = 0;
|
|
153
164
|
redraw();
|
|
154
165
|
return;
|
|
155
166
|
}
|
|
167
|
+
if (buffer.trim() && buffer !== inputHistory[inputHistory.length - 1]) {
|
|
168
|
+
inputHistory.push(buffer);
|
|
169
|
+
if (inputHistory.length > INPUT_HISTORY_MAX) inputHistory.shift();
|
|
170
|
+
}
|
|
156
171
|
clearPanel();
|
|
157
172
|
cleanup();
|
|
158
173
|
stdout.write('\n');
|
|
@@ -167,28 +182,67 @@ export function chatPrompt(_promptStr, allFiles) {
|
|
|
167
182
|
return;
|
|
168
183
|
}
|
|
169
184
|
|
|
170
|
-
// Arrow keys
|
|
185
|
+
// Arrow keys: Up/Down navigate picker when open, else input history (Up = older, Down = newer)
|
|
171
186
|
if (key === '\x1b[A') {
|
|
172
|
-
if (pickerFiles.length > 0) {
|
|
187
|
+
if (pickerFiles.length > 0) {
|
|
188
|
+
pickerIndex = (pickerIndex - 1 + pickerFiles.length) % pickerFiles.length;
|
|
189
|
+
redraw();
|
|
190
|
+
} else if (inputHistory.length > 0) {
|
|
191
|
+
historyIndex = historyIndex === -1 ? inputHistory.length - 1 : Math.max(0, historyIndex - 1);
|
|
192
|
+
buffer = inputHistory[historyIndex];
|
|
193
|
+
cursorPos = buffer.length;
|
|
194
|
+
updatePicker();
|
|
195
|
+
redraw();
|
|
196
|
+
}
|
|
173
197
|
return;
|
|
174
198
|
}
|
|
175
199
|
if (key === '\x1b[B') {
|
|
176
|
-
if (pickerFiles.length > 0) {
|
|
200
|
+
if (pickerFiles.length > 0) {
|
|
201
|
+
pickerIndex = (pickerIndex + 1) % pickerFiles.length;
|
|
202
|
+
redraw();
|
|
203
|
+
} else if (historyIndex >= 0) {
|
|
204
|
+
historyIndex++;
|
|
205
|
+
if (historyIndex >= inputHistory.length) {
|
|
206
|
+
historyIndex = -1;
|
|
207
|
+
buffer = '';
|
|
208
|
+
} else {
|
|
209
|
+
buffer = inputHistory[historyIndex];
|
|
210
|
+
}
|
|
211
|
+
cursorPos = buffer.length;
|
|
212
|
+
updatePicker();
|
|
213
|
+
redraw();
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
// Left/Right: move cursor within the line
|
|
218
|
+
if (key === '\x1b[D') {
|
|
219
|
+
if (cursorPos > 0) { cursorPos--; redraw(); }
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (key === '\x1b[C') {
|
|
223
|
+
if (cursorPos < buffer.length) { cursorPos++; redraw(); }
|
|
177
224
|
return;
|
|
178
225
|
}
|
|
179
|
-
if (key === '\x1b[C' || key === '\x1b[D') return; // left/right — ignore
|
|
180
226
|
|
|
181
|
-
// Backspace
|
|
227
|
+
// Backspace: delete character before cursor
|
|
182
228
|
if (key === '\x7f' || key === '\b') {
|
|
183
|
-
|
|
229
|
+
historyIndex = -1;
|
|
230
|
+
if (cursorPos > 0) {
|
|
231
|
+
buffer = buffer.slice(0, cursorPos - 1) + buffer.slice(cursorPos);
|
|
232
|
+
cursorPos--;
|
|
233
|
+
updatePicker();
|
|
234
|
+
redraw();
|
|
235
|
+
}
|
|
184
236
|
return;
|
|
185
237
|
}
|
|
186
238
|
|
|
187
239
|
// Ignore other control chars
|
|
188
240
|
if (key < ' ') return;
|
|
189
241
|
|
|
190
|
-
// Printable
|
|
191
|
-
|
|
242
|
+
// Printable: insert at cursor
|
|
243
|
+
historyIndex = -1;
|
|
244
|
+
buffer = buffer.slice(0, cursorPos) + key + buffer.slice(cursorPos);
|
|
245
|
+
cursorPos += key.length;
|
|
192
246
|
updatePicker();
|
|
193
247
|
redraw();
|
|
194
248
|
};
|