cowork-cli 2.3.0 → 2.4.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 +2 -1
- package/package.json +1 -1
- package/src/configs/sys.txt +8 -2
- package/src/utils/logger.js +1 -1
- package/src/utils/outputFormatter.js +286 -26
package/README.md
CHANGED
|
@@ -53,7 +53,8 @@ cwk "Explain the data flow in the engine/ models"
|
|
|
53
53
|
|
|
54
54
|
## ✨ Features that Matter
|
|
55
55
|
|
|
56
|
-
- **Zero-Whitespace UI**: High-density terminal output designed for professionals. No fluff,
|
|
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
58
|
- **Interactive Feedback**: The AI can request clarifications via the `askUser` tool or trigger an interactive `[ Yes ] No` toggle using `askConfirm`.
|
|
58
59
|
- **Smart Discovery**: Built-in `searchText`, `findFile`, and `projectTree` tools that respect your `.gitignore`.
|
|
59
60
|
- **Web Research**: Dynamically search the web (`webSearch`) and read documentation (`webFetch`) directly from the CLI.
|
package/package.json
CHANGED
package/src/configs/sys.txt
CHANGED
|
@@ -8,8 +8,14 @@ You are Cowork (cwk), an interactive CLI tool acting as a Full Engineering Read-
|
|
|
8
8
|
|
|
9
9
|
# CONSTRAINTS (Terminal Environment)
|
|
10
10
|
- NO conversational filler, pleasantries, preambles, or postambles.
|
|
11
|
-
-
|
|
12
|
-
|
|
11
|
+
- Use ONLY the following supported Markdown syntax elements:
|
|
12
|
+
* Headers: # (H1), ## (H2), ### (H3)
|
|
13
|
+
* Lists: starting with -, *, + (bullets) or 1. (numbers)
|
|
14
|
+
* Blockquotes: starting with >
|
|
15
|
+
* Fenced code blocks: using ```
|
|
16
|
+
* Inline styles: **bold**, *italic* (or _italic_), `inline code`
|
|
17
|
+
* Links: [label](url)
|
|
18
|
+
* Horizontal rules: ---
|
|
13
19
|
- Cite code locations using: file_path:line_number.
|
|
14
20
|
|
|
15
21
|
# EXECUTION & STYLE
|
package/src/utils/logger.js
CHANGED
|
@@ -1,3 +1,93 @@
|
|
|
1
|
+
import { colors } from './logger.js';
|
|
2
|
+
|
|
3
|
+
const reset = '\x1b[0m';
|
|
4
|
+
|
|
5
|
+
function stripAnsi(str) {
|
|
6
|
+
return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function visualLength(str) {
|
|
10
|
+
if (!str) return 0;
|
|
11
|
+
const clean = stripAnsi(str);
|
|
12
|
+
let len = 0;
|
|
13
|
+
for (let i = 0; i < clean.length; i++) {
|
|
14
|
+
const code = clean.charCodeAt(i);
|
|
15
|
+
if ((code >= 0x3000 && code <= 0x9FFF) || (code >= 0xFF00 && code <= 0xFFEF)) {
|
|
16
|
+
len += 2;
|
|
17
|
+
} else {
|
|
18
|
+
len += 1;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return len;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function applyInlineStyles(str) {
|
|
25
|
+
if (!str) return '';
|
|
26
|
+
|
|
27
|
+
// 1. Inline code: `code` -> tool color (amber) + code + reset foreground color
|
|
28
|
+
str = str.replace(/`([^`]+)`/g, (match, code) => {
|
|
29
|
+
return `${colors.tool}${code}\x1b[39m`;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// 2. Bold styling: **text** -> bold terminal style + main color (blue) + text + reset
|
|
33
|
+
str = str.replace(/\*\*([^*]+)\*\*/g, (match, boldText) => {
|
|
34
|
+
return `\x1b[1m${colors.main}${boldText}\x1b[22m\x1b[39m`;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// 3. Italic/dim styling: *text* or _text_ -> dim color (grey) + text + reset foreground color
|
|
38
|
+
str = str.replace(/\*([^*]+)\*/g, (match, italicText) => {
|
|
39
|
+
return `${colors.dim}${italicText}\x1b[39m`;
|
|
40
|
+
});
|
|
41
|
+
str = str.replace(/_([^_]+)_/g, (match, italicText) => {
|
|
42
|
+
return `${colors.dim}${italicText}\x1b[39m`;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// 4. Link styling: [label](url) -> label (main/blue) + dim url (grey)
|
|
46
|
+
str = str.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, label, url) => {
|
|
47
|
+
return `${colors.main}${label}\x1b[39m ${colors.dim}(${url})\x1b[39m`;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return str;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function balanceAnsiStyles(wrappedLines) {
|
|
54
|
+
let activeColor = null;
|
|
55
|
+
|
|
56
|
+
return wrappedLines.map(line => {
|
|
57
|
+
let prefix = '';
|
|
58
|
+
if (activeColor) prefix += activeColor;
|
|
59
|
+
|
|
60
|
+
const ansiRegex = /\x1b\[([0-9;]*)m/g;
|
|
61
|
+
let match;
|
|
62
|
+
while ((match = ansiRegex.exec(line)) !== null) {
|
|
63
|
+
const code = match[1];
|
|
64
|
+
const parts = code.split(';');
|
|
65
|
+
|
|
66
|
+
if (parts.includes('0') || parts.includes('39')) {
|
|
67
|
+
activeColor = null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < parts.length; i++) {
|
|
71
|
+
const part = parts[i];
|
|
72
|
+
if (part === '38') {
|
|
73
|
+
if (parts[i + 1] === '2' || parts[i + 1] === '5') {
|
|
74
|
+
activeColor = match[0];
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
} else if (/^[39][0-9]$/.test(part) && part !== '39') {
|
|
78
|
+
activeColor = match[0];
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let suffix = '';
|
|
85
|
+
if (activeColor) suffix += '\x1b[39m';
|
|
86
|
+
|
|
87
|
+
return prefix + line + suffix;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
1
91
|
/**
|
|
2
92
|
* Wraps text to the current terminal width without breaking words unless necessary.
|
|
3
93
|
* Dynamically detects terminal width and preserves whitespace/indentation.
|
|
@@ -6,52 +96,82 @@
|
|
|
6
96
|
* @returns {string} The formatted text.
|
|
7
97
|
*/
|
|
8
98
|
export function outputFormatted(text, overrideWidth) {
|
|
9
|
-
if (
|
|
99
|
+
if (text === null || text === undefined) return '';
|
|
100
|
+
// Normalize newlines, strip carriage returns, and expand tabs to preserve column alignment
|
|
101
|
+
const strText = String(text).replace(/\r/g, '').replace(/\t/g, ' ');
|
|
102
|
+
|
|
103
|
+
let rawWidth = overrideWidth || process.stdout.columns || 80;
|
|
104
|
+
if (typeof rawWidth !== 'number' || isNaN(rawWidth) || rawWidth < 10) {
|
|
105
|
+
rawWidth = 80;
|
|
106
|
+
}
|
|
10
107
|
|
|
11
|
-
|
|
12
|
-
|
|
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
|
+
}
|
|
114
|
+
const lines = strText.split('\n');
|
|
115
|
+
const wrappedLines = [];
|
|
13
116
|
|
|
14
|
-
|
|
15
|
-
const wrappedLines = lines.map(line => {
|
|
16
|
-
if (line.length <= width) return line;
|
|
117
|
+
let inCodeBlock = false;
|
|
17
118
|
|
|
18
|
-
|
|
19
|
-
|
|
119
|
+
// Helper function to wrap a string to a specific width (ANSI and wide-char aware)
|
|
120
|
+
function wrapString(content, wrapWidth) {
|
|
121
|
+
const safeWidth = Math.max(5, wrapWidth);
|
|
122
|
+
if (visualLength(content) <= safeWidth) return [content];
|
|
123
|
+
|
|
124
|
+
const tokens = content.split(/(\s+)/);
|
|
20
125
|
const result = [];
|
|
21
126
|
let currentLine = '';
|
|
22
127
|
|
|
23
128
|
for (const token of tokens) {
|
|
24
129
|
if (!token) continue;
|
|
25
130
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
131
|
+
const tokenLen = visualLength(token);
|
|
132
|
+
const currentLineLen = visualLength(currentLine);
|
|
133
|
+
|
|
134
|
+
if (currentLineLen + tokenLen > safeWidth) {
|
|
135
|
+
// If it's a long word that exceeds the entire width
|
|
136
|
+
if (tokenLen > safeWidth && !/^\s+$/.test(token)) {
|
|
32
137
|
if (currentLine) {
|
|
33
138
|
result.push(currentLine.trimEnd());
|
|
34
139
|
currentLine = '';
|
|
35
140
|
}
|
|
36
141
|
|
|
37
|
-
// Force-break the long word
|
|
38
142
|
let remainingWord = token;
|
|
39
|
-
while (remainingWord
|
|
40
|
-
|
|
41
|
-
|
|
143
|
+
while (visualLength(remainingWord) > safeWidth) {
|
|
144
|
+
let visibleChars = 0;
|
|
145
|
+
let splitIdx = 0;
|
|
146
|
+
while (visibleChars < safeWidth && splitIdx < remainingWord.length) {
|
|
147
|
+
if (remainingWord.startsWith('\x1b[', splitIdx)) {
|
|
148
|
+
const endEsc = remainingWord.indexOf('m', splitIdx);
|
|
149
|
+
if (endEsc !== -1) {
|
|
150
|
+
splitIdx = endEsc + 1;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const code = remainingWord.charCodeAt(splitIdx);
|
|
155
|
+
const isWide = (code >= 0x3000 && code <= 0x9FFF) || (code >= 0xFF00 && code <= 0xFFEF);
|
|
156
|
+
if (visibleChars + (isWide ? 2 : 1) > safeWidth) {
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
visibleChars += isWide ? 2 : 1;
|
|
160
|
+
splitIdx++;
|
|
161
|
+
}
|
|
162
|
+
result.push(remainingWord.substring(0, splitIdx));
|
|
163
|
+
remainingWord = remainingWord.substring(splitIdx);
|
|
42
164
|
}
|
|
43
165
|
currentLine = remainingWord;
|
|
44
166
|
}
|
|
45
|
-
// If it's whitespace that causes overflow
|
|
167
|
+
// If it's whitespace that causes overflow
|
|
46
168
|
else if (/^\s+$/.test(token)) {
|
|
47
169
|
if (currentLine) {
|
|
48
170
|
result.push(currentLine.trimEnd());
|
|
49
171
|
currentLine = '';
|
|
50
172
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
// If it's a normal word that exceeds width, push currentLine and start new with this word
|
|
173
|
+
}
|
|
174
|
+
// Normal word that causes overflow
|
|
55
175
|
else {
|
|
56
176
|
if (currentLine) {
|
|
57
177
|
result.push(currentLine.trimEnd());
|
|
@@ -59,14 +179,154 @@ export function outputFormatted(text, overrideWidth) {
|
|
|
59
179
|
currentLine = token;
|
|
60
180
|
}
|
|
61
181
|
} else {
|
|
62
|
-
// Just append the token (word or whitespace)
|
|
63
182
|
currentLine += token;
|
|
64
183
|
}
|
|
65
184
|
}
|
|
66
185
|
|
|
67
186
|
if (currentLine) result.push(currentLine.trimEnd());
|
|
68
|
-
return result.
|
|
69
|
-
}
|
|
187
|
+
return result.length > 0 ? result : [''];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
for (const line of lines) {
|
|
191
|
+
// Check if the line already contains ANSI escape formatting (e.g., pre-colored command outputs)
|
|
192
|
+
const hasAnsi = /\x1b\[[0-9;]*[a-zA-Z]/.test(line);
|
|
193
|
+
if (hasAnsi) {
|
|
194
|
+
const wrappedSegments = wrapString(line, width);
|
|
195
|
+
const balancedSegments = balanceAnsiStyles(wrappedSegments);
|
|
196
|
+
for (const segment of balancedSegments) {
|
|
197
|
+
wrappedLines.push(segment);
|
|
198
|
+
}
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 1. Detect Horizontal Rules (e.g., ---, ***, ___ )
|
|
203
|
+
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}`);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 2. Detect code block boundaries
|
|
211
|
+
if (line.trim().startsWith('```')) {
|
|
212
|
+
if (!inCodeBlock) {
|
|
213
|
+
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}`);
|
|
217
|
+
} else {
|
|
218
|
+
inCodeBlock = false;
|
|
219
|
+
const borderLength = Math.max(5, Math.min(30, width - 4));
|
|
220
|
+
wrappedLines.push(`${colors.dim}└───${'─'.repeat(borderLength)}${reset}`);
|
|
221
|
+
}
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 3. If in code block, preserve line with vertical boundary
|
|
226
|
+
if (inCodeBlock) {
|
|
227
|
+
wrappedLines.push(`${colors.dim}│ ${reset}${line}`);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 4. Check for Markdown Headers: # Header
|
|
232
|
+
const headerMatch = line.match(/^(#+)\s*(.*)/);
|
|
233
|
+
if (headerMatch) {
|
|
234
|
+
const level = headerMatch[1].length;
|
|
235
|
+
const title = headerMatch[2];
|
|
236
|
+
const symbol = level === 1 ? '◆' : level === 2 ? '◇' : '◈';
|
|
237
|
+
const wrapWidth = Math.max(10, width - 2); // 2 is symbol + space
|
|
238
|
+
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
|
+
}
|
|
248
|
+
}
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
// 5. Check for List Item structure: bullets (- * +) or numbering (e.g., 1. or 1.1.)
|
|
255
|
+
const listMatch = line.match(/^(\s*)([-*+]\s|\d+(\.\d+)*\.\s)/);
|
|
256
|
+
if (listMatch) {
|
|
257
|
+
const prefix = listMatch[0];
|
|
258
|
+
const content = line.substring(prefix.length);
|
|
259
|
+
const wrapWidth = Math.max(10, width - prefix.length);
|
|
260
|
+
const formattedContent = applyInlineStyles(content);
|
|
261
|
+
const wrappedSegments = wrapString(formattedContent, wrapWidth);
|
|
262
|
+
|
|
263
|
+
let formattedPrefix = prefix;
|
|
264
|
+
const bulletMatch = prefix.match(/^(\s*)([-*+])(\s)/);
|
|
265
|
+
if (bulletMatch) {
|
|
266
|
+
const bulletSymbol = bulletMatch[2] === '-' ? '•' : bulletMatch[2] === '*' ? '◦' : '▪';
|
|
267
|
+
formattedPrefix = `${bulletMatch[1]}${colors.main}${bulletSymbol}${reset}${bulletMatch[3]}`;
|
|
268
|
+
} else {
|
|
269
|
+
const numberMatch = prefix.match(/^(\s*)(\d+(\.\d+)*\.)(\s)/);
|
|
270
|
+
if (numberMatch) {
|
|
271
|
+
formattedPrefix = `${numberMatch[1]}${colors.main}${numberMatch[2]}${reset}${numberMatch[4]}`;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const balancedSegments = balanceAnsiStyles(wrappedSegments);
|
|
276
|
+
wrappedLines.push(formattedPrefix + balancedSegments[0]);
|
|
277
|
+
const hangingIndent = ' '.repeat(prefix.length);
|
|
278
|
+
for (let i = 1; i < balancedSegments.length; i++) {
|
|
279
|
+
wrappedLines.push(hangingIndent + balancedSegments[i]);
|
|
280
|
+
}
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 6. Check for Blockquote structure
|
|
285
|
+
const quoteMatch = line.match(/^(\s*)(>\s?)/);
|
|
286
|
+
if (quoteMatch) {
|
|
287
|
+
const prefix = quoteMatch[0];
|
|
288
|
+
const content = line.substring(prefix.length);
|
|
289
|
+
const wrapWidth = Math.max(10, width - prefix.length);
|
|
290
|
+
const baseColor = colors.dim;
|
|
291
|
+
|
|
292
|
+
// Prefix quote content with baseColor and restore it after any inline color resets
|
|
293
|
+
const formattedContent = `${baseColor}${applyInlineStyles(content).replace(/\x1b\[39m/g, `\x1b[39m${baseColor}`)}${reset}`;
|
|
294
|
+
const wrappedSegments = wrapString(formattedContent, wrapWidth);
|
|
295
|
+
|
|
296
|
+
// Use dim color for the vertical bar to make blockquotes feel integrated and soft
|
|
297
|
+
const formattedPrefix = prefix.replace(/>\s?/, `${colors.dim}│ ${reset}`);
|
|
298
|
+
const balancedSegments = balanceAnsiStyles(wrappedSegments);
|
|
299
|
+
|
|
300
|
+
for (const segment of balancedSegments) {
|
|
301
|
+
wrappedLines.push(formattedPrefix + segment);
|
|
302
|
+
}
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 7. Check for Indented paragraph structure
|
|
307
|
+
const indentMatch = line.match(/^(\s+)/);
|
|
308
|
+
if (indentMatch) {
|
|
309
|
+
const prefix = indentMatch[0];
|
|
310
|
+
const content = line.substring(prefix.length);
|
|
311
|
+
const wrapWidth = Math.max(10, width - prefix.length);
|
|
312
|
+
const formattedContent = applyInlineStyles(content);
|
|
313
|
+
const wrappedSegments = wrapString(formattedContent, wrapWidth);
|
|
314
|
+
const balancedSegments = balanceAnsiStyles(wrappedSegments);
|
|
315
|
+
|
|
316
|
+
for (const segment of balancedSegments) {
|
|
317
|
+
wrappedLines.push(prefix + segment);
|
|
318
|
+
}
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// 8. Fallback: plain text paragraph
|
|
323
|
+
const formatted = applyInlineStyles(line);
|
|
324
|
+
const wrappedSegments = wrapString(formatted, width);
|
|
325
|
+
const balancedSegments = balanceAnsiStyles(wrappedSegments);
|
|
326
|
+
for (const segment of balancedSegments) {
|
|
327
|
+
wrappedLines.push(segment);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
70
330
|
|
|
71
331
|
return wrappedLines.join('\n');
|
|
72
332
|
}
|