agent-sh 0.10.2 → 0.10.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.
@@ -97,7 +97,8 @@ export class AgentLoop {
97
97
  // Shell-history-shaped log. Default writes go through the advisable
98
98
  // `history:append` handler registered below; extensions swap the
99
99
  // backend without touching this wiring.
100
- this.historyFile = new HistoryFile({ instanceId: this.instanceId });
100
+ const filePath = process.env.AGENT_SH_HISTORY_FILE || getSettings().historyFilePath;
101
+ this.historyFile = new HistoryFile({ instanceId: this.instanceId, filePath });
101
102
  this.conversation = new ConversationState(this.handlers, this.instanceId);
102
103
  // Fall back to a single-mode placeholder if the caller passed an
103
104
  // empty array (agent-backend does this pre-resolution).
@@ -2,6 +2,7 @@ import { type NuclearEntry } from "./nuclear-form.js";
2
2
  export declare class HistoryFile {
3
3
  readonly instanceId: string;
4
4
  private filePath;
5
+ private lockPath;
5
6
  constructor(opts?: {
6
7
  filePath?: string;
7
8
  instanceId?: string;
@@ -12,14 +12,21 @@ import * as crypto from "node:crypto";
12
12
  import { CONFIG_DIR, getSettings } from "../settings.js";
13
13
  import { serializeEntry, deserializeEntry, formatNuclearLine, isReadOnly, } from "./nuclear-form.js";
14
14
  const HISTORY_PATH = path.join(CONFIG_DIR, "history");
15
- const LOCK_PATH = HISTORY_PATH + ".lock";
16
15
  const LOCK_STALE_MS = 10_000; // consider lock stale after 10s
17
16
  export class HistoryFile {
18
17
  instanceId;
19
18
  filePath;
19
+ lockPath;
20
20
  constructor(opts) {
21
21
  this.filePath = opts?.filePath ?? HISTORY_PATH;
22
+ this.lockPath = this.filePath + ".lock";
22
23
  this.instanceId = opts?.instanceId ?? crypto.randomBytes(2).toString("hex");
24
+ // Custom paths may target a dir that doesn't exist yet; create sync so
25
+ // the first append() can't race with the mkdir.
26
+ try {
27
+ fss.mkdirSync(path.dirname(this.filePath), { recursive: true });
28
+ }
29
+ catch { /* ignore */ }
23
30
  }
24
31
  /**
25
32
  * Append entries atomically. Uses O_APPEND for concurrency safety.
@@ -218,16 +225,16 @@ export class HistoryFile {
218
225
  try {
219
226
  // Check for stale lock
220
227
  try {
221
- const stat = await fs.stat(LOCK_PATH);
228
+ const stat = await fs.stat(this.lockPath);
222
229
  if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
223
- await fs.unlink(LOCK_PATH).catch(() => { });
230
+ await fs.unlink(this.lockPath).catch(() => { });
224
231
  }
225
232
  }
226
233
  catch {
227
234
  // Lock doesn't exist — good
228
235
  }
229
236
  // O_EXCL ensures atomicity
230
- const fd = await fs.open(LOCK_PATH, fss.constants.O_CREAT | fss.constants.O_EXCL | fss.constants.O_WRONLY);
237
+ const fd = await fs.open(this.lockPath, fss.constants.O_CREAT | fss.constants.O_EXCL | fss.constants.O_WRONLY);
231
238
  await fd.close();
232
239
  return true;
233
240
  }
@@ -236,6 +243,6 @@ export class HistoryFile {
236
243
  }
237
244
  }
238
245
  async releaseLock() {
239
- await fs.unlink(LOCK_PATH).catch(() => { });
246
+ await fs.unlink(this.lockPath).catch(() => { });
240
247
  }
241
248
  }
@@ -42,6 +42,13 @@ export interface Settings {
42
42
  historyMaxBytes?: number;
43
43
  /** Number of prior history entries to load on startup (default: 50). */
44
44
  historyStartupEntries?: number;
45
+ /**
46
+ * Override the history file path. Defaults to `~/.agent-sh/history`.
47
+ * The `AGENT_SH_HISTORY_FILE` env var takes precedence over this setting.
48
+ * Use a per-project path to keep sessions isolated (e.g. embedding apps
49
+ * that boot agent-sh as a library against a specific working tree).
50
+ */
51
+ historyFilePath?: string;
45
52
  /** Auto-compact threshold as fraction of conversation budget (0-1, default 0.5). */
46
53
  autoCompactThreshold?: number;
47
54
  /** Max command output lines shown inline in TUI. */
package/dist/settings.js CHANGED
@@ -21,6 +21,7 @@ const DEFAULTS = {
21
21
  shellTailLines: 10,
22
22
  historyMaxBytes: 104857600, // 100MB — history is only accessed via search/expand, never loaded wholesale
23
23
  historyStartupEntries: 100,
24
+ historyFilePath: undefined,
24
25
  autoCompactThreshold: 0.5,
25
26
  maxCommandOutputLines: 3,
26
27
  readOutputMaxLines: 10,
@@ -7,20 +7,29 @@ export declare const GRAY = "\u001B[90m";
7
7
  export declare const BOLD = "\u001B[1m";
8
8
  export declare const RESET = "\u001B[0m";
9
9
  /**
10
- * Check if a Unicode code point is a wide character (CJK, fullwidth, emoji, etc.)
11
- * Returns 2 for wide chars, 1 for normal chars, 0 for combining chars.
10
+ * Width of a single Unicode code point in terminal columns.
12
11
  *
13
- * Based on East Asian Width and Unicode categories.
12
+ * For correct rendering of emoji clusters (ZWJ, flags, skin-tone, VS16)
13
+ * prefer `clusterWidth` or `visibleLen`, which segment graphemes first.
14
+ * This code-point-level primitive is kept for callers that iterate over
15
+ * chars for wrap-detection purposes (e.g. CJK line-break rules).
14
16
  */
15
17
  export declare function charWidth(codePoint: number): number;
18
+ /**
19
+ * Width of one grapheme cluster in terminal columns. Handles ZWJ sequences,
20
+ * regional-indicator flags, skin-tone modifiers, and VS16 emoji presentation.
21
+ */
22
+ export declare function clusterWidth(cluster: string): number;
16
23
  /**
17
24
  * Measure visible string length in terminal columns.
18
- * Excludes SGR (color/style) sequences and accounts for CJK double-width chars.
25
+ * Excludes SGR (color/style) sequences, and counts each grapheme cluster
26
+ * (emoji, CJK, combining marks) as one terminal-visible unit.
19
27
  */
20
28
  export declare function visibleLen(str: string): number;
21
29
  /**
22
30
  * Truncate a string to fit within `maxWidth` visible columns.
23
- * Accounts for CJK double-width characters. Appends `…` if truncated.
31
+ * Iterates by grapheme cluster so emoji sequences (ZWJ, flags, VS16) are
32
+ * kept intact rather than split mid-cluster. Appends `…` if truncated.
24
33
  */
25
34
  export declare function truncateToWidth(str: string, maxWidth: number): string;
26
35
  /** Truncate to visible width while preserving SGR sequences — use when
@@ -28,8 +37,10 @@ export declare function truncateToWidth(str: string, maxWidth: number): string;
28
37
  export declare function truncateAnsiToWidth(str: string, maxWidth: number): string;
29
38
  /**
30
39
  * Pad a string with spaces to fill `targetWidth` visible columns.
31
- * Accounts for CJK double-width characters.
32
40
  */
33
41
  export declare function padEndToWidth(str: string, targetWidth: number): string;
34
- /** Strip all ANSI escape sequences (SGR, OSC, CSI, private mode) and carriage returns. */
42
+ /** Strip ANSI escape sequences and carriage returns.
43
+ * Delegates escape handling to the `strip-ansi` package (covers SGR, OSC,
44
+ * CSI, private-mode, 8-bit CSI, and newer variants). `\r` is not an escape
45
+ * but callers rely on it being stripped alongside. */
35
46
  export declare function stripAnsi(str: string): string;
@@ -1,3 +1,5 @@
1
+ import stringWidth from "string-width";
2
+ import stripAnsiPkg from "strip-ansi";
1
3
  // ── ANSI escape code constants ────────────────────────────────
2
4
  export const CYAN = "\x1b[36m";
3
5
  export const DIM = "\x1b[2m";
@@ -8,160 +10,65 @@ export const GRAY = "\x1b[90m";
8
10
  export const BOLD = "\x1b[1m";
9
11
  export const RESET = "\x1b[0m";
10
12
  // ── ANSI utility functions ───────────────────────────────────
13
+ // Reused across iterations. Segmenter construction is not free, and the API
14
+ // is pure (no per-call state) so a module-level instance is safe.
15
+ const GRAPHEME_SEGMENTER = new Intl.Segmenter(undefined, { granularity: "grapheme" });
11
16
  /**
12
- * Check if a Unicode code point is a wide character (CJK, fullwidth, emoji, etc.)
13
- * Returns 2 for wide chars, 1 for normal chars, 0 for combining chars.
17
+ * Width of a single Unicode code point in terminal columns.
14
18
  *
15
- * Based on East Asian Width and Unicode categories.
19
+ * For correct rendering of emoji clusters (ZWJ, flags, skin-tone, VS16)
20
+ * prefer `clusterWidth` or `visibleLen`, which segment graphemes first.
21
+ * This code-point-level primitive is kept for callers that iterate over
22
+ * chars for wrap-detection purposes (e.g. CJK line-break rules).
16
23
  */
17
24
  export function charWidth(codePoint) {
18
- // Combining characters (zero width)
19
- if (codePoint >= 0x0300 && codePoint <= 0x036f)
20
- return 0; // Combining Diacritical Marks
21
- if (codePoint >= 0x1ab0 && codePoint <= 0x1aff)
22
- return 0; // Combining Diacritical Marks Extended
23
- if (codePoint >= 0x1dc0 && codePoint <= 0x1dff)
24
- return 0; // Combining Diacritical Marks Supplement
25
- if (codePoint >= 0x20d0 && codePoint <= 0x20ff)
26
- return 0; // Combining Diacritical Marks for Symbols
27
- if (codePoint >= 0xfe20 && codePoint <= 0xfe2f)
28
- return 0; // Combining Half Marks
29
- if (codePoint >= 0xfe00 && codePoint <= 0xfe0f)
30
- return 0; // Variation Selectors
31
- if (codePoint >= 0xe0100 && codePoint <= 0xe01ef)
32
- return 0; // Variation Selectors Supplement
33
- // Emoji and symbols that render as wide (2 columns)
34
- // Emoji presentation sequences and keycap
35
- if (codePoint === 0x20e3)
36
- return 2; // Combining Enclosing Keycap
37
- // Emoji blocks
38
- if (codePoint >= 0x1f600 && codePoint <= 0x1f64f)
39
- return 2; // Emoticons
40
- if (codePoint >= 0x1f300 && codePoint <= 0x1f5ff)
41
- return 2; // Misc Symbols and Pictographs
42
- if (codePoint >= 0x1f680 && codePoint <= 0x1f6ff)
43
- return 2; // Transport and Map
44
- if (codePoint >= 0x1f700 && codePoint <= 0x1f77f)
45
- return 2; // Alchemical Symbols
46
- if (codePoint >= 0x1f780 && codePoint <= 0x1f7ff)
47
- return 2; // Geometric Shapes Extended
48
- if (codePoint >= 0x1f800 && codePoint <= 0x1f8ff)
49
- return 2; // Supplemental Arrows-C
50
- if (codePoint >= 0x1f900 && codePoint <= 0x1f9ff)
51
- return 2; // Supplemental Symbols and Pictographs
52
- if (codePoint >= 0x1fa00 && codePoint <= 0x1faff)
53
- return 2; // Chess Symbols, Symbols and Pictographs Extended-A
54
- // NOTE: 0x2300-0x23ff (Misc Technical), 0x2600-0x26ff (Misc Symbols),
55
- // and 0x2700-0x27bf (Dingbats) are mostly "Ambiguous" width — render as
56
- // 1 column in non-CJK terminal locales (e.g. ❯, ⌘, ★, ♦). But a handful
57
- // of dingbats have Emoji_Presentation=Yes and render as 2 cols everywhere.
58
- if (codePoint === 0x2705 || // ✅ white heavy check mark
59
- codePoint === 0x270a || // ✊ raised fist
60
- codePoint === 0x270b || // ✋ raised hand
61
- codePoint === 0x2728 || // ✨ sparkles
62
- codePoint === 0x274c || // ❌ cross mark
63
- codePoint === 0x274e || // ❎ negative squared cross mark
64
- (codePoint >= 0x2753 && codePoint <= 0x2755) || // ❓❔❕
65
- codePoint === 0x2757 || // ❗ heavy exclamation mark
66
- (codePoint >= 0x2795 && codePoint <= 0x2797) || // ➕➖➗
67
- codePoint === 0x27b0 || // ➰ curly loop
68
- codePoint === 0x27bf // ➿ double curly loop
69
- )
70
- return 2;
71
- // Regional indicator symbols (flag emoji components)
72
- if (codePoint >= 0x1f1e6 && codePoint <= 0x1f1ff)
73
- return 2;
74
- // CJK Unified Ideographs
75
- if (codePoint >= 0x4e00 && codePoint <= 0x9fff)
76
- return 2;
77
- // CJK Unified Ideographs Extension A
78
- if (codePoint >= 0x3400 && codePoint <= 0x4dbf)
79
- return 2;
80
- // Hangul Syllables
81
- if (codePoint >= 0xac00 && codePoint <= 0xd7af)
82
- return 2;
83
- // CJK Unified Ideographs Extension B-F and other CJK blocks
84
- if (codePoint >= 0x20000 && codePoint <= 0x2ebef)
85
- return 2;
86
- // Fullwidth ASCII variants
87
- if (codePoint >= 0xff01 && codePoint <= 0xff5e)
88
- return 2;
89
- // Fullwidth bracket forms
90
- if (codePoint >= 0xff5f && codePoint <= 0xff60)
91
- return 2;
92
- // Fullwidth symbol variants
93
- if (codePoint >= 0xffe0 && codePoint <= 0xffe6)
94
- return 2;
95
- // Japanese hiragana and katakana
96
- if (codePoint >= 0x3040 && codePoint <= 0x309f)
97
- return 2;
98
- if (codePoint >= 0x30a0 && codePoint <= 0x30ff)
99
- return 2;
100
- // CJK symbols and punctuation
101
- if (codePoint >= 0x3000 && codePoint <= 0x303f)
102
- return 2;
103
- // Enclosed CJK letters and months
104
- if (codePoint >= 0x3200 && codePoint <= 0x32ff)
105
- return 2;
106
- // CJK compatibility
107
- if (codePoint >= 0x3300 && codePoint <= 0x33ff)
108
- return 2;
109
- // Hangul Jamo
110
- if (codePoint >= 0x1100 && codePoint <= 0x11ff)
111
- return 2;
112
- // Hangul compatibility Jamo
113
- if (codePoint >= 0x3130 && codePoint <= 0x318f)
114
- return 2;
115
- return 1;
25
+ return stringWidth(String.fromCodePoint(codePoint));
26
+ }
27
+ /**
28
+ * Width of one grapheme cluster in terminal columns. Handles ZWJ sequences,
29
+ * regional-indicator flags, skin-tone modifiers, and VS16 emoji presentation.
30
+ */
31
+ export function clusterWidth(cluster) {
32
+ return stringWidth(cluster);
33
+ }
34
+ /** Strip SGR (color/style) sequences from a string. */
35
+ function stripSGR(str) {
36
+ return str.replace(/\x1b\[[^m]*m/g, "");
116
37
  }
117
38
  /**
118
39
  * Measure visible string length in terminal columns.
119
- * Excludes SGR (color/style) sequences and accounts for CJK double-width chars.
40
+ * Excludes SGR (color/style) sequences, and counts each grapheme cluster
41
+ * (emoji, CJK, combining marks) as one terminal-visible unit.
120
42
  */
121
43
  export function visibleLen(str) {
122
- // First strip ANSI escape sequences
123
- const cleanStr = str.replace(/\x1b\[[^m]*m/g, "");
124
- let width = 0;
125
- for (const char of cleanStr) {
126
- width += charWidth(char.codePointAt(0) ?? 0);
127
- }
128
- return width;
44
+ return stringWidth(stripSGR(str));
129
45
  }
130
46
  /**
131
47
  * Truncate a string to fit within `maxWidth` visible columns.
132
- * Accounts for CJK double-width characters. Appends `…` if truncated.
48
+ * Iterates by grapheme cluster so emoji sequences (ZWJ, flags, VS16) are
49
+ * kept intact rather than split mid-cluster. Appends `…` if truncated.
133
50
  */
134
51
  export function truncateToWidth(str, maxWidth) {
135
- const clean = str.replace(/\x1b\[[^m]*m/g, "");
52
+ const clean = stripSGR(str);
136
53
  if (maxWidth <= 0)
137
54
  return "";
138
- // First check if the entire string fits
139
- let fullWidth = 0;
140
- for (const char of clean) {
141
- fullWidth += charWidth(char.codePointAt(0) ?? 0);
142
- }
143
- if (fullWidth <= maxWidth)
55
+ if (visibleLen(clean) <= maxWidth)
144
56
  return clean;
145
- // String doesn't fit — truncate with "…"
146
- // At maxWidth=1 the ellipsis alone fills the budget.
147
57
  if (maxWidth === 1)
148
58
  return "…";
149
- // Reserve 1 column for "…", so target content width is maxWidth - 1
150
59
  const target = maxWidth - 1;
151
60
  let width = 0;
152
- let i = 0;
153
- for (const char of clean) {
154
- const cw = charWidth(char.codePointAt(0) ?? 0);
61
+ let out = "";
62
+ for (const { segment } of GRAPHEME_SEGMENTER.segment(clean)) {
63
+ const cw = clusterWidth(segment);
155
64
  if (width + cw > target)
156
65
  break;
157
66
  width += cw;
158
- i += char.length;
67
+ out += segment;
159
68
  }
160
- // If nothing fit (first char is wider than target), just show the ellipsis
161
- // rather than emit a character that would overflow the budget.
162
- if (i === 0)
69
+ if (out === "")
163
70
  return "…";
164
- return clean.slice(0, i) + "…";
71
+ return out + "…";
165
72
  }
166
73
  /** Truncate to visible width while preserving SGR sequences — use when
167
74
  * input carries color/bold codes. `truncateToWidth` strips them. */
@@ -173,43 +80,57 @@ export function truncateAnsiToWidth(str, maxWidth) {
173
80
  if (maxWidth === 1)
174
81
  return "…";
175
82
  const target = maxWidth - 1;
83
+ // Walk the string preserving SGR escapes in-place; buffer text between
84
+ // escapes and segment it into graphemes to count width correctly.
176
85
  let width = 0;
177
86
  let out = "";
87
+ let buf = "";
178
88
  let i = 0;
89
+ const flushBuf = () => {
90
+ if (!buf)
91
+ return false;
92
+ for (const { segment } of GRAPHEME_SEGMENTER.segment(buf)) {
93
+ const cw = clusterWidth(segment);
94
+ if (width + cw > target) {
95
+ buf = "";
96
+ return true; // budget exhausted
97
+ }
98
+ width += cw;
99
+ out += segment;
100
+ }
101
+ buf = "";
102
+ return false;
103
+ };
179
104
  while (i < str.length) {
180
105
  if (str[i] === "\x1b" && str[i + 1] === "[") {
181
106
  const end = str.indexOf("m", i);
182
107
  if (end !== -1) {
108
+ if (flushBuf())
109
+ break;
183
110
  out += str.slice(i, end + 1);
184
111
  i = end + 1;
185
112
  continue;
186
113
  }
187
114
  }
188
115
  const cp = str.codePointAt(i) ?? 0;
189
- const cw = charWidth(cp);
190
- if (width + cw > target)
191
- break;
192
116
  const chLen = cp > 0xffff ? 2 : 1;
193
- out += str.slice(i, i + chLen);
194
- width += cw;
117
+ buf += str.slice(i, i + chLen);
195
118
  i += chLen;
196
119
  }
120
+ flushBuf();
197
121
  return out + "\x1b[0m…";
198
122
  }
199
123
  /**
200
124
  * Pad a string with spaces to fill `targetWidth` visible columns.
201
- * Accounts for CJK double-width characters.
202
125
  */
203
126
  export function padEndToWidth(str, targetWidth) {
204
127
  const gap = targetWidth - visibleLen(str);
205
128
  return gap > 0 ? str + " ".repeat(gap) : str;
206
129
  }
207
- /** Strip all ANSI escape sequences (SGR, OSC, CSI, private mode) and carriage returns. */
130
+ /** Strip ANSI escape sequences and carriage returns.
131
+ * Delegates escape handling to the `strip-ansi` package (covers SGR, OSC,
132
+ * CSI, private-mode, 8-bit CSI, and newer variants). `\r` is not an escape
133
+ * but callers rely on it being stripped alongside. */
208
134
  export function stripAnsi(str) {
209
- return str
210
- .replace(/\x1b\][^\x07]*\x07/g, "") // OSC sequences
211
- .replace(/\x1b\[[^m]*m/g, "") // SGR (color) sequences
212
- .replace(/\x1b\[\?[^a-zA-Z]*[a-zA-Z]/g, "") // private mode sequences
213
- .replace(/\x1b\[[^a-zA-Z]*[a-zA-Z]/g, "") // CSI sequences
214
- .replace(/\r/g, ""); // carriage returns
135
+ return stripAnsiPkg(str).replace(/\r/g, "");
215
136
  }
@@ -269,12 +269,20 @@ export class MarkdownRenderer {
269
269
  const separatorWidth = (numCols - 1) * 3;
270
270
  const tableWidth = Math.max(10, this.width - 2);
271
271
  const availableWidth = tableWidth - separatorWidth;
272
- const totalWidth = colWidths.reduce((a, b) => a + b, 0);
273
- if (totalWidth > availableWidth && availableWidth > numCols) {
274
- const scale = availableWidth / totalWidth;
275
- for (let c = 0; c < numCols; c++) {
276
- colWidths[c] = Math.max(1, Math.floor(colWidths[c] * scale));
272
+ // Shrink the widest column one step at a time until the table fits.
273
+ // Preserves natural width on narrow columns proportional scaling
274
+ // over-truncates when only one column is oversized.
275
+ let total = colWidths.reduce((a, b) => a + b, 0);
276
+ while (total > availableWidth && availableWidth > numCols) {
277
+ let maxIdx = 0;
278
+ for (let c = 1; c < numCols; c++) {
279
+ if (colWidths[c] > colWidths[maxIdx])
280
+ maxIdx = c;
277
281
  }
282
+ if (colWidths[maxIdx] <= 1)
283
+ break;
284
+ colWidths[maxIdx]--;
285
+ total--;
278
286
  }
279
287
  // Render rows
280
288
  const hasHeader = sepIdx.includes(1) && dataRows.length > 1;
@@ -287,9 +295,13 @@ export class MarkdownRenderer {
287
295
  const cells = row.map((cell, c) => {
288
296
  const w = colWidths[c];
289
297
  const rendered = this.renderInline(cell);
290
- const text = visibleLen(rendered) > w
298
+ // Truncation can yield width < w when a CJK double-width char
299
+ // won't fit the remaining budget — always re-pad to keep cells
300
+ // aligned with the border grid.
301
+ const clipped = visibleLen(rendered) > w
291
302
  ? truncateAnsiToWidth(rendered, w)
292
- : padEndToWidth(rendered, w);
303
+ : rendered;
304
+ const text = padEndToWidth(clipped, w);
293
305
  return isHeader ? `${p.bold}${text}${p.reset}` : text;
294
306
  });
295
307
  this.writeLine(`${p.dim}│${p.reset} ${cells.join(` ${p.dim}│${p.reset} `)} ${p.dim}│${p.reset}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-sh",
3
- "version": "0.10.2",
3
+ "version": "0.10.3",
4
4
  "description": "A shell-first terminal where AI is one keystroke away",
5
5
  "type": "module",
6
6
  "main": "dist/core.js",
@@ -34,6 +34,10 @@
34
34
  "types": "./dist/extensions/index.d.ts",
35
35
  "default": "./dist/extensions/index.js"
36
36
  },
37
+ "./shell": {
38
+ "types": "./dist/shell/shell.d.ts",
39
+ "default": "./dist/shell/shell.js"
40
+ },
37
41
  "./utils/stream-transform": {
38
42
  "types": "./dist/utils/stream-transform.d.ts",
39
43
  "default": "./dist/utils/stream-transform.js"
@@ -122,6 +126,8 @@
122
126
  "marked": "^17.0.6",
123
127
  "node-pty": "^1.2.0-beta.12",
124
128
  "openai": "^6.34.0",
129
+ "string-width": "^8.2.0",
130
+ "strip-ansi": "^7.2.0",
125
131
  "tsx": "^4.19.0"
126
132
  },
127
133
  "devDependencies": {