clawmonitor 1.1.1 → 1.1.3

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
@@ -42,7 +42,6 @@ clawmonitor
42
42
  ```
43
43
  clawmonitor [options]
44
44
 
45
- --all Monitor all sessions (no time filter)
46
45
  --compact Compact one-line output
47
46
  --history N Show last N history entries (default: 10)
48
47
  --full Show full input/output (no truncation)
@@ -8,7 +8,7 @@ const os = require('os');
8
8
 
9
9
  // === Args ===
10
10
  const argv = process.argv.slice(2);
11
- const opts = { compact: argv.includes('--compact'), all: argv.includes('--all'), full: argv.includes('--full'), history: 10 };
11
+ const opts = { compact: argv.includes('--compact'), full: argv.includes('--full'), history: 10 };
12
12
  for (let i = 0; i < argv.length; i++) {
13
13
  if (argv[i] === '--history') { opts.history = parseInt(argv[i + 1]) || 10; i++; }
14
14
  else if (argv[i] === '--help' || argv[i] === '-h') {
@@ -16,7 +16,6 @@ for (let i = 0; i < argv.length; i++) {
16
16
 
17
17
  Usage: clawmonitor [options]
18
18
 
19
- --all Monitor all sessions (no time filter)
20
19
  --compact Compact one-line output
21
20
  --history N Show last N history entries (default: 10)
22
21
  --full Show full input/output (no truncation)
@@ -84,7 +83,7 @@ function findDir() {
84
83
  }
85
84
 
86
85
  const DIR = findDir();
87
- const TIME = opts.all ? 0 : 30;
86
+ const TIME = 0;
88
87
 
89
88
  // === Sessions ===
90
89
  function findSessions() {
@@ -207,7 +206,11 @@ const durColor = (d) => {
207
206
  // Pad a string to exactly N terminal columns (right-pad with spaces)
208
207
  const padTo = (s, n) => {
209
208
  const w = strWidth(s);
210
- if (w >= n) return s;
209
+ if (w > n) {
210
+ // Hard truncate to fit — strip ANSI, truncate, re-add border color
211
+ const plain = stripAnsi(s);
212
+ return truncTo(plain, n);
213
+ }
211
214
  return s + ' '.repeat(n - w);
212
215
  };
213
216
 
@@ -260,14 +263,14 @@ const strWidth = (s) => {
260
263
 
261
264
  // truncTo returns the truncated string. Also sets truncTo.consumed for caller.
262
265
  let _truncConsumed = 0;
263
- const truncTo = (s, maxCols) => {
266
+ const truncTo = (s, maxCols, noEllipsis) => {
264
267
  _truncConsumed = 0;
265
268
  if (!s) return s;
266
269
  // Fast path: pure ASCII
267
270
  if (/^[\x20-\x7E]*$/.test(s)) {
268
271
  if (s.length <= maxCols) { _truncConsumed = s.length; return s; }
269
272
  _truncConsumed = maxCols;
270
- return s.slice(0, maxCols) + '\u2026';
273
+ return noEllipsis ? s.slice(0, maxCols) : s.slice(0, maxCols) + '\u2026';
271
274
  }
272
275
  let w = 0, chars = [];
273
276
  for (const {segment} of segmenter.segment(s)) {
@@ -280,7 +283,7 @@ const truncTo = (s, maxCols) => {
280
283
  }
281
284
  const result = chars.join('');
282
285
  if (_truncConsumed < s.length) {
283
- return result + '\u2026';
286
+ return noEllipsis ? result : result + '\u2026';
284
287
  }
285
288
  return result;
286
289
  };
@@ -313,17 +316,17 @@ function wrapArg(key, val, maxCols, keyW) {
313
316
  const out = [];
314
317
  let text = s;
315
318
  let lineIdx = 0;
316
- while (text.length > 0 && lineIdx < (opts.full ? 999 : 3)) {
317
- const maxL = opts.full ? 999 : 3;
318
- const isLast = lineIdx === maxL - 1;
319
+ const maxL = opts.full ? 999 : 3;
320
+ while (text.length > 0 && lineIdx < maxL) {
321
+ const isLast = !opts.full && lineIdx === maxL - 1;
319
322
  const budget = isLast ? maxValW - 1 : maxValW;
320
- const chunk = truncTo(text, budget);
323
+ const chunk = truncTo(text, budget, !isLast); // only add … on last line
321
324
  out.push({ key, indent: lineIdx === 0 ? null : indent, val: chunk });
322
325
  text = text.slice(_truncConsumed);
323
326
  lineIdx++;
324
327
  if (!text) break;
325
328
  if (isLast && text) {
326
- out[out.length - 1].val = truncTo(chunk, budget - 1) + '\u2026';
329
+ out[out.length - 1].val = chunk.endsWith('\u2026') ? chunk : chunk + '\u2026';
327
330
  break;
328
331
  }
329
332
  }
@@ -387,11 +390,24 @@ const colorToken = (t) => {
387
390
 
388
391
  // Wrap tokens into lines, respecting token boundaries
389
392
  function wrapTokens(tokens, maxCols, maxLines) {
393
+ const fullMode = maxLines >= 999;
390
394
  const lines = [];
391
395
  let lineTokens = [];
392
396
  let lineW = 0;
393
397
  let linesUsed = 0;
394
398
 
399
+ // Split a string to fit maxCols without adding ellipsis
400
+ const splitFit = (s, cols) => {
401
+ let w = 0, i = 0;
402
+ for (const {segment} of segmenter.segment(s)) {
403
+ const cw = charWidth(segment);
404
+ if (w + cw > cols) break;
405
+ w += cw;
406
+ i += segment.length;
407
+ }
408
+ return { text: s.slice(0, i), consumed: i, rest: s.slice(i) };
409
+ };
410
+
395
411
  for (let ti = 0; ti < tokens.length; ti++) {
396
412
  const t = tokens[ti];
397
413
  if (lineTokens.length === 0 && t.type === 'ws') continue;
@@ -406,31 +422,32 @@ function wrapTokens(tokens, maxCols, maxLines) {
406
422
  continue;
407
423
  }
408
424
 
409
- // Token doesn't fit — split it to fill remaining space
425
+ // Token doesn't fit — split it
410
426
  if (lineTokens.length > 0 && remaining > 5) {
411
- // Split token: put first part on current line
412
- const head = truncTo(t.text, remaining);
413
- lineTokens.push({ ...t, text: head });
427
+ const sp = splitFit(t.text, remaining);
428
+ lineTokens.push({ ...t, text: sp.text });
414
429
  lines.push(lineTokens);
415
430
  linesUsed++;
416
431
  lineTokens = [];
417
432
  lineW = 0;
418
433
 
419
- // Put rest on next line(s)
420
- let rest = t.text.slice(_truncConsumed);
421
- if (rest && linesUsed < maxLines) {
422
- while (rest && linesUsed < maxLines) {
423
- const isLast = linesUsed === maxLines - 1;
424
- const budget = isLast ? maxCols - 1 : maxCols;
425
- const chunk = truncTo(rest, budget);
426
- lines.push([{ ...t, text: chunk }]);
434
+ let rest = sp.rest;
435
+ while (rest && linesUsed < maxLines) {
436
+ const isLast = !fullMode && linesUsed === maxLines - 1;
437
+ if (isLast) {
438
+ // Last line: truncate with ellipsis
439
+ const chunk = truncTo(rest, maxCols - 1);
440
+ lines.push([{ ...t, text: chunk + '\u2026' }]);
441
+ linesUsed++;
442
+ rest = '';
443
+ } else {
444
+ const sp2 = splitFit(rest, maxCols);
445
+ lines.push([{ ...t, text: sp2.text }]);
427
446
  linesUsed++;
428
- rest = rest.slice(_truncConsumed);
429
- if (chunk.endsWith('\u2026')) break;
447
+ rest = sp2.rest;
430
448
  }
431
449
  }
432
450
  } else {
433
- // Flush current line, start fresh
434
451
  if (lineTokens.length > 0) {
435
452
  lines.push(lineTokens);
436
453
  linesUsed++;
@@ -439,28 +456,34 @@ function wrapTokens(tokens, maxCols, maxLines) {
439
456
  }
440
457
  if (linesUsed >= maxLines) break;
441
458
 
442
- // Split token onto new line(s)
443
459
  let rest = t.text;
444
460
  while (rest && linesUsed < maxLines) {
445
- const isLast = linesUsed === maxLines - 1;
446
- const budget = isLast ? maxCols - 1 : maxCols;
447
- const chunk = truncTo(rest, budget);
448
- lines.push([{ ...t, text: chunk }]);
449
- linesUsed++;
450
- rest = rest.slice(_truncConsumed);
451
- if (chunk.endsWith('\u2026')) break;
461
+ const isLast = !fullMode && linesUsed === maxLines - 1;
462
+ if (isLast) {
463
+ const chunk = truncTo(rest, maxCols - 1);
464
+ lines.push([{ ...t, text: chunk + '\u2026' }]);
465
+ linesUsed++;
466
+ rest = '';
467
+ } else {
468
+ const sp = splitFit(rest, maxCols);
469
+ lines.push([{ ...t, text: sp.text }]);
470
+ linesUsed++;
471
+ rest = sp.rest;
472
+ }
452
473
  }
453
474
  }
454
475
  }
455
476
  if (lineTokens.length > 0 && linesUsed < maxLines) lines.push(lineTokens);
456
477
 
457
- while (lines.length > maxLines) lines.pop();
458
- if (lines.length >= maxLines) {
459
- const last = lines[lines.length - 1];
460
- const lastStr = last.map(t => t.text).join('');
461
- if (!lastStr.endsWith('\u2026')) {
462
- const truncated = truncTo(lastStr, maxCols - 1);
463
- lines[lines.length - 1] = [{ type: 'plain', text: truncated + '\u2026' }];
478
+ if (!fullMode) {
479
+ while (lines.length > maxLines) lines.pop();
480
+ if (lines.length >= maxLines) {
481
+ const last = lines[lines.length - 1];
482
+ const lastStr = last.map(t => t.text).join('');
483
+ if (!lastStr.endsWith('\u2026')) {
484
+ const truncated = truncTo(lastStr, maxCols - 1);
485
+ lines[lines.length - 1] = [{ type: 'plain', text: truncated + '\u2026' }];
486
+ }
464
487
  }
465
488
  }
466
489
 
@@ -472,22 +495,20 @@ const renderTokenLines = (tokenLines) => tokenLines.map(line => line.map(colorTo
472
495
 
473
496
  // Wrap result text into up to N lines, each fitting within maxCols
474
497
  function wrapResult(text, maxCols, maxLines) {
498
+ const fullMode = maxLines >= 999;
475
499
  const flat = text.replace(/[\r\n]/g, ' ').replace(/\s+/g, ' ').trim();
476
500
  const lines = [];
477
- let remaining = flat;
478
- while (remaining && lines.length < maxLines) {
479
- const isLast = lines.length === maxLines - 1;
501
+ let pos = 0;
502
+ while (pos < flat.length && lines.length < maxLines) {
503
+ const isLast = !fullMode && lines.length === maxLines - 1;
504
+ // Non-last lines: no ellipsis, strict budget
505
+ // Last line: ellipsis, budget - 1 for …
480
506
  const budget = isLast ? maxCols - 1 : maxCols;
481
- const chunk = truncTo(remaining, budget);
482
- lines.push(chunk);
483
- remaining = remaining.slice(_truncConsumed);
484
- if (!remaining) break;
485
- if (isLast) {
486
- // Force … on last line if more text remains
487
- if (!lines[lines.length - 1].endsWith('\u2026'))
488
- lines[lines.length - 1] = truncTo(chunk, budget - 1) + '\u2026';
489
- break;
490
- }
507
+ const chunk = truncTo(flat.slice(pos), budget, !isLast); // noEllipsis for non-last
508
+ lines.push(isLast && !chunk.endsWith('\u2026') && pos + _truncConsumed < flat.length ? chunk + '\u2026' : chunk);
509
+ pos += _truncConsumed;
510
+ if (pos >= flat.length) break;
511
+ if (isLast) break;
491
512
  }
492
513
  return lines;
493
514
  }
@@ -508,7 +529,7 @@ function fmt(e, label) {
508
529
  }
509
530
 
510
531
  // Card layout — full width
511
- const W = process.stdout.columns || 80;
532
+ const W = parseInt(process.env.COLUMNS) || process.stdout.columns || 80;
512
533
  const lines = [];
513
534
 
514
535
  // Top border
@@ -542,12 +563,12 @@ function fmt(e, label) {
542
563
  const isJson = flat.startsWith('{') || flat.startsWith('[');
543
564
  if (isJson) {
544
565
  const tokens = highlightJson(flat);
545
- const tokenLines = wrapTokens(tokens, W - 10, opts.full ? 999 : 2);
566
+ const tokenLines = wrapTokens(tokens, W - 5, opts.full ? 999 : 2);
546
567
  for (const tl of renderTokenLines(tokenLines)) {
547
568
  lines.push(padTo(`${T.x('│')} ${tl}`, W - 1) + T.x('│'));
548
569
  }
549
570
  } else {
550
- const rLines = wrapResult(flat, W - 10, opts.full ? 999 : 2);
571
+ const rLines = wrapResult(flat, W - 5, opts.full ? 999 : 2);
551
572
  for (const rl of rLines) {
552
573
  lines.push(padTo(`${T.x('│')} ${T.d(rl)}`, W - 1) + T.x('│'));
553
574
  }
@@ -563,7 +584,7 @@ function fmt(e, label) {
563
584
  function fmtLiveCall(tc, label) {
564
585
  const t = fmtT(tc.timestamp);
565
586
  const id = tc.id?.slice(0, 10) || '?';
566
- const W = process.stdout.columns || 80;
587
+ const W = parseInt(process.env.COLUMNS) || process.stdout.columns || 80;
567
588
 
568
589
  if (opts.compact) {
569
590
  const args = Object.entries(tc.arguments || {}).slice(0, 2).map(([k, v]) => T.x(`${k}=`) + trunc(v, 50)).join(' ');
@@ -602,18 +623,18 @@ function fmtLiveResult(msg, ts) {
602
623
  if (opts.compact) {
603
624
  console.log(` ${T.x('↳')} ${meta} ${T.d(trunc(rStr, 70))}`);
604
625
  } else {
605
- const W = process.stdout.columns || 80;
626
+ const W = parseInt(process.env.COLUMNS) || process.stdout.columns || 80;
606
627
  console.log(padTo(`${T.x('│')} ${icon} ${T.b(name)} ${meta}`, W - 1) + T.x('│'));
607
628
  const flat = rStr.replace(/[\r\n]/g, ' ').replace(/\s+/g, ' ').trim();
608
629
  const isJson = flat.startsWith('{') || flat.startsWith('[');
609
630
  if (isJson) {
610
631
  const tokens = highlightJson(flat);
611
- const tokenLines = wrapTokens(tokens, W - 10, opts.full ? 999 : 2);
632
+ const tokenLines = wrapTokens(tokens, W - 5, opts.full ? 999 : 2);
612
633
  for (const tl of renderTokenLines(tokenLines)) {
613
634
  console.log(padTo(`${T.x('│')} ${tl}`, W - 1) + T.x('│'));
614
635
  }
615
636
  } else {
616
- const rLines = wrapResult(flat, W - 10, opts.full ? 999 : 2);
637
+ const rLines = wrapResult(flat, W - 5, opts.full ? 999 : 2);
617
638
  for (const rl of rLines) {
618
639
  console.log(padTo(`${T.x('│')} ${T.d(rl)}`, W - 1) + T.x('│'));
619
640
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmonitor",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "Real-time OpenClaw tool call monitor with TUI, JSON highlighting, and zero dependencies",
5
5
  "bin": "./bin/clawmonitor.js",
6
6
  "scripts": {