@yanzc/claude-pro 0.4.6 → 0.5.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 +38 -4
- package/package.json +8 -2
- package/src/agent.js +38 -1
- package/src/api.js +12 -0
- package/src/checkpoints.js +20 -1
- package/src/commands.js +169 -35
- package/src/config.js +6 -0
- package/src/main.js +2 -0
- package/src/peers.js +40 -7
- package/src/prompts.js +17 -0
- package/src/qr.js +206 -0
- package/src/remote.js +540 -0
- package/src/session.js +4 -1
- package/src/supervisor.js +2 -1
- package/src/tools/files.js +13 -4
- package/src/tools/peers.js +26 -19
- package/src/transfer.js +260 -0
- package/src/ui/dialogs.js +3 -2
- package/src/ui/editor.js +72 -41
- package/src/ui/repl.js +675 -70
- package/src/ui/screen.js +259 -5
- package/src/utils/downloads.js +83 -0
- package/src/utils/images.js +175 -0
package/src/peers.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
// Peers: the other interactive claude-pro sessions on this machine. Each session registers itself in
|
|
2
2
|
// CONFIG_DIR/peers/<id>.json (name, directory, pid, and a port and token of a small HTTP server on 127.0.0.1)
|
|
3
|
-
// so that sessions can send each other tasks: the user with
|
|
3
|
+
// so that sessions can send each other tasks: the user with `&name task` in the input box, the model with the
|
|
4
4
|
// MessagePeer tool. A task is answered automatically with the final text of the turn that handled it.
|
|
5
|
+
// Sessions on the user's other computers are reached through the gateway instead (see remote.js): they are
|
|
6
|
+
// listed as {id, name, machine, cwd, remote: true, label: 'machine/name'} and addressed by that label.
|
|
5
7
|
|
|
6
8
|
import fs from 'node:fs';
|
|
9
|
+
import os from 'node:os';
|
|
7
10
|
import path from 'node:path';
|
|
8
11
|
import http from 'node:http';
|
|
9
12
|
import crypto from 'node:crypto';
|
|
@@ -20,7 +23,7 @@ function alive(pid) {
|
|
|
20
23
|
try { process.kill(pid, 0); return true; } catch (e) { return e.code === 'EPERM'; }
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
export const cleanName = (s) => String(s || '').replace(/[^\
|
|
26
|
+
export const cleanName = (s) => String(s || '').replace(/[^\p{L}\p{M}\p{N}_.-]+/gu, '-').replace(/^[-.]+|[-.]+$/g, '').slice(0, 40) || 'agent';
|
|
24
27
|
|
|
25
28
|
// The sessions that are running now (stale entries of sessions that died are removed).
|
|
26
29
|
export function listPeers({ excludeId } = {}) {
|
|
@@ -37,8 +40,34 @@ export function listPeers({ excludeId } = {}) {
|
|
|
37
40
|
return peers.sort((a, b) => a.startedAt - b.startedAt);
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
// The name of this computer for sessions on the other computers: CLAUDE_PRO_MACHINE_NAME (公司, home...), else
|
|
44
|
+
// the host name. Letters of any script, digits, _ . and -.
|
|
45
|
+
export function machineName(env = process.env) {
|
|
46
|
+
const raw = env.CLAUDE_PRO_MACHINE_NAME || os.hostname() || 'computer';
|
|
47
|
+
return String(raw).replace(/[^\p{L}\p{M}\p{N}_.-]+/gu, '-').replace(/^[-.]+|[-.]+$/g, '').slice(0, 40) || 'computer';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Find the session `ref` (with or without @) names among the local ones and those on other computers: a local
|
|
51
|
+
// name (or id prefix); else machine/name, a name only one remote session has, a computer with only one session
|
|
52
|
+
// (`&公司`), or an id prefix. `ids: false` leaves the id prefixes out (for what the user types).
|
|
53
|
+
// Returns {peer}, {error, ambiguous: true} when several match, or {error: null} when none does.
|
|
54
|
+
export function resolvePeer(local, remote, ref, { ids = true } = {}) {
|
|
55
|
+
const q = String(ref || '').replace(/^[@&&]/, '').toLowerCase();
|
|
56
|
+
if (!q) return { error: null };
|
|
57
|
+
const near = ids ? findPeer(local, q) : local.find((p) => p.name.toLowerCase() === q);
|
|
58
|
+
if (near) return { peer: near };
|
|
59
|
+
const same = (v) => String(v || '').toLowerCase() === q;
|
|
60
|
+
let hits = remote.filter((p) => same(p.label));
|
|
61
|
+
if (!hits.length) hits = remote.filter((p) => same(p.name));
|
|
62
|
+
if (!hits.length) hits = remote.filter((p) => same(p.machine));
|
|
63
|
+
if (!hits.length && ids && q.length >= 4) hits = remote.filter((p) => p.id.toLowerCase().startsWith(q));
|
|
64
|
+
if (hits.length === 1) return { peer: hits[0] };
|
|
65
|
+
if (hits.length > 1) return { error: `"&${String(ref).replace(/^[@&&]/, '')}" could be ${hits.map((p) => '&' + p.label).join(', ')}: say which.`, ambiguous: true };
|
|
66
|
+
return { error: null };
|
|
67
|
+
}
|
|
68
|
+
|
|
40
69
|
export function findPeer(peers, ref) {
|
|
41
|
-
const q = String(ref || '').replace(
|
|
70
|
+
const q = String(ref || '').replace(/^[@&&]/, '').toLowerCase();
|
|
42
71
|
if (!q) return null;
|
|
43
72
|
return peers.find((p) => p.name.toLowerCase() === q) || peers.find((p) => p.id.startsWith(q)) || null;
|
|
44
73
|
}
|
|
@@ -116,15 +145,18 @@ export class PeerNode {
|
|
|
116
145
|
if (typeof msg?.text !== 'string' || !msg.from?.name) return reply(400, { error: 'text and from are required' });
|
|
117
146
|
const id = crypto.randomBytes(4).toString('hex');
|
|
118
147
|
reply(200, { ok: true, id, name: this.name });
|
|
119
|
-
|
|
148
|
+
// files on this computer: [{path, name, size}] (the receiver copies them where it keeps what it is sent)
|
|
149
|
+
const files = (Array.isArray(msg.files) ? msg.files : []).filter((f) => f && typeof f.path === 'string' && path.isAbsolute(f.path))
|
|
150
|
+
.map((f) => ({ path: f.path, name: String(f.name || path.basename(f.path)), size: Number(f.size) || 0 }));
|
|
151
|
+
try { this.onMessage?.({ id, kind: msg.kind === 'reply' ? 'reply' : 'task', text: msg.text, inReplyTo: msg.inReplyTo || null, from: msg.from, ...(files.length ? { files } : {}) }); } catch { /* the receiver's problem */ }
|
|
120
152
|
});
|
|
121
153
|
}
|
|
122
154
|
|
|
123
155
|
// Send to a peer (an entry of peers(), or its name). Resolves with {id} (the id the receiver gave the message).
|
|
124
|
-
async send(target, { text, kind = 'task', inReplyTo = null }) {
|
|
156
|
+
async send(target, { text, kind = 'task', inReplyTo = null, files = [] }) {
|
|
125
157
|
const peer = typeof target === 'string' ? findPeer(this.peers(), target) : target;
|
|
126
158
|
if (!peer) throw new Error(`No running session called "${target}". /peers lists them.`);
|
|
127
|
-
const body = JSON.stringify({ kind, text, inReplyTo, from: { id: this.id, name: this.name, cwd: this.cwd } });
|
|
159
|
+
const body = JSON.stringify({ kind, text, inReplyTo, from: { id: this.id, name: this.name, cwd: this.cwd }, ...(files.length ? { files } : {}) });
|
|
128
160
|
return await new Promise((resolve, reject) => {
|
|
129
161
|
const req = http.request({
|
|
130
162
|
host: '127.0.0.1', port: peer.port, path: '/message', method: 'POST', timeout: 10_000,
|
|
@@ -149,7 +181,8 @@ export class PeerNode {
|
|
|
149
181
|
|
|
150
182
|
// What the model is given for a message from another session.
|
|
151
183
|
export function peerMessagePrompt(msg) {
|
|
152
|
-
const
|
|
184
|
+
const where = msg.from.machine ? ` on the computer "${msg.from.machine}"` : '';
|
|
185
|
+
const from = `${msg.from.name}${msg.from.cwd ? ` (working in ${msg.from.cwd}${where})` : where}`;
|
|
153
186
|
if (msg.kind === 'reply') {
|
|
154
187
|
return `<peer-message from="${msg.from.name}" kind="reply">\n${msg.text}\n</peer-message>\n<system-reminder>This is the answer of another ${'claude-pro'} session, ${from}, to a message you sent it with MessagePeer. Carry on with your task using it.</system-reminder>`;
|
|
155
188
|
}
|
package/src/prompts.js
CHANGED
|
@@ -151,6 +151,23 @@ Write the summary in the same language the user has been using.
|
|
|
151
151
|
Do NOT call any tools. Respond with text only.${instructions ? `\n\nAdditional instructions from the user for this summary:\n${instructions}` : ''}`;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
// The instruction for /rewind's "Summarize from here" (part 'after') and "Summarize up to here" ('before'): only
|
|
155
|
+
// that part of the conversation is summarized, the rest stays in it word for word.
|
|
156
|
+
export function partialCompactPrompt(part, label, instructions = '') {
|
|
157
|
+
const at = label ? `the user's message that begins "${String(label).slice(0, 200)}"` : 'the point the user chose';
|
|
158
|
+
const scope = part === 'before'
|
|
159
|
+
? `Summarize ONLY the part of the conversation BEFORE ${at}. That message and everything after it stay in the conversation word for word: do not summarize them. The summary replaces only what comes before that message, so it has to carry everything from that earlier part that the rest of the conversation builds on.`
|
|
160
|
+
: `Summarize ONLY the part of the conversation that starts with ${at} (that message included) and goes on to the end. Everything before that message stays in the conversation word for word: do not summarize it. The summary replaces only that later part.`;
|
|
161
|
+
return compactPrompt(`${scope}${instructions ? `\n${instructions}` : ''}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// The message that stands in the conversation for the part a partial summary replaced.
|
|
165
|
+
export function partialSummaryMessage(part, summary) {
|
|
166
|
+
return part === 'before'
|
|
167
|
+
? `[Summarized conversation] The earlier part of this conversation was summarized at the user's request; the messages after this one are as they were. The summary of the earlier part:\n${summary}`
|
|
168
|
+
: `[Summarized conversation] The later part of this conversation, from the user's message where this summary stands to where it was, was summarized at the user's request. The summary of that part:\n${summary}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
154
171
|
export function continuationMessage(summary, { auto }) {
|
|
155
172
|
return `This session is being continued from a previous conversation that ran out of context. The conversation is summarized below:
|
|
156
173
|
${summary}${auto ? '\n\nPlease continue the conversation from where we left it off without asking the user any further questions. Continue with the last task that you were asked to work on.' : ''}`;
|
package/src/qr.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// QR codes in the terminal: /remote-control prints one, so that a phone opens the session by pointing its camera
|
|
2
|
+
// at the screen. Byte mode, error correction level L, versions 1-6 (up to 134 bytes, which a URL with a token
|
|
3
|
+
// fits in easily); the mask is chosen with the standard penalty rules.
|
|
4
|
+
|
|
5
|
+
// [EC codewords per block, blocks, data codewords per block] at level L, by version.
|
|
6
|
+
const EC_L = [null, [7, 1, 19], [10, 1, 34], [15, 1, 55], [20, 1, 80], [26, 1, 108], [18, 2, 68]];
|
|
7
|
+
const MAX_VERSION = 6;
|
|
8
|
+
|
|
9
|
+
// GF(256) with the polynomial 0x11d.
|
|
10
|
+
const EXP = new Uint8Array(512);
|
|
11
|
+
const LOG = new Uint8Array(256);
|
|
12
|
+
for (let i = 0, x = 1; i < 255; i++) {
|
|
13
|
+
EXP[i] = x;
|
|
14
|
+
LOG[x] = i;
|
|
15
|
+
x <<= 1;
|
|
16
|
+
if (x & 0x100) x ^= 0x11d;
|
|
17
|
+
}
|
|
18
|
+
for (let i = 255; i < 512; i++) EXP[i] = EXP[i - 255];
|
|
19
|
+
const mul = (a, b) => (a && b ? EXP[LOG[a] + LOG[b]] : 0);
|
|
20
|
+
|
|
21
|
+
function rsRemainder(data, n) {
|
|
22
|
+
let gen = [1]; // (x - a^0)(x - a^1)...(x - a^(n-1)), highest degree first
|
|
23
|
+
for (let i = 0; i < n; i++) {
|
|
24
|
+
const next = new Array(gen.length + 1).fill(0);
|
|
25
|
+
for (let j = 0; j < gen.length; j++) { next[j] ^= gen[j]; next[j + 1] ^= mul(gen[j], EXP[i]); }
|
|
26
|
+
gen = next;
|
|
27
|
+
}
|
|
28
|
+
const rem = new Array(n).fill(0);
|
|
29
|
+
for (const byte of data) {
|
|
30
|
+
const factor = byte ^ rem.shift();
|
|
31
|
+
rem.push(0);
|
|
32
|
+
for (let i = 0; i < n; i++) rem[i] ^= mul(gen[i + 1], factor);
|
|
33
|
+
}
|
|
34
|
+
return rem;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function codewords(bytes, version) {
|
|
38
|
+
const [ecLen, blocks, dataLen] = EC_L[version];
|
|
39
|
+
const capacity = blocks * dataLen;
|
|
40
|
+
const bits = [];
|
|
41
|
+
const put = (value, len) => { for (let i = len - 1; i >= 0; i--) bits.push((value >>> i) & 1); };
|
|
42
|
+
put(0b0100, 4);
|
|
43
|
+
put(bytes.length, 8);
|
|
44
|
+
for (const b of bytes) put(b, 8);
|
|
45
|
+
put(0, Math.min(4, capacity * 8 - bits.length));
|
|
46
|
+
while (bits.length % 8) bits.push(0);
|
|
47
|
+
const data = [];
|
|
48
|
+
for (let i = 0; i < bits.length; i += 8) data.push(bits.slice(i, i + 8).reduce((v, b) => (v << 1) | b, 0));
|
|
49
|
+
for (let pad = 0xec; data.length < capacity; pad ^= 0xec ^ 0x11) data.push(pad);
|
|
50
|
+
const dataBlocks = [];
|
|
51
|
+
const ecBlocks = [];
|
|
52
|
+
for (let b = 0; b < blocks; b++) {
|
|
53
|
+
const d = data.slice(b * dataLen, (b + 1) * dataLen);
|
|
54
|
+
dataBlocks.push(d);
|
|
55
|
+
ecBlocks.push(rsRemainder(d, ecLen));
|
|
56
|
+
}
|
|
57
|
+
const out = [];
|
|
58
|
+
for (let i = 0; i < dataLen; i++) for (const d of dataBlocks) out.push(d[i]);
|
|
59
|
+
for (let i = 0; i < ecLen; i++) for (const e of ecBlocks) out.push(e[i]);
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const MASKS = [
|
|
64
|
+
(x, y) => (x + y) % 2 === 0,
|
|
65
|
+
(x, y) => y % 2 === 0,
|
|
66
|
+
(x) => x % 3 === 0,
|
|
67
|
+
(x, y) => (x + y) % 3 === 0,
|
|
68
|
+
(x, y) => (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0,
|
|
69
|
+
(x, y) => ((x * y) % 2) + ((x * y) % 3) === 0,
|
|
70
|
+
(x, y) => (((x * y) % 2) + ((x * y) % 3)) % 2 === 0,
|
|
71
|
+
(x, y) => (((x + y) % 2) + ((x * y) % 3)) % 2 === 0,
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
function penalty(m) {
|
|
75
|
+
const n = m.length;
|
|
76
|
+
let score = 0;
|
|
77
|
+
let dark = 0;
|
|
78
|
+
const lines = [];
|
|
79
|
+
for (let i = 0; i < n; i++) {
|
|
80
|
+
lines.push(m[i]);
|
|
81
|
+
lines.push(m.map((row) => row[i]));
|
|
82
|
+
}
|
|
83
|
+
for (const line of lines) {
|
|
84
|
+
for (let i = 0; i < n;) {
|
|
85
|
+
let j = i;
|
|
86
|
+
while (j < n && line[j] === line[i]) j++;
|
|
87
|
+
if (j - i >= 5) score += 3 + (j - i - 5);
|
|
88
|
+
i = j;
|
|
89
|
+
}
|
|
90
|
+
const s = line.map((v) => (v ? 1 : 0)).join('');
|
|
91
|
+
for (const p of ['10111010000', '00001011101']) for (let k = s.indexOf(p); k >= 0; k = s.indexOf(p, k + 1)) score += 40;
|
|
92
|
+
}
|
|
93
|
+
for (let y = 0; y < n; y++) {
|
|
94
|
+
for (let x = 0; x < n; x++) {
|
|
95
|
+
if (m[y][x]) dark++;
|
|
96
|
+
if (x < n - 1 && y < n - 1 && m[y][x] === m[y][x + 1] && m[y][x] === m[y + 1][x] && m[y][x] === m[y + 1][x + 1]) score += 3;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return score + 10 * Math.floor(Math.abs((dark * 100) / (n * n) - 50) / 5);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// The modules of the QR code for `text` (rows of booleans, true = dark), or null when it is too long.
|
|
103
|
+
// `mask`: use this mask (0-7) instead of the best one.
|
|
104
|
+
export function qrMatrix(text, { mask: forcedMask } = {}) {
|
|
105
|
+
const bytes = [...Buffer.from(String(text), 'utf8')];
|
|
106
|
+
let version = 1;
|
|
107
|
+
while (version <= MAX_VERSION && EC_L[version][1] * EC_L[version][2] * 8 < 12 + 8 * bytes.length) version++;
|
|
108
|
+
if (version > MAX_VERSION) return null;
|
|
109
|
+
const size = 17 + 4 * version;
|
|
110
|
+
const base = Array.from({ length: size }, () => new Array(size).fill(false));
|
|
111
|
+
const fn = Array.from({ length: size }, () => new Array(size).fill(false));
|
|
112
|
+
const set = (m, x, y, dark) => { m[y][x] = dark; fn[y][x] = true; };
|
|
113
|
+
|
|
114
|
+
for (const [cx, cy] of [[3, 3], [size - 4, 3], [3, size - 4]]) {
|
|
115
|
+
for (let dy = -4; dy <= 4; dy++) {
|
|
116
|
+
for (let dx = -4; dx <= 4; dx++) {
|
|
117
|
+
const x = cx + dx;
|
|
118
|
+
const y = cy + dy;
|
|
119
|
+
if (x < 0 || y < 0 || x >= size || y >= size) continue;
|
|
120
|
+
const d = Math.max(Math.abs(dx), Math.abs(dy));
|
|
121
|
+
set(base, x, y, d !== 2 && d !== 4);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
for (let i = 0; i < size; i++) {
|
|
126
|
+
if (!fn[6][i]) set(base, i, 6, i % 2 === 0);
|
|
127
|
+
if (!fn[i][6]) set(base, 6, i, i % 2 === 0);
|
|
128
|
+
}
|
|
129
|
+
if (version >= 2) {
|
|
130
|
+
const c = size - 7;
|
|
131
|
+
for (let dy = -2; dy <= 2; dy++) for (let dx = -2; dx <= 2; dx++) set(base, c + dx, c + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);
|
|
132
|
+
}
|
|
133
|
+
const drawFormat = (m, mask) => {
|
|
134
|
+
const data = (1 << 3) | mask; // level L = 01
|
|
135
|
+
let rem = data;
|
|
136
|
+
for (let i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >>> 9) * 0x537);
|
|
137
|
+
const bits = ((data << 10) | rem) ^ 0x5412;
|
|
138
|
+
const bit = (i) => ((bits >>> i) & 1) === 1;
|
|
139
|
+
for (let i = 0; i <= 5; i++) set(m, 8, i, bit(i));
|
|
140
|
+
set(m, 8, 7, bit(6));
|
|
141
|
+
set(m, 8, 8, bit(7));
|
|
142
|
+
set(m, 7, 8, bit(8));
|
|
143
|
+
for (let i = 9; i < 15; i++) set(m, 14 - i, 8, bit(i));
|
|
144
|
+
for (let i = 0; i < 8; i++) set(m, size - 1 - i, 8, bit(i));
|
|
145
|
+
for (let i = 8; i < 15; i++) set(m, 8, size - 15 + i, bit(i));
|
|
146
|
+
set(m, 8, size - 8, true);
|
|
147
|
+
};
|
|
148
|
+
drawFormat(base, 0); // reserves the format areas before the data goes in
|
|
149
|
+
|
|
150
|
+
const data = codewords(bytes, version);
|
|
151
|
+
let bitIndex = 0;
|
|
152
|
+
for (let right = size - 1; right >= 1; right -= 2) {
|
|
153
|
+
if (right === 6) right = 5;
|
|
154
|
+
for (let vert = 0; vert < size; vert++) {
|
|
155
|
+
for (let j = 0; j < 2; j++) {
|
|
156
|
+
const x = right - j;
|
|
157
|
+
const upward = ((right + 1) & 2) === 0;
|
|
158
|
+
const y = upward ? size - 1 - vert : vert;
|
|
159
|
+
if (fn[y][x]) continue;
|
|
160
|
+
if (bitIndex < data.length * 8) base[y][x] = ((data[bitIndex >>> 3] >>> (7 - (bitIndex & 7))) & 1) === 1;
|
|
161
|
+
bitIndex++;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let best = null;
|
|
167
|
+
for (let mask = 0; mask < 8; mask++) {
|
|
168
|
+
if (forcedMask !== undefined && mask !== forcedMask) continue;
|
|
169
|
+
const m = base.map((row, y) => row.map((v, x) => (fn[y][x] ? v : v !== MASKS[mask](x, y))));
|
|
170
|
+
drawFormat(m, mask);
|
|
171
|
+
const score = forcedMask !== undefined ? 0 : penalty(m);
|
|
172
|
+
if (!best || score < best.score) best = { m, score };
|
|
173
|
+
}
|
|
174
|
+
return best.m;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// The QR code as terminal lines, two rows of modules per line. With colors it is drawn black on white whatever the
|
|
178
|
+
// terminal's theme; without, it assumes a dark background (light modules are the drawn ones). Null when too long.
|
|
179
|
+
export function qrLines(text, { color = true, quiet = 2 } = {}) {
|
|
180
|
+
const m = qrMatrix(text);
|
|
181
|
+
if (!m) return null;
|
|
182
|
+
const n = m.length + quiet * 2;
|
|
183
|
+
const dark = (x, y) => {
|
|
184
|
+
const mx = x - quiet;
|
|
185
|
+
const my = y - quiet;
|
|
186
|
+
return mx >= 0 && my >= 0 && mx < m.length && my < m.length && m[my][mx];
|
|
187
|
+
};
|
|
188
|
+
const lines = [];
|
|
189
|
+
for (let y = 0; y < n; y += 2) {
|
|
190
|
+
let line = '';
|
|
191
|
+
let last = null;
|
|
192
|
+
for (let x = 0; x < n; x++) {
|
|
193
|
+
const top = dark(x, y);
|
|
194
|
+
const bottom = y + 1 < n && dark(x, y + 1);
|
|
195
|
+
if (color) {
|
|
196
|
+
const sgr = `\x1b[${top ? 30 : 97};${bottom ? 40 : 107}m`;
|
|
197
|
+
if (sgr !== last) { line += sgr; last = sgr; }
|
|
198
|
+
line += '▀';
|
|
199
|
+
} else {
|
|
200
|
+
line += !top && !bottom ? '█' : !top ? '▀' : !bottom ? '▄' : ' ';
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
lines.push(color ? `${line}\x1b[0m` : line);
|
|
204
|
+
}
|
|
205
|
+
return lines;
|
|
206
|
+
}
|