cowork-cli 2.4.0 → 2.5.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 +1 -1
- package/package.json +1 -1
- package/src/configs/config.json +1 -1
- package/src/configs/sys.txt +1 -0
- package/src/utils/logger.js +1 -1
- package/src/utils/outputFormatter.js +182 -30
- package/src/utils/ui.js +1 -1
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ cwk "Explain the data flow in the engine/ models"
|
|
|
54
54
|
## ✨ Features that Matter
|
|
55
55
|
|
|
56
56
|
- **Zero-Whitespace UI**: High-density terminal output designed for professionals. No fluff, just clean, ansi-balanced structured layouts.
|
|
57
|
-
- **Rich Terminal Markdown Rendering**: Custom lightweight layout engine supporting responsive headers, bullets, blockquotes, code blocks with clean borders,
|
|
57
|
+
- **Rich Terminal Markdown Rendering**: Custom lightweight layout engine supporting responsive headers, bullets, blockquotes, code blocks with clean borders, horizontal rules, and tables, with full CJK visual character column matching.
|
|
58
58
|
- **Interactive Feedback**: The AI can request clarifications via the `askUser` tool or trigger an interactive `[ Yes ] No` toggle using `askConfirm`.
|
|
59
59
|
- **Smart Discovery**: Built-in `searchText`, `findFile`, and `projectTree` tools that respect your `.gitignore`.
|
|
60
60
|
- **Web Research**: Dynamically search the web (`webSearch`) and read documentation (`webFetch`) directly from the CLI.
|
package/package.json
CHANGED
package/src/configs/config.json
CHANGED
package/src/configs/sys.txt
CHANGED
|
@@ -16,6 +16,7 @@ You are Cowork (cwk), an interactive CLI tool acting as a Full Engineering Read-
|
|
|
16
16
|
* Inline styles: **bold**, *italic* (or _italic_), `inline code`
|
|
17
17
|
* Links: [label](url)
|
|
18
18
|
* Horizontal rules: ---
|
|
19
|
+
* Tables: standard markdown tables using | and -
|
|
19
20
|
- Cite code locations using: file_path:line_number.
|
|
20
21
|
|
|
21
22
|
# EXECUTION & STYLE
|
package/src/utils/logger.js
CHANGED
|
@@ -105,16 +105,139 @@ export function outputFormatted(text, overrideWidth) {
|
|
|
105
105
|
rawWidth = 80;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
} else {
|
|
112
|
-
width = Math.max(10, Math.floor(width * 0.95));
|
|
113
|
-
}
|
|
108
|
+
// Use full terminal width with a small 2-char right margin to prevent
|
|
109
|
+
// edge-wrapping artifacts on terminals that wrap at the last column.
|
|
110
|
+
let width = Math.max(10, rawWidth - 2);
|
|
114
111
|
const lines = strText.split('\n');
|
|
115
112
|
const wrappedLines = [];
|
|
116
|
-
|
|
117
|
-
let inCodeBlock
|
|
113
|
+
|
|
114
|
+
let inCodeBlock = false;
|
|
115
|
+
let tableBuffer = []; // accumulates raw pipe-delimited rows
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Table renderer — called when a table block is complete
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
function renderTable(rawRows) {
|
|
121
|
+
// Parse each row into trimmed cell strings
|
|
122
|
+
const parsed = rawRows.map(row => {
|
|
123
|
+
const cells = row.split('|');
|
|
124
|
+
// strip the leading/trailing empty strings caused by outer pipes
|
|
125
|
+
if (cells[0].trim() === '') cells.shift();
|
|
126
|
+
if (cells[cells.length - 1].trim() === '') cells.pop();
|
|
127
|
+
return cells.map(c => c.trim());
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (parsed.length === 0) return;
|
|
131
|
+
|
|
132
|
+
// Identify the separator row (only dashes/colons/pipes) and extract alignment
|
|
133
|
+
let sepIdx = parsed.findIndex(row =>
|
|
134
|
+
row.every(c => /^:?-+:?$/.test(c))
|
|
135
|
+
);
|
|
136
|
+
if (sepIdx === -1) sepIdx = 1; // assume row 1 is separator if not found
|
|
137
|
+
|
|
138
|
+
const alignRow = parsed[sepIdx] || [];
|
|
139
|
+
const aligns = alignRow.map(c => {
|
|
140
|
+
if (/^:-+:$/.test(c)) return 'center';
|
|
141
|
+
if (/^-+:$/.test(c)) return 'right';
|
|
142
|
+
return 'left';
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const headerRows = parsed.slice(0, sepIdx);
|
|
146
|
+
const dataRows = parsed.slice(sepIdx + 1);
|
|
147
|
+
const allDataRows = [...headerRows, ...dataRows];
|
|
148
|
+
|
|
149
|
+
// Determine number of columns
|
|
150
|
+
const numCols = Math.max(...allDataRows.map(r => r.length), aligns.length);
|
|
151
|
+
|
|
152
|
+
// Compute natural column widths (visual length of cell content)
|
|
153
|
+
const colWidths = Array(numCols).fill(0);
|
|
154
|
+
for (const row of allDataRows) {
|
|
155
|
+
for (let i = 0; i < numCols; i++) {
|
|
156
|
+
const cell = row[i] || '';
|
|
157
|
+
const cellLen = visualLength(stripAnsi(applyInlineStyles(cell)));
|
|
158
|
+
if (cellLen > colWidths[i]) colWidths[i] = cellLen;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Cap table to available width: shrink columns proportionally if needed
|
|
163
|
+
// Total width = borders(numCols+1) + padding(numCols*2) + sum(colWidths)
|
|
164
|
+
const borderOverhead = (numCols + 1) + (numCols * 2);
|
|
165
|
+
const totalNatural = colWidths.reduce((a, b) => a + b, 0) + borderOverhead;
|
|
166
|
+
if (totalNatural > width) {
|
|
167
|
+
const budget = width - borderOverhead;
|
|
168
|
+
const ratio = budget / colWidths.reduce((a, b) => a + b, 0);
|
|
169
|
+
for (let i = 0; i < numCols; i++) {
|
|
170
|
+
colWidths[i] = Math.max(1, Math.floor(colWidths[i] * ratio));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Helper: pad/truncate a cell to the target visual width
|
|
175
|
+
function fitCell(rawText, colW, align) {
|
|
176
|
+
const styled = applyInlineStyles(rawText || '');
|
|
177
|
+
const visLen = visualLength(stripAnsi(styled));
|
|
178
|
+
const overflow = visLen - colW;
|
|
179
|
+
let content = styled;
|
|
180
|
+
if (overflow > 0) {
|
|
181
|
+
// Truncate the raw text first, then re-style
|
|
182
|
+
const plain = stripAnsi(styled);
|
|
183
|
+
const cut = plain.slice(0, plain.length - overflow - 1) + '…';
|
|
184
|
+
content = applyInlineStyles(rawText.slice(0, cut.length));
|
|
185
|
+
}
|
|
186
|
+
const padTotal = colW - Math.min(visLen, colW);
|
|
187
|
+
if (align === 'right') {
|
|
188
|
+
return ' '.repeat(padTotal) + content;
|
|
189
|
+
} else if (align === 'center') {
|
|
190
|
+
const left = Math.floor(padTotal / 2);
|
|
191
|
+
const right = padTotal - left;
|
|
192
|
+
return ' '.repeat(left) + content + ' '.repeat(right);
|
|
193
|
+
} else {
|
|
194
|
+
return content + ' '.repeat(padTotal);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const D = colors.dim;
|
|
199
|
+
const R = reset;
|
|
200
|
+
|
|
201
|
+
// Box-drawing helpers
|
|
202
|
+
const hbar = (w) => '─'.repeat(w + 2); // col width + 2 padding
|
|
203
|
+
|
|
204
|
+
// ┌─...─┬─...─┐
|
|
205
|
+
const topBorder = D + '┌' + colWidths.map(hbar).join('┬') + '┐' + R;
|
|
206
|
+
// ├─...─┼─...─┤ (after header)
|
|
207
|
+
const midBorder = D + '├' + colWidths.map(hbar).join('┼') + '┤' + R;
|
|
208
|
+
// └─...─┴─...─┘
|
|
209
|
+
const botBorder = D + '└' + colWidths.map(hbar).join('┴') + '┘' + R;
|
|
210
|
+
|
|
211
|
+
function buildRow(cells, isHeader) {
|
|
212
|
+
const parts = colWidths.map((w, i) => {
|
|
213
|
+
const raw = cells[i] || '';
|
|
214
|
+
const align = aligns[i] || 'left';
|
|
215
|
+
const fitted = fitCell(raw, w, align);
|
|
216
|
+
return isHeader
|
|
217
|
+
? ` \x1b[1m${colors.main}${stripAnsi(fitted)}\x1b[22m${R} `
|
|
218
|
+
: ` ${fitted} `;
|
|
219
|
+
});
|
|
220
|
+
return D + '│' + R + parts.join(D + '│' + R) + D + '│' + R;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Add blank line before table if previous line is not blank
|
|
224
|
+
if (wrappedLines.length > 0 && wrappedLines[wrappedLines.length - 1] !== '') {
|
|
225
|
+
wrappedLines.push('');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
wrappedLines.push(topBorder);
|
|
229
|
+
for (const hrow of headerRows) {
|
|
230
|
+
wrappedLines.push(buildRow(hrow, true));
|
|
231
|
+
}
|
|
232
|
+
if (dataRows.length > 0) {
|
|
233
|
+
wrappedLines.push(midBorder);
|
|
234
|
+
for (const drow of dataRows) {
|
|
235
|
+
wrappedLines.push(buildRow(drow, false));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
wrappedLines.push(botBorder);
|
|
239
|
+
wrappedLines.push('');
|
|
240
|
+
}
|
|
118
241
|
|
|
119
242
|
// Helper function to wrap a string to a specific width (ANSI and wide-char aware)
|
|
120
243
|
function wrapString(content, wrapWidth) {
|
|
@@ -188,6 +311,18 @@ export function outputFormatted(text, overrideWidth) {
|
|
|
188
311
|
}
|
|
189
312
|
|
|
190
313
|
for (const line of lines) {
|
|
314
|
+
// --- Table row detection (pipe-delimited, outside code blocks) -----------
|
|
315
|
+
const isTableRow = !inCodeBlock && /^\s*\|/.test(line);
|
|
316
|
+
if (isTableRow) {
|
|
317
|
+
tableBuffer.push(line.trim());
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
// Flush buffered table when a non-table line is encountered
|
|
321
|
+
if (tableBuffer.length > 0) {
|
|
322
|
+
renderTable(tableBuffer);
|
|
323
|
+
tableBuffer = [];
|
|
324
|
+
}
|
|
325
|
+
|
|
191
326
|
// Check if the line already contains ANSI escape formatting (e.g., pre-colored command outputs)
|
|
192
327
|
const hasAnsi = /\x1b\[[0-9;]*[a-zA-Z]/.test(line);
|
|
193
328
|
if (hasAnsi) {
|
|
@@ -201,9 +336,7 @@ export function outputFormatted(text, overrideWidth) {
|
|
|
201
336
|
|
|
202
337
|
// 1. Detect Horizontal Rules (e.g., ---, ***, ___ )
|
|
203
338
|
if (line.match(/^(\s*)([-*_])\2\2+(\s*)$/)) {
|
|
204
|
-
|
|
205
|
-
const padding = ' '.repeat(Math.max(0, Math.floor((width - dividerWidth) / 2)));
|
|
206
|
-
wrappedLines.push(`${padding}${colors.dim}${'· '.repeat(dividerWidth / 2)}${reset}`);
|
|
339
|
+
wrappedLines.push(`${colors.dim}${'─'.repeat(width)}${reset}`);
|
|
207
340
|
continue;
|
|
208
341
|
}
|
|
209
342
|
|
|
@@ -211,13 +344,12 @@ export function outputFormatted(text, overrideWidth) {
|
|
|
211
344
|
if (line.trim().startsWith('```')) {
|
|
212
345
|
if (!inCodeBlock) {
|
|
213
346
|
inCodeBlock = true;
|
|
214
|
-
const lang = line.trim().substring(3)
|
|
215
|
-
|
|
216
|
-
|
|
347
|
+
const lang = line.trim().substring(3).trim();
|
|
348
|
+
if (lang) {
|
|
349
|
+
wrappedLines.push(`${colors.dim} ${lang}${reset}`);
|
|
350
|
+
}
|
|
217
351
|
} else {
|
|
218
352
|
inCodeBlock = false;
|
|
219
|
-
const borderLength = Math.max(5, Math.min(30, width - 4));
|
|
220
|
-
wrappedLines.push(`${colors.dim}└───${'─'.repeat(borderLength)}${reset}`);
|
|
221
353
|
}
|
|
222
354
|
continue;
|
|
223
355
|
}
|
|
@@ -233,18 +365,28 @@ export function outputFormatted(text, overrideWidth) {
|
|
|
233
365
|
if (headerMatch) {
|
|
234
366
|
const level = headerMatch[1].length;
|
|
235
367
|
const title = headerMatch[2];
|
|
236
|
-
|
|
237
|
-
|
|
368
|
+
|
|
369
|
+
// Color hierarchy: H1 = header (purple), H2 = main (blue), H3+ = dim
|
|
370
|
+
const levelColor = level === 1 ? colors.header
|
|
371
|
+
: level === 2 ? colors.main
|
|
372
|
+
: colors.dim;
|
|
373
|
+
|
|
374
|
+
// Bold for H1 and H2, normal for H3+
|
|
375
|
+
const boldStart = level <= 2 ? '\x1b[1m' : '';
|
|
376
|
+
const boldEnd = level <= 2 ? '\x1b[22m' : '';
|
|
377
|
+
|
|
378
|
+
// Add a blank line before headers for breathing room
|
|
379
|
+
if (wrappedLines.length > 0 && wrappedLines[wrappedLines.length - 1] !== '') {
|
|
380
|
+
wrappedLines.push('');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const wrapWidth = Math.max(10, width);
|
|
238
384
|
const wrappedTitle = wrapString(title, wrapWidth);
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
wrappedLines.push(`${
|
|
244
|
-
for (let i = 1; i < wrappedTitle.length; i++) {
|
|
245
|
-
const formattedSub = applyInlineStyles(wrappedTitle[i]).replace(/\x1b\[39m/g, `\x1b[39m${baseColor}`);
|
|
246
|
-
wrappedLines.push(` ${baseColor}${formattedSub}${reset}`);
|
|
247
|
-
}
|
|
385
|
+
|
|
386
|
+
for (const segment of wrappedTitle) {
|
|
387
|
+
const formatted = applyInlineStyles(segment)
|
|
388
|
+
.replace(/\x1b\[39m/g, `\x1b[39m${levelColor}`);
|
|
389
|
+
wrappedLines.push(`${boldStart}${levelColor}${formatted}${boldEnd}${reset}`);
|
|
248
390
|
}
|
|
249
391
|
continue;
|
|
250
392
|
}
|
|
@@ -263,8 +405,7 @@ export function outputFormatted(text, overrideWidth) {
|
|
|
263
405
|
let formattedPrefix = prefix;
|
|
264
406
|
const bulletMatch = prefix.match(/^(\s*)([-*+])(\s)/);
|
|
265
407
|
if (bulletMatch) {
|
|
266
|
-
|
|
267
|
-
formattedPrefix = `${bulletMatch[1]}${colors.main}${bulletSymbol}${reset}${bulletMatch[3]}`;
|
|
408
|
+
formattedPrefix = `${bulletMatch[1]}${colors.main}•${reset}${bulletMatch[3]}`;
|
|
268
409
|
} else {
|
|
269
410
|
const numberMatch = prefix.match(/^(\s*)(\d+(\.\d+)*\.)(\s)/);
|
|
270
411
|
if (numberMatch) {
|
|
@@ -284,6 +425,10 @@ export function outputFormatted(text, overrideWidth) {
|
|
|
284
425
|
// 6. Check for Blockquote structure
|
|
285
426
|
const quoteMatch = line.match(/^(\s*)(>\s?)/);
|
|
286
427
|
if (quoteMatch) {
|
|
428
|
+
// Add spacing before blockquotes for breathing room
|
|
429
|
+
if (wrappedLines.length > 0 && wrappedLines[wrappedLines.length - 1] !== '') {
|
|
430
|
+
wrappedLines.push('');
|
|
431
|
+
}
|
|
287
432
|
const prefix = quoteMatch[0];
|
|
288
433
|
const content = line.substring(prefix.length);
|
|
289
434
|
const wrapWidth = Math.max(10, width - prefix.length);
|
|
@@ -328,5 +473,12 @@ export function outputFormatted(text, overrideWidth) {
|
|
|
328
473
|
}
|
|
329
474
|
}
|
|
330
475
|
|
|
331
|
-
|
|
476
|
+
// Flush any table that ends at end-of-input
|
|
477
|
+
if (tableBuffer.length > 0) {
|
|
478
|
+
renderTable(tableBuffer);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return wrappedLines
|
|
482
|
+
.map(line => line.replace(/[\s\u200B]+(?=(?:\x1b\[[0-9;]*[a-zA-Z]|\s)*$)/g, ''))
|
|
483
|
+
.join('\n');
|
|
332
484
|
}
|
package/src/utils/ui.js
CHANGED
|
@@ -10,7 +10,7 @@ const COLORS = {
|
|
|
10
10
|
data: [194, 198, 197], // #C2C6C5 – silver (args, data)
|
|
11
11
|
success: [122, 195, 145], // #7AC391 – green (● on stop)
|
|
12
12
|
error: [224, 112, 112], // #E07070 – red (● on fail)
|
|
13
|
-
dim: [
|
|
13
|
+
dim: [143, 147, 153], // #8F9399 – cool grey (dim annotations)
|
|
14
14
|
header: [163, 122, 204], // #A37ACC – purple (● header dot)
|
|
15
15
|
};
|
|
16
16
|
|