copperhead 0.7.0 → 0.8.0
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 +36 -4
- package/dist/agent/animate.js +76 -0
- package/dist/agent/animate.js.map +1 -0
- package/dist/agent/box.js +89 -0
- package/dist/agent/box.js.map +1 -0
- package/dist/agent/dock-renderer.js +173 -0
- package/dist/agent/dock-renderer.js.map +1 -0
- package/dist/agent/logo.js +21 -0
- package/dist/agent/logo.js.map +1 -0
- package/dist/agent/loop.js +15 -5
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/providers/claude-code.js +1 -212
- package/dist/agent/providers/claude-code.js.map +1 -1
- package/dist/agent/providers/cursor.js +317 -0
- package/dist/agent/providers/cursor.js.map +1 -0
- package/dist/agent/providers/tool-protocol.js +205 -0
- package/dist/agent/providers/tool-protocol.js.map +1 -0
- package/dist/agent/render.js +32 -15
- package/dist/agent/render.js.map +1 -1
- package/dist/agent/runmeta.js +4 -5
- package/dist/agent/runmeta.js.map +1 -1
- package/dist/agent/theme.js +84 -0
- package/dist/agent/theme.js.map +1 -0
- package/dist/cli.js +134 -13
- package/dist/cli.js.map +1 -1
- package/dist/commands/create.js +41 -32
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/demo.js +146 -0
- package/dist/commands/demo.js.map +1 -0
- package/dist/commands/doctor.js +240 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/repl-inspect.js +342 -0
- package/dist/commands/repl-inspect.js.map +1 -0
- package/dist/commands/repl.js +618 -0
- package/dist/commands/repl.js.map +1 -0
- package/dist/config.js +5 -2
- package/dist/config.js.map +1 -1
- package/dist/kicad/cli.js +126 -6
- package/dist/kicad/cli.js.map +1 -1
- package/dist/util/cli-args.js +35 -0
- package/dist/util/cli-args.js.map +1 -0
- package/dist/util/dock.js +155 -0
- package/dist/util/dock.js.map +1 -0
- package/dist/util/git.js +129 -4
- package/dist/util/git.js.map +1 -1
- package/dist/util/live-prompt.js +542 -0
- package/dist/util/live-prompt.js.map +1 -0
- package/dist/util/paths.js +9 -0
- package/dist/util/paths.js.map +1 -1
- package/dist/util/select.js +172 -0
- package/dist/util/select.js.map +1 -0
- package/package.json +3 -2
- package/src/agent/animate.ts +90 -0
- package/src/agent/box.ts +99 -0
- package/src/agent/dock-renderer.ts +181 -0
- package/src/agent/logo.ts +23 -0
- package/src/agent/loop.ts +15 -5
- package/src/agent/providers/claude-code.ts +2 -216
- package/src/agent/providers/cursor.ts +364 -0
- package/src/agent/providers/tool-protocol.ts +212 -0
- package/src/agent/render.ts +33 -16
- package/src/agent/runmeta.ts +6 -7
- package/src/agent/theme.ts +91 -0
- package/src/cli.ts +139 -15
- package/src/commands/create.ts +81 -30
- package/src/commands/demo.ts +184 -0
- package/src/commands/doctor.ts +289 -0
- package/src/commands/repl-inspect.ts +353 -0
- package/src/commands/repl.ts +685 -0
- package/src/config.ts +6 -3
- package/src/kicad/cli.ts +132 -7
- package/src/layout/claude-ui-layout.md +72 -0
- package/src/layout/repl-ui-layout.md +139 -0
- package/src/util/cli-args.ts +42 -0
- package/src/util/dock.ts +161 -0
- package/src/util/git.ts +140 -4
- package/src/util/live-prompt.ts +595 -0
- package/src/util/paths.ts +10 -0
- package/src/util/select.ts +192 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal arrow-key select menu for TTY prompts (no extra deps).
|
|
3
|
+
* Renders an in-place "dropdown" navigable with ↑/↓ + Enter.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { copper, dim } from '../agent/theme.js';
|
|
7
|
+
|
|
8
|
+
export interface SelectItem {
|
|
9
|
+
value: string;
|
|
10
|
+
label: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SelectOptions {
|
|
15
|
+
title?: string;
|
|
16
|
+
items: SelectItem[];
|
|
17
|
+
input?: NodeJS.ReadStream;
|
|
18
|
+
output?: NodeJS.WriteStream;
|
|
19
|
+
/** Injected key stream for tests. */
|
|
20
|
+
keys?: AsyncIterable<string>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const HIDE = '\x1b[?25l';
|
|
24
|
+
const SHOW = '\x1b[?25h';
|
|
25
|
+
const CLEAR_LINE = '\r\x1b[2K';
|
|
26
|
+
|
|
27
|
+
function menuLines(title: string, items: SelectItem[], index: number): string[] {
|
|
28
|
+
const width = Math.max(...items.map((i) => i.label.length), 8);
|
|
29
|
+
return [
|
|
30
|
+
copper(` ${title}`),
|
|
31
|
+
dim(' ↑/↓ move · Enter select · Esc cancel'),
|
|
32
|
+
'',
|
|
33
|
+
...items.map((item, i) => {
|
|
34
|
+
const cursor = i === index ? copper('❯') : ' ';
|
|
35
|
+
const label = i === index ? copper(item.label.padEnd(width)) : dim(item.label.padEnd(width));
|
|
36
|
+
const desc = item.description ? dim(` ${item.description}`) : '';
|
|
37
|
+
return ` ${cursor} ${label}${desc}`;
|
|
38
|
+
}),
|
|
39
|
+
'',
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function* stdinKeys(input: NodeJS.ReadStream): AsyncGenerator<string> {
|
|
44
|
+
// Listener-based on purpose: `for await` over a Readable destroys the
|
|
45
|
+
// stream when the consumer breaks out, which would kill stdin for
|
|
46
|
+
// whatever runs after the menu (e.g. the REPL's KeyReader).
|
|
47
|
+
const wasRaw = input.isRaw;
|
|
48
|
+
if (typeof input.setRawMode === 'function') input.setRawMode(true);
|
|
49
|
+
input.resume();
|
|
50
|
+
input.setEncoding('utf8');
|
|
51
|
+
|
|
52
|
+
const queue: string[] = [];
|
|
53
|
+
let ended = false;
|
|
54
|
+
let notify: (() => void) | null = null;
|
|
55
|
+
const onData = (c: string | Buffer): void => {
|
|
56
|
+
queue.push(String(c));
|
|
57
|
+
notify?.();
|
|
58
|
+
};
|
|
59
|
+
const onEnd = (): void => {
|
|
60
|
+
ended = true;
|
|
61
|
+
notify?.();
|
|
62
|
+
};
|
|
63
|
+
input.on('data', onData);
|
|
64
|
+
input.on('end', onEnd);
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
for (;;) {
|
|
68
|
+
while (queue.length) {
|
|
69
|
+
const s = queue.shift()!;
|
|
70
|
+
let i = 0;
|
|
71
|
+
while (i < s.length) {
|
|
72
|
+
if (s[i] === '\x1b' && s[i + 1] === '[') {
|
|
73
|
+
yield s.slice(i, i + 3);
|
|
74
|
+
i += 3;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
// Lone Esc
|
|
78
|
+
if (s[i] === '\x1b') {
|
|
79
|
+
yield '\x1b';
|
|
80
|
+
i += 1;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
yield s[i]!;
|
|
84
|
+
i += 1;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (ended) return;
|
|
88
|
+
await new Promise<void>((resolve) => {
|
|
89
|
+
notify = resolve;
|
|
90
|
+
});
|
|
91
|
+
notify = null;
|
|
92
|
+
}
|
|
93
|
+
} finally {
|
|
94
|
+
input.off('data', onData);
|
|
95
|
+
input.off('end', onEnd);
|
|
96
|
+
input.pause();
|
|
97
|
+
if (typeof input.setRawMode === 'function') input.setRawMode(wasRaw ?? false);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Model choices offered when no model is configured anywhere. */
|
|
102
|
+
export const MODEL_CHOICES: SelectItem[] = [
|
|
103
|
+
{ value: 'claude-code', label: 'claude-code', description: 'Claude Code CLI (saved login)' },
|
|
104
|
+
{ value: 'codex', label: 'codex', description: 'Codex CLI (saved login)' },
|
|
105
|
+
{ value: 'cursor', label: 'cursor', description: 'Cursor CLI (saved login)' },
|
|
106
|
+
{ value: 'claude', label: 'claude', description: 'Anthropic API (needs ANTHROPIC_API_KEY)' },
|
|
107
|
+
{ value: 'gpt-5', label: 'gpt-5', description: 'OpenAI API (needs OPENAI_API_KEY)' },
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Interactive model picker for a TTY session with no configured model.
|
|
112
|
+
* Resolves to the chosen model id, or null if cancelled.
|
|
113
|
+
*/
|
|
114
|
+
export async function pickModel(opts: Omit<SelectOptions, 'title' | 'items'> = {}): Promise<string | null> {
|
|
115
|
+
return selectMenu({ title: 'Select a model for this session', items: MODEL_CHOICES, ...opts });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Show a selectable list. Resolves to the chosen value, or null if cancelled.
|
|
120
|
+
*/
|
|
121
|
+
export async function selectMenu(opts: SelectOptions): Promise<string | null> {
|
|
122
|
+
const items = opts.items;
|
|
123
|
+
if (!items.length) return null;
|
|
124
|
+
|
|
125
|
+
const input = opts.input ?? process.stdin;
|
|
126
|
+
const output = opts.output ?? process.stdout;
|
|
127
|
+
const title = opts.title ?? 'Select';
|
|
128
|
+
let index = 0;
|
|
129
|
+
let lineCount = 0;
|
|
130
|
+
|
|
131
|
+
const paint = (): void => {
|
|
132
|
+
const lines = menuLines(title, items, index);
|
|
133
|
+
if (lineCount > 0) {
|
|
134
|
+
// Move to the first line of the previous paint and rewrite in place.
|
|
135
|
+
output.write(`\x1b[${lineCount}A`);
|
|
136
|
+
}
|
|
137
|
+
output.write(HIDE);
|
|
138
|
+
for (const line of lines) {
|
|
139
|
+
output.write(CLEAR_LINE + line + '\n');
|
|
140
|
+
}
|
|
141
|
+
lineCount = lines.length;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const erase = (): void => {
|
|
145
|
+
if (lineCount <= 0) {
|
|
146
|
+
output.write(SHOW);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
output.write(`\x1b[${lineCount}A`);
|
|
150
|
+
for (let i = 0; i < lineCount; i++) output.write(CLEAR_LINE + (i < lineCount - 1 ? '\n' : ''));
|
|
151
|
+
if (lineCount > 1) output.write(`\x1b[${lineCount - 1}A`);
|
|
152
|
+
output.write('\r' + SHOW);
|
|
153
|
+
lineCount = 0;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
paint();
|
|
157
|
+
|
|
158
|
+
const keys = opts.keys ?? stdinKeys(input);
|
|
159
|
+
try {
|
|
160
|
+
for await (const key of keys) {
|
|
161
|
+
if (key === '\x03' || key === '\x1b') {
|
|
162
|
+
erase();
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
if (key === '\r' || key === '\n') {
|
|
166
|
+
const chosen = items[index]!.value;
|
|
167
|
+
erase();
|
|
168
|
+
return chosen;
|
|
169
|
+
}
|
|
170
|
+
if (key === '\x1b[A' || key === 'k') {
|
|
171
|
+
index = (index - 1 + items.length) % items.length;
|
|
172
|
+
paint();
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (key === '\x1b[B' || key === 'j') {
|
|
176
|
+
index = (index + 1) % items.length;
|
|
177
|
+
paint();
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (key >= '1' && key <= '9') {
|
|
181
|
+
const n = Number(key) - 1;
|
|
182
|
+
if (n >= 0 && n < items.length) {
|
|
183
|
+
index = n;
|
|
184
|
+
paint();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
} finally {
|
|
189
|
+
erase();
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|