coherence-cli 0.8.3 → 0.9.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.
@@ -4,30 +4,47 @@
4
4
 
5
5
  import { get, post } from "../api.mjs";
6
6
 
7
+ /** Truncate at word boundary, append "..." if needed */
7
8
  function truncate(str, len) {
8
9
  if (!str) return "";
9
- return str.length > len ? str.slice(0, len - 1) + "\u2026" : str;
10
+ if (str.length <= len) return str;
11
+ const trimmed = str.slice(0, len - 3);
12
+ const lastSpace = trimmed.lastIndexOf(" ");
13
+ return (lastSpace > len * 0.4 ? trimmed.slice(0, lastSpace) : trimmed) + "...";
10
14
  }
11
15
 
12
- export async function showNewsFeed() {
16
+ /** Format relative time from ISO string */
17
+ function timeAgo(isoStr) {
18
+ if (!isoStr) return "";
19
+ const min = Math.floor((Date.now() - new Date(isoStr).getTime()) / 60000);
20
+ if (min < 1) return "just now";
21
+ if (min < 60) return `${min}m ago`;
22
+ if (min < 1440) return `${Math.floor(min / 60)}h ago`;
23
+ return `${Math.floor(min / 1440)}d ago`;
24
+ }
25
+
26
+ export async function showNewsFeed(args) {
27
+ const limit = (args && parseInt(args[0])) || 10;
13
28
  const data = await get("/api/news/feed");
14
- const items = Array.isArray(data) ? data : data?.items || data?.articles;
15
- if (!items || !Array.isArray(items)) {
29
+ const allItems = Array.isArray(data) ? data : data?.items || data?.articles;
30
+ if (!allItems || !Array.isArray(allItems)) {
16
31
  console.log("Could not fetch news feed.");
17
32
  return;
18
33
  }
34
+ const items = allItems.slice(0, limit);
19
35
  if (items.length === 0) {
20
36
  console.log("No news items.");
21
37
  return;
22
38
  }
23
39
 
24
40
  console.log();
25
- console.log(`\x1b[1m NEWS FEED\x1b[0m (${items.length})`);
26
- console.log(` ${"─".repeat(60)}`);
41
+ console.log(`\x1b[1m NEWS FEED\x1b[0m (showing ${items.length} of ${allItems.length})`);
42
+ console.log(` ${"─".repeat(74)}`);
27
43
  for (const item of items) {
28
- const title = truncate(item.title || item.headline || "?", 55);
29
- const source = item.source ? `\x1b[2m${truncate(item.source, 15)}\x1b[0m` : "";
30
- console.log(` ${title} ${source}`);
44
+ const title = truncate(item.title || item.headline || "?", 50).padEnd(52);
45
+ const source = item.source ? truncate(item.source, 12).padStart(12) : "";
46
+ const ago = timeAgo(item.published_at || item.created_at || item.timestamp);
47
+ console.log(` ${title} \x1b[2m${source.padStart(12)}\x1b[0m \x1b[2m${ago}\x1b[0m`);
31
48
  }
32
49
  console.log();
33
50
  }
@@ -1,88 +1,10 @@
1
1
  /**
2
- * Federation node commands: nodes, msg, cmd, broadcast
2
+ * Federation node commands: nodes, msg, broadcast
3
3
  */
4
4
 
5
5
  import { get, post } from "../api.mjs";
6
6
  import { hostname } from "node:os";
7
7
 
8
- /**
9
- * Send a remote command to a node.
10
- * Usage: cc cmd <node_id_or_name> <command> [...args]
11
- * Commands: update, status, diagnose, restart, ping
12
- */
13
- export async function sendCommand(args) {
14
- if (args.length < 2) {
15
- console.log("Usage: cc cmd <node_id_or_name> <command>");
16
- console.log("Commands: update, status, diagnose, restart, ping");
17
- return;
18
- }
19
-
20
- const [target, command, ...cmdArgs] = args;
21
-
22
- // Resolve target: could be node_id prefix or hostname
23
- const nodes = await get("/api/federation/nodes");
24
- const node = nodes?.find(
25
- (n) =>
26
- n.node_id?.startsWith(target) ||
27
- n.hostname?.toLowerCase().includes(target.toLowerCase()),
28
- );
29
- if (!node) {
30
- console.log(`Node not found: ${target}`);
31
- console.log("Available nodes:");
32
- for (const n of nodes || []) {
33
- console.log(` ${n.node_id?.slice(0, 12)} ${n.hostname}`);
34
- }
35
- return;
36
- }
37
-
38
- const myNodeId = nodes?.find(
39
- (n) => n.hostname === hostname(),
40
- )?.node_id || "unknown";
41
-
42
- console.log(
43
- `Sending \x1b[1m${command}\x1b[0m to \x1b[1m${node.hostname}\x1b[0m (${node.node_id?.slice(0, 12)})...`,
44
- );
45
-
46
- const result = await post(`/api/federation/nodes/${myNodeId}/messages`, {
47
- from_node: myNodeId,
48
- to_node: node.node_id,
49
- type: "command",
50
- text: `Remote command: ${command} ${cmdArgs.join(" ")}`.trim(),
51
- payload: { command, args: cmdArgs },
52
- });
53
-
54
- if (result?.id) {
55
- const msgId = result.id;
56
- console.log(`\x1b[32m✓\x1b[0m Command sent (msg ${msgId.slice(0, 12)})`);
57
- console.log(" Waiting for reply (up to 3 min)...");
58
-
59
- // Poll for reply
60
- const deadline = Date.now() + 180_000;
61
- const pollInterval = 10_000;
62
- while (Date.now() < deadline) {
63
- await new Promise((r) => setTimeout(r, pollInterval));
64
- process.stdout.write(".");
65
-
66
- const inbox = await get(
67
- `/api/federation/nodes/${myNodeId}/messages?unread_only=false&limit=20`,
68
- );
69
- const reply = inbox?.messages?.find(
70
- (m) =>
71
- (m.type === "command_response" || m.type === "ack") &&
72
- m.payload?.in_reply_to === msgId,
73
- );
74
- if (reply) {
75
- console.log(`\n\x1b[32m✓\x1b[0m Reply from ${node.hostname}:`);
76
- console.log(` ${reply.text}`);
77
- return;
78
- }
79
- }
80
- console.log("\n\x1b[33m⏱\x1b[0m No reply within 3 min. Check later: cc inbox");
81
- } else {
82
- console.log("\x1b[31m✗\x1b[0m Failed to send command");
83
- }
84
- }
85
-
86
8
  export async function listNodes() {
87
9
  const nodes = await get("/api/federation/nodes");
88
10
  if (!nodes || !Array.isArray(nodes)) {
@@ -94,6 +16,21 @@ export async function listNodes() {
94
16
  console.log("\x1b[1m FEDERATION NODES\x1b[0m");
95
17
  console.log(` ${"─".repeat(50)}`);
96
18
 
19
+ /** Format relative time */
20
+ function relativeTime(min) {
21
+ if (min < 1) return "just now";
22
+ if (min < 60) return `${min}m ago`;
23
+ if (min < 1440) return `${Math.floor(min / 60)}h ago`;
24
+ return `${Math.floor(min / 1440)}d ago`;
25
+ }
26
+
27
+ /** Colored provider badge */
28
+ function providerBadge(name) {
29
+ const colors = { openrouter: "\x1b[36m", ollama: "\x1b[32m", anthropic: "\x1b[33m" };
30
+ const color = colors[name.toLowerCase()] || "\x1b[2m";
31
+ return `${color}[${name}]\x1b[0m`;
32
+ }
33
+
97
34
  const now = Date.now();
98
35
  for (const node of nodes) {
99
36
  const lastSeen = node.last_seen_at ? new Date(node.last_seen_at) : null;
@@ -105,10 +42,6 @@ export async function listNodes() {
105
42
  if (ageMin < 5) dot = "\x1b[32m●\x1b[0m"; // green
106
43
  else if (ageMin < 60) dot = "\x1b[33m●\x1b[0m"; // yellow
107
44
 
108
- // OS icon
109
- const os = node.os_type || "?";
110
- const icon = os === "macos" ? "🍎" : os === "windows" ? "🪟" : os === "linux" ? "🐧" : "🖥️";
111
-
112
45
  // Providers
113
46
  let providers = [];
114
47
  try {
@@ -117,22 +50,17 @@ export async function listNodes() {
117
50
  : (node.providers || []);
118
51
  } catch { providers = []; }
119
52
 
120
- const ago = ageMin < 1 ? "now" : ageMin < 60 ? `${ageMin}m ago` : `${Math.floor(ageMin / 60)}h ago`;
121
-
122
- // Git version from capabilities
123
- const git = node.capabilities?.git || {};
124
- const sha = git.local_sha || "?";
125
- const originSha = git.origin_sha || "?";
126
- const upToDate = git.up_to_date === "yes";
127
- const versionTag = upToDate
128
- ? `\x1b[32m${sha}\x1b[0m`
129
- : `\x1b[31m${sha} (origin: ${originSha})\x1b[0m`;
53
+ const shortId = (node.node_id || "").slice(0, 7);
54
+ const ago = relativeTime(ageMin);
55
+ const hostName = (node.hostname || "?").slice(0, 24);
56
+ const os = node.os_type || "?";
130
57
 
131
- console.log(` ${dot} ${icon} \x1b[1m${node.hostname || "?"}\x1b[0m ${ago} sha:${versionTag}`);
132
- console.log(` ${providers.join(", ")}`);
133
- console.log(` id: ${node.node_id}`);
134
- console.log();
58
+ console.log(` ${dot} \x1b[1m${hostName.padEnd(26)}\x1b[0m ${ago.padEnd(10)} \x1b[2m${shortId}\x1b[0m ${os}`);
59
+ if (providers.length > 0) {
60
+ console.log(` ${providers.map(providerBadge).join(" ")}`);
61
+ }
135
62
  }
63
+ console.log();
136
64
  }
137
65
 
138
66
  export async function sendMessage(args) {
@@ -173,45 +101,15 @@ export async function sendMessage(args) {
173
101
  console.log("\x1b[31m✗\x1b[0m Failed to broadcast");
174
102
  }
175
103
  } else {
176
- // Resolve target name to node_id
177
- const targetNode = Array.isArray(nodes)
178
- ? nodes.find(
179
- (n) =>
180
- n.node_id?.startsWith(targetOrBroadcast) ||
181
- n.hostname?.toLowerCase().includes(targetOrBroadcast.toLowerCase()),
182
- )
183
- : null;
184
- const toNodeId = targetNode?.node_id || targetOrBroadcast;
185
- const toName = targetNode?.hostname || targetOrBroadcast.slice(0, 12);
186
-
187
104
  const result = await post(`/api/federation/nodes/${myNodeId}/messages`, {
188
105
  from_node: myNodeId,
189
- to_node: toNodeId,
106
+ to_node: targetOrBroadcast,
190
107
  type: "text",
191
108
  text,
192
109
  payload: {},
193
110
  });
194
- if (result?.id) {
195
- const msgId = result.id;
196
- console.log(`\x1b[32m✓\x1b[0m Message sent to ${toName}: ${text.slice(0, 60)}`);
197
- console.log(" Waiting for ack (up to 3 min)...");
198
-
199
- const deadline = Date.now() + 180_000;
200
- while (Date.now() < deadline) {
201
- await new Promise((r) => setTimeout(r, 10_000));
202
- process.stdout.write(".");
203
- const inbox = await get(
204
- `/api/federation/nodes/${myNodeId}/messages?unread_only=false&limit=20`,
205
- );
206
- const ack = inbox?.messages?.find(
207
- (m) => m.type === "ack" && m.payload?.in_reply_to === msgId,
208
- );
209
- if (ack) {
210
- console.log(`\n\x1b[32m✓\x1b[0m Acknowledged by ${toName}`);
211
- return;
212
- }
213
- }
214
- console.log("\n\x1b[33m⏱\x1b[0m No ack within 3 min. Node may be offline. Check: cc inbox");
111
+ if (result) {
112
+ console.log(`\x1b[32m✓\x1b[0m Message sent to ${targetOrBroadcast.slice(0, 12)}: ${text.slice(0, 60)}`);
215
113
  } else {
216
114
  console.log("\x1b[31m✗\x1b[0m Failed to send message");
217
115
  }
@@ -4,9 +4,21 @@
4
4
 
5
5
  import { get } from "../api.mjs";
6
6
 
7
+ /** Truncate at word boundary, append "..." if needed */
7
8
  function truncate(str, len) {
8
9
  if (!str) return "";
9
- return str.length > len ? str.slice(0, len - 1) + "\u2026" : str;
10
+ if (str.length <= len) return str;
11
+ const trimmed = str.slice(0, len - 3);
12
+ const lastSpace = trimmed.lastIndexOf(" ");
13
+ return (lastSpace > len * 0.4 ? trimmed.slice(0, lastSpace) : trimmed) + "...";
14
+ }
15
+
16
+ /** Mini bar for success rate */
17
+ function rateBar(rate, width = 10) {
18
+ const pct = Math.max(0, Math.min(1, rate));
19
+ const filled = Math.round(pct * width);
20
+ const color = pct >= 0.9 ? "\x1b[32m" : pct >= 0.7 ? "\x1b[33m" : "\x1b[31m";
21
+ return `${color}${"\u2593".repeat(filled)}${"\u2591".repeat(width - filled)}\x1b[0m`;
10
22
  }
11
23
 
12
24
  export async function listProviders() {
@@ -23,11 +35,22 @@ export async function listProviders() {
23
35
 
24
36
  console.log();
25
37
  console.log(`\x1b[1m PROVIDERS\x1b[0m (${data.length})`);
26
- console.log(` ${"─".repeat(50)}`);
38
+ console.log(` ${"─".repeat(68)}`);
27
39
  for (const p of data) {
28
- const name = truncate(p.name || p.id || "?", 30);
29
- const type = (p.type || p.category || "").padEnd(15);
30
- console.log(` ${type} ${name}`);
40
+ const name = truncate(p.name || p.id || "?", 22).padEnd(24);
41
+ const successRate = p.success_rate ?? p.rate ?? null;
42
+ const samples = p.sample_count ?? p.count ?? p.total ?? null;
43
+
44
+ let rateStr = "";
45
+ if (successRate != null) {
46
+ const pct = (successRate * 100).toFixed(0);
47
+ rateStr = `${rateBar(successRate)} ${pct}%`.padEnd(22);
48
+ } else {
49
+ rateStr = "\x1b[2m-\x1b[0m".padEnd(22);
50
+ }
51
+
52
+ const sampleStr = samples != null ? `\x1b[2m${String(samples).padStart(5)} samples\x1b[0m` : "";
53
+ console.log(` ${name} ${rateStr} ${sampleStr}`);
31
54
  }
32
55
  console.log();
33
56
  }
@@ -41,16 +64,38 @@ export async function showProviderStats() {
41
64
 
42
65
  console.log();
43
66
  console.log(`\x1b[1m PROVIDER STATS\x1b[0m`);
44
- console.log(` ${"─".repeat(50)}`);
67
+ console.log(` ${"─".repeat(68)}`);
45
68
  if (Array.isArray(data)) {
46
69
  for (const p of data) {
47
- const name = (p.name || p.provider || "?").padEnd(20);
48
- const count = p.count != null ? `${p.count} uses` : "";
49
- console.log(` ${name} ${count}`);
70
+ const name = (p.name || p.provider || "?").padEnd(22);
71
+ const successRate = p.success_rate ?? p.rate ?? null;
72
+ const count = p.count ?? p.total ?? null;
73
+
74
+ let rateStr = "";
75
+ if (successRate != null) {
76
+ const pct = (successRate * 100).toFixed(0);
77
+ rateStr = `${rateBar(successRate)} ${pct}%`.padEnd(22);
78
+ }
79
+
80
+ const countStr = count != null ? `${String(count).padStart(6)} uses` : "";
81
+ console.log(` ${name} ${rateStr} ${countStr}`);
50
82
  }
51
83
  } else if (typeof data === "object") {
52
84
  for (const [key, val] of Object.entries(data)) {
53
- console.log(` ${key}: ${JSON.stringify(val)}`);
85
+ const name = key.padEnd(22);
86
+ if (typeof val === "object" && val != null) {
87
+ const successRate = val.success_rate ?? val.rate ?? null;
88
+ const count = val.count ?? val.total ?? null;
89
+ let rateStr = "";
90
+ if (successRate != null) {
91
+ const pct = (successRate * 100).toFixed(0);
92
+ rateStr = `${rateBar(successRate)} ${pct}%`.padEnd(22);
93
+ }
94
+ const countStr = count != null ? `${String(count).padStart(6)} uses` : "";
95
+ console.log(` ${name} ${rateStr} ${countStr}`);
96
+ } else {
97
+ console.log(` ${name} ${val}`);
98
+ }
54
99
  }
55
100
  }
56
101
  console.log();
@@ -4,9 +4,13 @@
4
4
 
5
5
  import { get } from "../api.mjs";
6
6
 
7
+ /** Truncate at word boundary, append "..." if needed */
7
8
  function truncate(str, len) {
8
9
  if (!str) return "";
9
- return str.length > len ? str.slice(0, len - 1) + "\u2026" : str;
10
+ if (str.length <= len) return str;
11
+ const trimmed = str.slice(0, len - 3);
12
+ const lastSpace = trimmed.lastIndexOf(" ");
13
+ return (lastSpace > len * 0.4 ? trimmed.slice(0, lastSpace) : trimmed) + "...";
10
14
  }
11
15
 
12
16
  export async function listServices() {
@@ -23,12 +27,16 @@ export async function listServices() {
23
27
 
24
28
  console.log();
25
29
  console.log(`\x1b[1m SERVICES\x1b[0m (${data.length})`);
26
- console.log(` ${"─".repeat(60)}`);
30
+ console.log(` ${"─".repeat(68)}`);
27
31
  for (const s of data) {
28
- const name = truncate(s.name || s.id, 30);
29
- const status = s.status || "?";
30
- const dot = status === "healthy" || status === "up" ? "\x1b[32m●\x1b[0m" : status === "degraded" ? "\x1b[33m●\x1b[0m" : "\x1b[31m●\x1b[0m";
31
- console.log(` ${dot} ${name.padEnd(32)} ${status}`);
32
+ const name = truncate(s.name || s.id, 28).padEnd(30);
33
+ const status = (s.status || "?").toLowerCase();
34
+ const dot = status === "healthy" || status === "up" ? "\x1b[32m●\x1b[0m"
35
+ : status === "degraded" ? "\x1b[33m●\x1b[0m"
36
+ : "\x1b[31m●\x1b[0m";
37
+ const ver = s.version ? `\x1b[2mv${s.version}\x1b[0m`.padEnd(20) : "\x1b[2m-\x1b[0m".padEnd(20);
38
+ const caps = s.capabilities ? `${Array.isArray(s.capabilities) ? s.capabilities.length : 0} caps` : "";
39
+ console.log(` ${dot} ${name} ${ver} ${caps}`);
32
40
  }
33
41
  console.log();
34
42
  }
@@ -4,9 +4,13 @@
4
4
 
5
5
  import { get } from "../api.mjs";
6
6
 
7
+ /** Truncate at word boundary, append "..." if needed */
7
8
  function truncate(str, len) {
8
9
  if (!str) return "";
9
- return str.length > len ? str.slice(0, len - 1) + "\u2026" : str;
10
+ if (str.length <= len) return str;
11
+ const trimmed = str.slice(0, len - 3);
12
+ const lastSpace = trimmed.lastIndexOf(" ");
13
+ return (lastSpace > len * 0.4 ? trimmed.slice(0, lastSpace) : trimmed) + "...";
10
14
  }
11
15
 
12
16
  export async function listSpecs(args) {
@@ -23,11 +27,15 @@ export async function listSpecs(args) {
23
27
 
24
28
  console.log();
25
29
  console.log(`\x1b[1m SPECS\x1b[0m (${data.length})`);
26
- console.log(` ${"─".repeat(72)}`);
30
+ console.log(` ${"─".repeat(74)}`);
31
+ console.log(`\x1b[2m ${"Spec ID".padEnd(22)} ${"Title".padEnd(37)} ${"ROI".padStart(6)} ${"Gap".padStart(5)}\x1b[0m`);
32
+ console.log(` ${"─".repeat(74)}`);
27
33
  for (const s of data) {
28
- const roi = s.estimated_roi != null ? `ROI ${s.estimated_roi.toFixed(1)}` : "";
29
- const gap = s.value_gap != null ? `Gap ${s.value_gap.toFixed(0)}` : "";
30
- console.log(` ${(s.spec_id || "").padEnd(8)} ${truncate(s.title || "", 38).padEnd(40)} ${roi.padEnd(12)} ${gap}`);
34
+ const specId = truncate(s.spec_id || "", 20).padEnd(22);
35
+ const title = truncate(s.title || "", 35).padEnd(37);
36
+ const roi = s.estimated_roi != null ? String(s.estimated_roi.toFixed(1)).padStart(6) : " -";
37
+ const gap = s.value_gap != null ? String(s.value_gap.toFixed(0)).padStart(5) : " -";
38
+ console.log(` ${specId} ${title} ${roi} ${gap}`);
31
39
  }
32
40
  console.log();
33
41
  }
@@ -5,6 +5,31 @@
5
5
  import { get } from "../api.mjs";
6
6
  import { getContributorId, getHubUrl } from "../config.mjs";
7
7
  import { hostname } from "node:os";
8
+ import { execSync } from "node:child_process";
9
+ import { stdin, stdout } from "node:process";
10
+ import { readFileSync } from "node:fs";
11
+ import { fileURLToPath } from "node:url";
12
+ import { dirname, join } from "node:path";
13
+
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = dirname(__filename);
16
+
17
+ function getLocalVersion() {
18
+ try {
19
+ const pkg = JSON.parse(readFileSync(join(__dirname, "../../package.json"), "utf8"));
20
+ return pkg.version;
21
+ } catch { return "?"; }
22
+ }
23
+
24
+ async function checkForUpdate(localVersion) {
25
+ try {
26
+ const latest = execSync("npm view coherence-cli version", { encoding: "utf8", timeout: 5000 }).trim();
27
+ if (latest && latest !== localVersion && localVersion !== "?") {
28
+ return latest;
29
+ }
30
+ } catch {}
31
+ return null;
32
+ }
8
33
 
9
34
  export async function showStatus() {
10
35
  // Fetch everything in parallel
@@ -66,7 +91,7 @@ export async function showStatus() {
66
91
 
67
92
  console.log(` Hub: ${getHubUrl()}`);
68
93
  console.log(` Node: ${hostname()}`);
69
- console.log(` Identity: ${getContributorId() || "\x1b[33m(not set run: cc identity set <id>)\x1b[0m"}`);
94
+ console.log(` Identity: ${getContributorId() || "\x1b[2m(anonymouslink with: cc identity link github <handle>)\x1b[0m"}`);
70
95
 
71
96
  // Ideas
72
97
  if (ideas) {
@@ -140,6 +165,40 @@ export async function showStatus() {
140
165
  }
141
166
  }
142
167
 
168
+ // Version check + auto-update prompt
169
+ const localVersion = getLocalVersion();
170
+ const latestVersion = await checkForUpdate(localVersion);
171
+ if (latestVersion) {
172
+ console.log();
173
+ console.log(`\x1b[33m Update available: ${localVersion} → ${latestVersion}\x1b[0m`);
174
+
175
+ if (stdin.isTTY) {
176
+ // Interactive — ask
177
+ const { createInterface } = await import("node:readline/promises");
178
+ const rl = createInterface({ input: stdin, output: stdout });
179
+ const answer = await rl.question(" Update now? (Y/n): ");
180
+ rl.close();
181
+ if (!answer || answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
182
+ console.log(` Updating to ${latestVersion}...`);
183
+ try {
184
+ execSync("npm i -g coherence-cli@latest", { stdio: "inherit", timeout: 60000 });
185
+ console.log(`\x1b[32m ✓ Updated to ${latestVersion}\x1b[0m`);
186
+ } catch {
187
+ console.log(`\x1b[31m ✗ Update failed. Run manually: npm i -g coherence-cli@latest\x1b[0m`);
188
+ }
189
+ }
190
+ } else {
191
+ // Non-interactive — auto-update silently
192
+ console.log(` Auto-updating to ${latestVersion}...`);
193
+ try {
194
+ execSync("npm i -g coherence-cli@latest", { stdio: "pipe", timeout: 60000 });
195
+ console.log(`\x1b[32m ✓ Updated to ${latestVersion}\x1b[0m`);
196
+ } catch {
197
+ console.log(`\x1b[2m Auto-update failed. Run: npm i -g coherence-cli@latest\x1b[0m`);
198
+ }
199
+ }
200
+ }
201
+
143
202
  console.log();
144
203
  }
145
204