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 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, and subtle horizontal rules, with full CJK visual character column matching.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cowork-cli",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "description": "work with cowork",
5
5
  "bin": {
6
6
  "cwk": "bin/cli.js"
@@ -5,7 +5,7 @@
5
5
  "data": "#C2C6C5",
6
6
  "success": "#7AC391",
7
7
  "error": "#E07070",
8
- "dim": "#606060",
8
+ "dim": "#8F9399",
9
9
  "header": "#A37ACC"
10
10
  }
11
11
  }
@@ -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
@@ -16,7 +16,7 @@ try {
16
16
  data: '#C2C6C5',
17
17
  success: '#7AC391',
18
18
  error: '#E07070',
19
- dim: '#606060',
19
+ dim: '#8F9399',
20
20
  header: '#A37ACC',
21
21
  }
22
22
  };
@@ -105,16 +105,139 @@ export function outputFormatted(text, overrideWidth) {
105
105
  rawWidth = 80;
106
106
  }
107
107
 
108
- let width = rawWidth;
109
- if (width >= 60) {
110
- width = Math.floor(width * 0.80);
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 = false;
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
- const dividerWidth = Math.min(40, width);
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) || 'code';
215
- const borderLength = Math.max(5, Math.min(30, width - lang.length - 7));
216
- wrappedLines.push(`${colors.dim}┌── [${colors.tool}${lang}${colors.dim}] ${'─'.repeat(borderLength)}${reset}`);
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
- const symbol = level === 1 ? '◆' : level === 2 ? '◇' : '◈';
237
- const wrapWidth = Math.max(10, width - 2); // 2 is symbol + space
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
- const baseColor = colors.header;
240
-
241
- if (wrappedTitle.length > 0) {
242
- const formattedFirst = applyInlineStyles(wrappedTitle[0]).replace(/\x1b\[39m/g, `\x1b[39m${baseColor}`);
243
- wrappedLines.push(`${colors.header}${symbol} ${baseColor}${formattedFirst}${reset}`);
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
- const bulletSymbol = bulletMatch[2] === '-' ? '•' : bulletMatch[2] === '*' ? '◦' : '▪';
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
- return wrappedLines.join('\n');
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: [ 96, 96, 96], // #606060 – grey (dim annotations)
13
+ dim: [143, 147, 153], // #8F9399cool grey (dim annotations)
14
14
  header: [163, 122, 204], // #A37ACC – purple (● header dot)
15
15
  };
16
16