copperhead 0.6.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 +130 -18
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/prompts.js +2 -1
- package/dist/agent/prompts.js.map +1 -1
- package/dist/agent/providers/claude-code.js +85 -116
- 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/recovery.js +148 -0
- package/dist/agent/recovery.js.map +1 -0
- package/dist/agent/render.js +44 -12
- package/dist/agent/render.js.map +1 -1
- package/dist/agent/response-cache.js +81 -0
- package/dist/agent/response-cache.js.map +1 -0
- 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/agent/tools.js +61 -4
- package/dist/agent/tools.js.map +1 -1
- package/dist/agent/transcript.js.map +1 -1
- package/dist/cli.js +134 -13
- package/dist/cli.js.map +1 -1
- package/dist/commands/create.js +482 -38
- 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 +24 -2
- package/dist/config.js.map +1 -1
- package/dist/kicad/bootstrap.js +166 -0
- package/dist/kicad/bootstrap.js.map +1 -0
- package/dist/kicad/cli.js +126 -6
- package/dist/kicad/cli.js.map +1 -1
- package/dist/kicad/spice.js +306 -0
- package/dist/kicad/spice.js.map +1 -0
- package/dist/kicad/symlib.js +228 -0
- package/dist/kicad/symlib.js.map +1 -0
- package/dist/memory/bom-table.js +193 -22
- package/dist/memory/bom-table.js.map +1 -1
- package/dist/memory/drift.js +33 -11
- package/dist/memory/drift.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 +165 -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/preflight.js +37 -0
- package/dist/util/preflight.js.map +1 -1
- package/dist/util/retry.js +23 -0
- package/dist/util/retry.js.map +1 -1
- package/dist/util/select.js +172 -0
- package/dist/util/select.js.map +1 -0
- package/dist/util/tmp.js +119 -0
- package/dist/util/tmp.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 +148 -18
- package/src/agent/prompts.ts +2 -1
- package/src/agent/providers/claude-code.ts +91 -122
- package/src/agent/providers/cursor.ts +364 -0
- package/src/agent/providers/tool-protocol.ts +212 -0
- package/src/agent/recovery.ts +162 -0
- package/src/agent/render.ts +56 -12
- package/src/agent/response-cache.ts +80 -0
- package/src/agent/runmeta.ts +6 -7
- package/src/agent/theme.ts +91 -0
- package/src/agent/tools.ts +62 -4
- package/src/agent/transcript.ts +1 -0
- package/src/agent/types.ts +17 -0
- package/src/cli.ts +139 -15
- package/src/commands/create.ts +581 -40
- 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 +40 -3
- package/src/kicad/bootstrap.ts +181 -0
- package/src/kicad/cli.ts +132 -7
- package/src/kicad/spice.ts +399 -0
- package/src/kicad/symlib.ts +248 -0
- package/src/layout/claude-ui-layout.md +72 -0
- package/src/layout/repl-ui-layout.md +139 -0
- package/src/memory/bom-table.ts +191 -20
- package/src/memory/drift.ts +42 -11
- package/src/util/cli-args.ts +42 -0
- package/src/util/dock.ts +161 -0
- package/src/util/git.ts +176 -4
- package/src/util/live-prompt.ts +595 -0
- package/src/util/paths.ts +10 -0
- package/src/util/preflight.ts +44 -0
- package/src/util/retry.ts +29 -0
- package/src/util/select.ts +192 -0
- package/src/util/tmp.ts +113 -0
package/dist/util/retry.js
CHANGED
|
@@ -3,6 +3,29 @@ export function isRateLimit(err) {
|
|
|
3
3
|
?? err?.statusCode;
|
|
4
4
|
return status === 429;
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* Detect a saved-login SESSION / USAGE limit (claude-code, codex), distinct from
|
|
8
|
+
* an HTTP 429 rate limit and from a code bug (2.4, I13). It is not a transient
|
|
9
|
+
* blip to back off on: it names its own reset time and clears only then. Because
|
|
10
|
+
* every completed turn is already in `.copperhead/llm-cache/`, re-running after
|
|
11
|
+
* the reset replays them at ~0 tokens and resumes in place — so the right
|
|
12
|
+
* handling is a schedulable pause with the reset time surfaced, not a bare
|
|
13
|
+
* "provider error". Returns the parsed reset time (verbatim) or null when the
|
|
14
|
+
* error is not a session/usage limit.
|
|
15
|
+
*/
|
|
16
|
+
export function sessionLimit(err) {
|
|
17
|
+
const status = err?.status
|
|
18
|
+
?? err?.statusCode;
|
|
19
|
+
if (status === 429)
|
|
20
|
+
return null; // a real rate limit: handled by backoff, not a pause
|
|
21
|
+
const msg = err?.message ?? '';
|
|
22
|
+
if (!/(session|usage|weekly)\s+limit|hit your .*limit|reached your usage|limit .*reset/i.test(msg)) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
// "resets 1:40pm", "resets at 1:40 pm", "reset at 13:40" — capture the clock text.
|
|
26
|
+
const reset = msg.match(/reset[s]?(?:\s+at)?\s+([0-9]{1,2}(?::[0-9]{2})?\s*(?:am|pm)?)/i);
|
|
27
|
+
return { resetsAt: reset?.[1]?.trim() ?? null };
|
|
28
|
+
}
|
|
6
29
|
/** Exponential backoff ×N for rate limits (SPEC §4.5). */
|
|
7
30
|
export async function withRetry(fn, opts = {}) {
|
|
8
31
|
const retries = opts.retries ?? 3;
|
package/dist/util/retry.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/util/retry.ts"],"names":[],"mappings":"AAQA,MAAM,UAAU,WAAW,CAAC,GAAY;IACtC,MAAM,MAAM,GAAI,GAAgD,EAAE,MAAM;WAClE,GAA+B,EAAE,UAAU,CAAC;IAClD,OAAO,MAAM,KAAK,GAAG,CAAC;AACxB,CAAC;AAED,0DAA0D;AAC1D,MAAM,CAAC,KAAK,UAAU,SAAS,CAAI,EAAoB,EAAE,OAAkB,EAAE;IAC3E,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,CAAC,CAAC;IAClC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC;IACnC,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,WAAW,CAAC;IACpD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;IAC5E,IAAI,OAAgB,CAAC;IACrB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC;QACpD,IAAI,CAAC;YACH,OAAO,MAAM,EAAE,EAAE,CAAC;QACpB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,GAAG,GAAG,CAAC;YACd,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,OAAO,KAAK,OAAO;gBAAE,MAAM,GAAG,CAAC;YACxD,IAAI,CAAC,OAAO,EAAE,CAAC,OAAO,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC;YACjC,MAAM,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,OAAO,CAAC,CAAC;QACrC,CAAC;IACH,CAAC;IACD,MAAM,OAAO,CAAC;AAChB,CAAC"}
|
|
1
|
+
{"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/util/retry.ts"],"names":[],"mappings":"AAQA,MAAM,UAAU,WAAW,CAAC,GAAY;IACtC,MAAM,MAAM,GAAI,GAAgD,EAAE,MAAM;WAClE,GAA+B,EAAE,UAAU,CAAC;IAClD,OAAO,MAAM,KAAK,GAAG,CAAC;AACxB,CAAC;AAQD;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAAC,GAAY;IACvC,MAAM,MAAM,GAAI,GAAgD,EAAE,MAAM;WAClE,GAA+B,EAAE,UAAU,CAAC;IAClD,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC,CAAC,qDAAqD;IACtF,MAAM,GAAG,GAAI,GAAa,EAAE,OAAO,IAAI,EAAE,CAAC;IAC1C,IAAI,CAAC,mFAAmF,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACnG,OAAO,IAAI,CAAC;IACd,CAAC;IACD,mFAAmF;IACnF,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,gEAAgE,CAAC,CAAC;IAC1F,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,IAAI,EAAE,CAAC;AAClD,CAAC;AAED,0DAA0D;AAC1D,MAAM,CAAC,KAAK,UAAU,SAAS,CAAI,EAAoB,EAAE,OAAkB,EAAE;IAC3E,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,CAAC,CAAC;IAClC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC;IACnC,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,WAAW,CAAC;IACpD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;IAC5E,IAAI,OAAgB,CAAC;IACrB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC;QACpD,IAAI,CAAC;YACH,OAAO,MAAM,EAAE,EAAE,CAAC;QACpB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,GAAG,GAAG,CAAC;YACd,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,OAAO,KAAK,OAAO;gBAAE,MAAM,GAAG,CAAC;YACxD,IAAI,CAAC,OAAO,EAAE,CAAC,OAAO,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC;YACjC,MAAM,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,OAAO,CAAC,CAAC;QACrC,CAAC;IACH,CAAC;IACD,MAAM,OAAO,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal arrow-key select menu for TTY prompts (no extra deps).
|
|
3
|
+
* Renders an in-place "dropdown" navigable with ↑/↓ + Enter.
|
|
4
|
+
*/
|
|
5
|
+
import { copper, dim } from '../agent/theme.js';
|
|
6
|
+
const HIDE = '\x1b[?25l';
|
|
7
|
+
const SHOW = '\x1b[?25h';
|
|
8
|
+
const CLEAR_LINE = '\r\x1b[2K';
|
|
9
|
+
function menuLines(title, items, index) {
|
|
10
|
+
const width = Math.max(...items.map((i) => i.label.length), 8);
|
|
11
|
+
return [
|
|
12
|
+
copper(` ${title}`),
|
|
13
|
+
dim(' ↑/↓ move · Enter select · Esc cancel'),
|
|
14
|
+
'',
|
|
15
|
+
...items.map((item, i) => {
|
|
16
|
+
const cursor = i === index ? copper('❯') : ' ';
|
|
17
|
+
const label = i === index ? copper(item.label.padEnd(width)) : dim(item.label.padEnd(width));
|
|
18
|
+
const desc = item.description ? dim(` ${item.description}`) : '';
|
|
19
|
+
return ` ${cursor} ${label}${desc}`;
|
|
20
|
+
}),
|
|
21
|
+
'',
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
async function* stdinKeys(input) {
|
|
25
|
+
// Listener-based on purpose: `for await` over a Readable destroys the
|
|
26
|
+
// stream when the consumer breaks out, which would kill stdin for
|
|
27
|
+
// whatever runs after the menu (e.g. the REPL's KeyReader).
|
|
28
|
+
const wasRaw = input.isRaw;
|
|
29
|
+
if (typeof input.setRawMode === 'function')
|
|
30
|
+
input.setRawMode(true);
|
|
31
|
+
input.resume();
|
|
32
|
+
input.setEncoding('utf8');
|
|
33
|
+
const queue = [];
|
|
34
|
+
let ended = false;
|
|
35
|
+
let notify = null;
|
|
36
|
+
const onData = (c) => {
|
|
37
|
+
queue.push(String(c));
|
|
38
|
+
notify?.();
|
|
39
|
+
};
|
|
40
|
+
const onEnd = () => {
|
|
41
|
+
ended = true;
|
|
42
|
+
notify?.();
|
|
43
|
+
};
|
|
44
|
+
input.on('data', onData);
|
|
45
|
+
input.on('end', onEnd);
|
|
46
|
+
try {
|
|
47
|
+
for (;;) {
|
|
48
|
+
while (queue.length) {
|
|
49
|
+
const s = queue.shift();
|
|
50
|
+
let i = 0;
|
|
51
|
+
while (i < s.length) {
|
|
52
|
+
if (s[i] === '\x1b' && s[i + 1] === '[') {
|
|
53
|
+
yield s.slice(i, i + 3);
|
|
54
|
+
i += 3;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
// Lone Esc
|
|
58
|
+
if (s[i] === '\x1b') {
|
|
59
|
+
yield '\x1b';
|
|
60
|
+
i += 1;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
yield s[i];
|
|
64
|
+
i += 1;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (ended)
|
|
68
|
+
return;
|
|
69
|
+
await new Promise((resolve) => {
|
|
70
|
+
notify = resolve;
|
|
71
|
+
});
|
|
72
|
+
notify = null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
input.off('data', onData);
|
|
77
|
+
input.off('end', onEnd);
|
|
78
|
+
input.pause();
|
|
79
|
+
if (typeof input.setRawMode === 'function')
|
|
80
|
+
input.setRawMode(wasRaw ?? false);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Model choices offered when no model is configured anywhere. */
|
|
84
|
+
export const MODEL_CHOICES = [
|
|
85
|
+
{ value: 'claude-code', label: 'claude-code', description: 'Claude Code CLI (saved login)' },
|
|
86
|
+
{ value: 'codex', label: 'codex', description: 'Codex CLI (saved login)' },
|
|
87
|
+
{ value: 'cursor', label: 'cursor', description: 'Cursor CLI (saved login)' },
|
|
88
|
+
{ value: 'claude', label: 'claude', description: 'Anthropic API (needs ANTHROPIC_API_KEY)' },
|
|
89
|
+
{ value: 'gpt-5', label: 'gpt-5', description: 'OpenAI API (needs OPENAI_API_KEY)' },
|
|
90
|
+
];
|
|
91
|
+
/**
|
|
92
|
+
* Interactive model picker for a TTY session with no configured model.
|
|
93
|
+
* Resolves to the chosen model id, or null if cancelled.
|
|
94
|
+
*/
|
|
95
|
+
export async function pickModel(opts = {}) {
|
|
96
|
+
return selectMenu({ title: 'Select a model for this session', items: MODEL_CHOICES, ...opts });
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Show a selectable list. Resolves to the chosen value, or null if cancelled.
|
|
100
|
+
*/
|
|
101
|
+
export async function selectMenu(opts) {
|
|
102
|
+
const items = opts.items;
|
|
103
|
+
if (!items.length)
|
|
104
|
+
return null;
|
|
105
|
+
const input = opts.input ?? process.stdin;
|
|
106
|
+
const output = opts.output ?? process.stdout;
|
|
107
|
+
const title = opts.title ?? 'Select';
|
|
108
|
+
let index = 0;
|
|
109
|
+
let lineCount = 0;
|
|
110
|
+
const paint = () => {
|
|
111
|
+
const lines = menuLines(title, items, index);
|
|
112
|
+
if (lineCount > 0) {
|
|
113
|
+
// Move to the first line of the previous paint and rewrite in place.
|
|
114
|
+
output.write(`\x1b[${lineCount}A`);
|
|
115
|
+
}
|
|
116
|
+
output.write(HIDE);
|
|
117
|
+
for (const line of lines) {
|
|
118
|
+
output.write(CLEAR_LINE + line + '\n');
|
|
119
|
+
}
|
|
120
|
+
lineCount = lines.length;
|
|
121
|
+
};
|
|
122
|
+
const erase = () => {
|
|
123
|
+
if (lineCount <= 0) {
|
|
124
|
+
output.write(SHOW);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
output.write(`\x1b[${lineCount}A`);
|
|
128
|
+
for (let i = 0; i < lineCount; i++)
|
|
129
|
+
output.write(CLEAR_LINE + (i < lineCount - 1 ? '\n' : ''));
|
|
130
|
+
if (lineCount > 1)
|
|
131
|
+
output.write(`\x1b[${lineCount - 1}A`);
|
|
132
|
+
output.write('\r' + SHOW);
|
|
133
|
+
lineCount = 0;
|
|
134
|
+
};
|
|
135
|
+
paint();
|
|
136
|
+
const keys = opts.keys ?? stdinKeys(input);
|
|
137
|
+
try {
|
|
138
|
+
for await (const key of keys) {
|
|
139
|
+
if (key === '\x03' || key === '\x1b') {
|
|
140
|
+
erase();
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
if (key === '\r' || key === '\n') {
|
|
144
|
+
const chosen = items[index].value;
|
|
145
|
+
erase();
|
|
146
|
+
return chosen;
|
|
147
|
+
}
|
|
148
|
+
if (key === '\x1b[A' || key === 'k') {
|
|
149
|
+
index = (index - 1 + items.length) % items.length;
|
|
150
|
+
paint();
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (key === '\x1b[B' || key === 'j') {
|
|
154
|
+
index = (index + 1) % items.length;
|
|
155
|
+
paint();
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (key >= '1' && key <= '9') {
|
|
159
|
+
const n = Number(key) - 1;
|
|
160
|
+
if (n >= 0 && n < items.length) {
|
|
161
|
+
index = n;
|
|
162
|
+
paint();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
erase();
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=select.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"select.js","sourceRoot":"","sources":["../../src/util/select.ts"],"names":[],"mappings":"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"}
|
package/dist/util/tmp.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { readdir, stat, rm } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
/** Newest local-history entries to keep when capping `.history/` (see
|
|
5
|
+
* pruneHistoryDir). Enough to preserve a useful recovery window, bounded enough
|
|
6
|
+
* that the dir cannot grow without limit across a long run. */
|
|
7
|
+
export const DEFAULT_HISTORY_KEEP = 200;
|
|
8
|
+
/**
|
|
9
|
+
* Cap the growth of a repo's `.history/` directory (4.1). KiCad (and editor
|
|
10
|
+
* local-history) rewrite a snapshot on every project touch, so across a long
|
|
11
|
+
* run `.history/` grows without bound and was a contributor to the disk-fill
|
|
12
|
+
* halt (I8). It is gitignored, so its contents are disposable: keep the newest
|
|
13
|
+
* `keepNewest` files by mtime and remove the rest. Recursive (local history
|
|
14
|
+
* mirrors the workspace tree), best-effort (every error is swallowed — pruning
|
|
15
|
+
* housekeeping must never fail a run), and a no-op when the dir is absent or
|
|
16
|
+
* already under the cap. Returns the number of files removed.
|
|
17
|
+
*/
|
|
18
|
+
export async function pruneHistoryDir(repoRoot, keepNewest = DEFAULT_HISTORY_KEEP) {
|
|
19
|
+
const root = path.join(repoRoot, '.history');
|
|
20
|
+
const files = [];
|
|
21
|
+
const walk = async (dir) => {
|
|
22
|
+
let entries;
|
|
23
|
+
try {
|
|
24
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return; // unreadable dir — skip it
|
|
28
|
+
}
|
|
29
|
+
for (const e of entries) {
|
|
30
|
+
const full = path.join(dir, e.name);
|
|
31
|
+
if (e.isDirectory()) {
|
|
32
|
+
await walk(full);
|
|
33
|
+
}
|
|
34
|
+
else if (e.isFile()) {
|
|
35
|
+
try {
|
|
36
|
+
const st = await stat(full);
|
|
37
|
+
files.push({ full, mtimeMs: st.mtimeMs });
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// stat race — skip this file
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
await walk(root);
|
|
46
|
+
if (files.length <= keepNewest)
|
|
47
|
+
return 0;
|
|
48
|
+
files.sort((a, b) => b.mtimeMs - a.mtimeMs); // newest first
|
|
49
|
+
let removed = 0;
|
|
50
|
+
for (const f of files.slice(keepNewest)) {
|
|
51
|
+
try {
|
|
52
|
+
await rm(f.full, { force: true });
|
|
53
|
+
removed++;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// permission/race — leave it, keep pruning the rest
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return removed;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Every scratch dir copperhead makes under the OS temp dir shares this prefix:
|
|
63
|
+
* kicad-cli ERC/DRC (`copperhead-`), the KiCad edit probe (`copperhead-validate-`),
|
|
64
|
+
* the failed-run backup (`copperhead-runs-`), and the provider working dirs
|
|
65
|
+
* (`copperhead-cc-`, `copperhead-codex-`). Each site removes its own dir in a
|
|
66
|
+
* `finally`, but a watchdog SIGKILL of the process tree or a hard abort skips
|
|
67
|
+
* that cleanup, so stale dirs accumulate across runs and can eventually fill the
|
|
68
|
+
* disk (I8). A prefix match lets one sweep reclaim all of them.
|
|
69
|
+
*/
|
|
70
|
+
export const TEMP_PREFIX = 'copperhead-';
|
|
71
|
+
/** Default staleness cutoff for the startup sweep: 2h. Safe even for multi-hour
|
|
72
|
+
* runs (10-min turns × per-stage retries × 8 stages): a live run's only
|
|
73
|
+
* long-lived scratch dir is the provider's reused cwd, which is `utimes`-touched
|
|
74
|
+
* every turn (ClaudeCodeProvider.ensureCwd), so its mtime never goes stale while
|
|
75
|
+
* the process is alive; per-call kicad-cli dirs are removed within a turn. A dir
|
|
76
|
+
* older than this therefore belongs to a dead run, and the window is short enough
|
|
77
|
+
* that such a leak is reclaimed on the very next invocation. */
|
|
78
|
+
export const DEFAULT_STALE_MS = 2 * 60 * 60 * 1000;
|
|
79
|
+
/**
|
|
80
|
+
* Remove leaked `copperhead-*` scratch dirs left in the OS temp dir by earlier
|
|
81
|
+
* runs whose `finally` cleanup was skipped (watchdog kill / hard abort). Only
|
|
82
|
+
* dirs whose mtime is older than `maxAgeMs` are removed, so a concurrent run's
|
|
83
|
+
* fresh scratch dirs are never touched. Best-effort: every error is swallowed
|
|
84
|
+
* (a temp dir we can't stat or remove is not worth failing a run over), and the
|
|
85
|
+
* function returns the paths it removed so a caller can log the reclaim.
|
|
86
|
+
*
|
|
87
|
+
* `now` is injected so the behaviour is deterministically testable; callers pass
|
|
88
|
+
* `Date.now()`.
|
|
89
|
+
*/
|
|
90
|
+
export async function sweepStaleTempDirs(now, maxAgeMs = DEFAULT_STALE_MS) {
|
|
91
|
+
const root = tmpdir();
|
|
92
|
+
const removed = [];
|
|
93
|
+
let entries;
|
|
94
|
+
try {
|
|
95
|
+
entries = await readdir(root);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return removed; // no temp dir / not readable — nothing to sweep
|
|
99
|
+
}
|
|
100
|
+
for (const name of entries) {
|
|
101
|
+
if (!name.startsWith(TEMP_PREFIX))
|
|
102
|
+
continue;
|
|
103
|
+
const full = path.join(root, name);
|
|
104
|
+
try {
|
|
105
|
+
const st = await stat(full);
|
|
106
|
+
if (!st.isDirectory())
|
|
107
|
+
continue;
|
|
108
|
+
if (now - st.mtimeMs < maxAgeMs)
|
|
109
|
+
continue; // too fresh: could be a live run
|
|
110
|
+
await rm(full, { recursive: true, force: true });
|
|
111
|
+
removed.push(full);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// stat/rm race or permission issue — skip this entry, keep sweeping.
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return removed;
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=tmp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tmp.js","sourceRoot":"","sources":["../../src/util/tmp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B;;gEAEgE;AAChE,MAAM,CAAC,MAAM,oBAAoB,GAAG,GAAG,CAAC;AAExC;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,QAAgB,EAAE,UAAU,GAAG,oBAAoB;IACvF,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IAC7C,MAAM,KAAK,GAA6C,EAAE,CAAC;IAC3D,MAAM,IAAI,GAAG,KAAK,EAAE,GAAW,EAAiB,EAAE;QAChD,IAAI,OAA2E,CAAC;QAChF,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QACxD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,CAAC,2BAA2B;QACrC,CAAC;QACD,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACxB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;YACpC,IAAI,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;gBACpB,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC;YACnB,CAAC;iBAAM,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;gBACtB,IAAI,CAAC;oBACH,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC;oBAC5B,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC;gBAC5C,CAAC;gBAAC,MAAM,CAAC;oBACP,6BAA6B;gBAC/B,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC,CAAC;IACF,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC;IACjB,IAAI,KAAK,CAAC,MAAM,IAAI,UAAU;QAAE,OAAO,CAAC,CAAC;IACzC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe;IAC5D,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC;QACxC,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YAClC,OAAO,EAAE,CAAC;QACZ,CAAC;QAAC,MAAM,CAAC;YACP,oDAAoD;QACtD,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,aAAa,CAAC;AAEzC;;;;;;gEAMgE;AAChE,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAEnD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,GAAW,EAAE,QAAQ,GAAG,gBAAgB;IAC/E,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC;IACtB,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,IAAI,OAAiB,CAAC;IACtB,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,OAAO,CAAC,CAAC,gDAAgD;IAClE,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;QAC3B,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC;YAAE,SAAS;QAC5C,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACnC,IAAI,CAAC;YACH,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC;YAC5B,IAAI,CAAC,EAAE,CAAC,WAAW,EAAE;gBAAE,SAAS;YAChC,IAAI,GAAG,GAAG,EAAE,CAAC,OAAO,GAAG,QAAQ;gBAAE,SAAS,CAAC,iCAAiC;YAC5E,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YACjD,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrB,CAAC;QAAC,MAAM,CAAC;YACP,qEAAqE;QACvE,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "copperhead",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Cursor for circuit boards: an AI agent that designs, documents, and validates real PCBs on KiCad repositories",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Animesh Chouhan <animeshchouhan@outlook.com>",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"prepublishOnly": "npm run typecheck && npm run build",
|
|
49
49
|
"dev": "tsx src/cli.ts",
|
|
50
50
|
"demo:simple": "bash scripts/demo-simple.sh",
|
|
51
|
+
"demo:ui": "tsx scripts/ui-demo.ts",
|
|
51
52
|
"lint:md": "markdownlint-cli2 \"**/*.md\" \"#node_modules/**\" \"#**/node_modules/**\" \"#dist/**\" \"#**/dist/**\" \"#docs/.astro/**\" \"#demo-runs/**\" \"#.copperhead/runs/**\" \"#_site/**\"",
|
|
52
53
|
"test": "vitest run",
|
|
53
54
|
"test:watch": "vitest",
|
|
@@ -72,7 +73,7 @@
|
|
|
72
73
|
"devDependencies": {
|
|
73
74
|
"@openai/codex-sdk": "^0.144.6",
|
|
74
75
|
"@types/node": "^22.13.10",
|
|
75
|
-
"markdownlint-cli2": "0.
|
|
76
|
+
"markdownlint-cli2": "0.23.2",
|
|
76
77
|
"tsx": "^4.19.3",
|
|
77
78
|
"typescript": "^5.8.2",
|
|
78
79
|
"vitest": "^3.0.8"
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subtle TTY motion for attended copperhead chrome. Disabled when color is
|
|
3
|
+
* off, stdout is not a TTY, CI=1, or COPPERHEAD_NO_ANIM=1.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { copper, isColorEnabled } from './theme.js';
|
|
7
|
+
|
|
8
|
+
export function prefersAnimation(): boolean {
|
|
9
|
+
return (
|
|
10
|
+
isColorEnabled() &&
|
|
11
|
+
Boolean(process.stdout.isTTY) &&
|
|
12
|
+
!process.env.CI &&
|
|
13
|
+
!process.env.COPPERHEAD_NO_ANIM
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function sleep(ms: number): Promise<void> {
|
|
18
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const HIDE = '\x1b[?25l';
|
|
22
|
+
const SHOW = '\x1b[?25h';
|
|
23
|
+
|
|
24
|
+
/** Growing fiducial frames (3 rows), converging on the block mark. Exported for tests. */
|
|
25
|
+
export function fiducialBootFrames(): string[][] {
|
|
26
|
+
return [
|
|
27
|
+
[' ', ' ██ ', ' '],
|
|
28
|
+
[' ▗▄▄▖ ', ' █ █ ', ' ▝▀▀▘ '],
|
|
29
|
+
[' ▄▟▙▄ ', ' ██ ██ ', ' ▀▜▛▀ '],
|
|
30
|
+
[' ▄▟▙▄ ', ' ███ ███', ' ▀▜▛▀ '],
|
|
31
|
+
].map((frame) => frame.map((row) => copper(row)));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Pulse the fiducial mark in place over the already-rendered banner (rows
|
|
36
|
+
* `topRow..topRow+2`, column 1). The full screen loads first; this animates
|
|
37
|
+
* after, two grow cycles ending on the final mark. Frames are constant
|
|
38
|
+
* width so they overwrite each other without clearing the banner text
|
|
39
|
+
* beside them. Deliberately avoids DECSC/DECRC: the dock owns that slot for
|
|
40
|
+
* its caret-parking protocol; the caller repaints the dock afterwards to
|
|
41
|
+
* restore the caret.
|
|
42
|
+
*/
|
|
43
|
+
export async function animateMarkAt(
|
|
44
|
+
out: NodeJS.WriteStream,
|
|
45
|
+
topRow: number,
|
|
46
|
+
opts?: { slow?: boolean },
|
|
47
|
+
): Promise<void> {
|
|
48
|
+
if (!prefersAnimation()) return;
|
|
49
|
+
const frames = fiducialBootFrames();
|
|
50
|
+
const frameMs = opts?.slow ? 110 : 45;
|
|
51
|
+
const holdMs = opts?.slow ? 220 : 90;
|
|
52
|
+
try {
|
|
53
|
+
for (let cycle = 0; cycle < 2; cycle++) {
|
|
54
|
+
for (const frame of frames) {
|
|
55
|
+
let seq = HIDE;
|
|
56
|
+
frame.forEach((row, i) => {
|
|
57
|
+
seq += `\x1b[${topRow + i};1H${row}`;
|
|
58
|
+
});
|
|
59
|
+
out.write(seq);
|
|
60
|
+
await sleep(frameMs);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
await sleep(holdMs);
|
|
64
|
+
} finally {
|
|
65
|
+
// Never leave the terminal with a hidden cursor, even if a write or
|
|
66
|
+
// sleep throws mid-animation.
|
|
67
|
+
out.write(SHOW);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Copper horizontal rule (PCB-trace vibe). */
|
|
72
|
+
export function traceRule(width = 32): string {
|
|
73
|
+
return copper(' ' + '─'.repeat(Math.max(8, Math.min(width, 48))));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Cascade lines (help / demo sections). Instant when animation is off. */
|
|
77
|
+
export async function staggerWrite(
|
|
78
|
+
lines: string[],
|
|
79
|
+
write: (line: string) => void,
|
|
80
|
+
delayMs = 12,
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
if (!prefersAnimation()) {
|
|
83
|
+
for (const line of lines) write(line);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
for (const line of lines) {
|
|
87
|
+
write(line);
|
|
88
|
+
await sleep(delayMs);
|
|
89
|
+
}
|
|
90
|
+
}
|
package/src/agent/box.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Box-drawing primitives for the bottom-docked REPL chrome: input box,
|
|
3
|
+
* status bar, and notice callouts. All width math ignores SGR sequences so
|
|
4
|
+
* colored segments never break padding or wrap accounting.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { copper, err, isColorEnabled, ruleDim, warn } from './theme.js';
|
|
8
|
+
|
|
9
|
+
const SGR = /\x1b\[[0-9;]*m/g;
|
|
10
|
+
|
|
11
|
+
/** Visible width ignoring SGR color codes. */
|
|
12
|
+
export function visibleWidth(s: string): number {
|
|
13
|
+
return s.replace(SGR, '').length;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Inverse video (synthetic caret / hover); no-op when color is off. */
|
|
17
|
+
export function inverse(s: string): string {
|
|
18
|
+
if (!isColorEnabled() || s === '') return s;
|
|
19
|
+
return `\x1b[7m${s}\x1b[0m`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A run of plain text with an optional style applied after slicing. */
|
|
23
|
+
export interface Span {
|
|
24
|
+
text: string;
|
|
25
|
+
paint?: (s: string) => string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Wrap styled spans into lines no wider than `width`. Styling is applied per
|
|
30
|
+
* slice, so a span can be cut at a wrap boundary without leaking SGR state.
|
|
31
|
+
*/
|
|
32
|
+
export function wrapSpans(spans: Span[], width: number): string[] {
|
|
33
|
+
const w = Math.max(1, width);
|
|
34
|
+
const lines: string[] = [];
|
|
35
|
+
let cur = '';
|
|
36
|
+
let curLen = 0;
|
|
37
|
+
for (const span of spans) {
|
|
38
|
+
let text = span.text;
|
|
39
|
+
while (text.length) {
|
|
40
|
+
const take = text.slice(0, w - curLen);
|
|
41
|
+
cur += span.paint ? span.paint(take) : take;
|
|
42
|
+
curLen += take.length;
|
|
43
|
+
text = text.slice(take.length);
|
|
44
|
+
if (curLen >= w) {
|
|
45
|
+
lines.push(cur);
|
|
46
|
+
cur = '';
|
|
47
|
+
curLen = 0;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (cur !== '' || !lines.length) lines.push(cur);
|
|
52
|
+
return lines;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Full-width horizontal rule (the input-area separators), #888888. */
|
|
56
|
+
export function rule(width: number): string {
|
|
57
|
+
return ruleDim('─'.repeat(Math.max(1, width)));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Truncate to a visible width, keeping SGR sequences intact and closed. */
|
|
61
|
+
export function truncateVisible(s: string, width: number): string {
|
|
62
|
+
if (visibleWidth(s) <= width) return s;
|
|
63
|
+
let out = '';
|
|
64
|
+
let vis = 0;
|
|
65
|
+
let i = 0;
|
|
66
|
+
let hadSgr = false;
|
|
67
|
+
while (i < s.length && vis < width) {
|
|
68
|
+
if (s[i] === '\x1b') {
|
|
69
|
+
const m = /^\x1b\[[0-9;]*m/.exec(s.slice(i));
|
|
70
|
+
if (m) {
|
|
71
|
+
out += m[0];
|
|
72
|
+
hadSgr = true;
|
|
73
|
+
i += m[0].length;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
out += s[i];
|
|
78
|
+
vis++;
|
|
79
|
+
i++;
|
|
80
|
+
}
|
|
81
|
+
return out + (hadSgr ? '\x1b[0m' : '');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** One status line with a left- and a right-justified half. */
|
|
85
|
+
export function statusBar(left: string, right: string, width: number): string {
|
|
86
|
+
const w = Math.max(8, width);
|
|
87
|
+
const l = visibleWidth(left);
|
|
88
|
+
const r = visibleWidth(right);
|
|
89
|
+
// Too narrow for both: the hints matter more than the meta.
|
|
90
|
+
if (l + r + 2 > w) return truncateVisible(left, w);
|
|
91
|
+
return left + ' '.repeat(w - l - r) + right;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Notice block: thin colored left bar + title + dim body (banner callouts). */
|
|
95
|
+
export function callout(kind: 'info' | 'warn' | 'err', title: string, body: string[]): string[] {
|
|
96
|
+
const paintBar = kind === 'err' ? err : kind === 'warn' ? warn : copper;
|
|
97
|
+
const bar = paintBar(' ▎');
|
|
98
|
+
return [`${bar} ${paintBar(title)}`, ...body.map((b) => `${bar} ${b}`)];
|
|
99
|
+
}
|