nex-code 0.3.4 → 0.3.7

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/cli/costs.js DELETED
@@ -1,290 +0,0 @@
1
- /**
2
- * cli/costs.js — Token Cost Tracking + Dashboard + Budget Limits
3
- * Tracks token usage per provider/model and estimates session costs.
4
- * Supports per-provider cost limits with budget gating.
5
- */
6
-
7
- const fs = require('fs');
8
- const path = require('path');
9
-
10
- const PRICING = {
11
- 'openai': {
12
- 'gpt-4o': { input: 2.50, output: 10.00 },
13
- 'gpt-4o-mini': { input: 0.15, output: 0.60 },
14
- 'gpt-4.1': { input: 2.00, output: 8.00 },
15
- 'gpt-4.1-mini': { input: 0.40, output: 1.60 },
16
- 'gpt-4.1-nano': { input: 0.10, output: 0.40 },
17
- 'o1': { input: 15.00, output: 60.00 },
18
- 'o3': { input: 10.00, output: 40.00 },
19
- 'o3-mini': { input: 1.10, output: 4.40 },
20
- 'o4-mini': { input: 1.10, output: 4.40 },
21
- },
22
- 'anthropic': {
23
- 'claude-sonnet': { input: 3.00, output: 15.00 },
24
- 'claude-opus': { input: 5.00, output: 25.00 },
25
- 'claude-haiku': { input: 0.80, output: 4.00 },
26
- 'claude-sonnet-4-5': { input: 3.00, output: 15.00 },
27
- 'claude-sonnet-4': { input: 3.00, output: 15.00 },
28
- },
29
- 'gemini': {
30
- 'gemini-2.5-pro': { input: 1.25, output: 10.00 },
31
- 'gemini-2.5-flash': { input: 0.15, output: 0.60 },
32
- 'gemini-2.0-flash': { input: 0.10, output: 0.40 },
33
- 'gemini-2.0-flash-lite': { input: 0.075, output: 0.30 },
34
- },
35
- 'ollama': {
36
- 'qwen3-coder:480b': { input: 0, output: 0 },
37
- 'qwen3-coder-next': { input: 0, output: 0 },
38
- 'devstral-2:123b': { input: 0, output: 0 },
39
- 'devstral-small-2:24b': { input: 0, output: 0 },
40
- 'kimi-k2.5': { input: 0, output: 0 },
41
- 'kimi-k2:1t': { input: 0, output: 0 },
42
- 'deepseek-v3.2': { input: 0, output: 0 },
43
- 'minimax-m2.5': { input: 0, output: 0 },
44
- 'glm-5': { input: 0, output: 0 },
45
- 'glm-4.7': { input: 0, output: 0 },
46
- 'gpt-oss:120b': { input: 0, output: 0 },
47
- },
48
- 'local': {},
49
- };
50
-
51
- // Session usage accumulator
52
- let usageLog = [];
53
-
54
- // Per-provider cost limits (e.g. { anthropic: 5.00, openai: 10.00 })
55
- let costLimits = {};
56
-
57
- /**
58
- * Track token usage for a single API call.
59
- * @param {string} provider
60
- * @param {string} model
61
- * @param {number} inputTokens
62
- * @param {number} outputTokens
63
- */
64
- function trackUsage(provider, model, inputTokens, outputTokens) {
65
- usageLog.push({ provider, model, input: inputTokens, output: outputTokens });
66
-
67
- // Check budget after tracking
68
- if (costLimits[provider] !== undefined) {
69
- const budget = checkBudget(provider);
70
- if (!budget.allowed) {
71
- process.stderr.write(`\x1b[33m\u26a0 Budget limit reached for ${provider}: $${budget.spent.toFixed(2)} / $${budget.limit.toFixed(2)}\x1b[0m\n`);
72
- }
73
- }
74
- }
75
-
76
- /**
77
- * Get pricing for a provider/model pair.
78
- * Returns { input: 0, output: 0 } for unknown models.
79
- */
80
- function getPricing(provider, model) {
81
- const providerPricing = PRICING[provider];
82
- if (!providerPricing) return { input: 0, output: 0 };
83
- return providerPricing[model] || { input: 0, output: 0 };
84
- }
85
-
86
- /**
87
- * Calculate cost for a single usage entry.
88
- */
89
- function calcCost(entry) {
90
- const pricing = getPricing(entry.provider, entry.model);
91
- return (entry.input * pricing.input + entry.output * pricing.output) / 1_000_000;
92
- }
93
-
94
- /**
95
- * Get aggregated session costs.
96
- * @returns {{ totalCost: number, totalInput: number, totalOutput: number, breakdown: Array }}
97
- */
98
- function getSessionCosts() {
99
- const byKey = {};
100
-
101
- for (const entry of usageLog) {
102
- const key = `${entry.provider}:${entry.model}`;
103
- if (!byKey[key]) {
104
- byKey[key] = { provider: entry.provider, model: entry.model, input: 0, output: 0 };
105
- }
106
- byKey[key].input += entry.input;
107
- byKey[key].output += entry.output;
108
- }
109
-
110
- const breakdown = Object.values(byKey).map((b) => ({
111
- ...b,
112
- cost: calcCost(b),
113
- }));
114
-
115
- const totalCost = breakdown.reduce((sum, b) => sum + b.cost, 0);
116
- const totalInput = breakdown.reduce((sum, b) => sum + b.input, 0);
117
- const totalOutput = breakdown.reduce((sum, b) => sum + b.output, 0);
118
-
119
- return { totalCost, totalInput, totalOutput, breakdown };
120
- }
121
-
122
- /**
123
- * Format costs for display (/costs command).
124
- * @returns {string}
125
- */
126
- function formatCosts() {
127
- const { totalCost, totalInput, totalOutput, breakdown } = getSessionCosts();
128
-
129
- if (breakdown.length === 0) {
130
- return 'No token usage recorded this session.';
131
- }
132
-
133
- const lines = [];
134
- lines.push('Session Token Usage:');
135
- lines.push('');
136
-
137
- for (const b of breakdown) {
138
- const costStr = b.cost > 0 ? `$${b.cost.toFixed(4)}` : 'free';
139
- lines.push(` ${b.provider}:${b.model}`);
140
- lines.push(` Input: ${b.input.toLocaleString()} tokens`);
141
- lines.push(` Output: ${b.output.toLocaleString()} tokens`);
142
- lines.push(` Cost: ${costStr}`);
143
- }
144
-
145
- lines.push('');
146
- lines.push(` Total: ${totalInput.toLocaleString()} in + ${totalOutput.toLocaleString()} out = $${totalCost.toFixed(4)}`);
147
-
148
- return lines.join('\n');
149
- }
150
-
151
- /**
152
- * Format a short cost hint for inline display after responses.
153
- * @returns {string} e.g. "[~$0.003]" or "" if free
154
- */
155
- function formatCostHint(provider, model, inputTokens, outputTokens) {
156
- const pricing = getPricing(provider, model);
157
- const cost = (inputTokens * pricing.input + outputTokens * pricing.output) / 1_000_000;
158
- if (cost <= 0) return '';
159
- return `[~$${cost.toFixed(4)}]`;
160
- }
161
-
162
- /**
163
- * Reset all tracked usage (for testing or new sessions).
164
- */
165
- function resetCosts() {
166
- usageLog = [];
167
- }
168
-
169
- // ─── Cost Limits ───────────────────────────────────────────────
170
-
171
- /**
172
- * Set a cost limit for a provider (in dollars).
173
- */
174
- function setCostLimit(provider, maxDollars) {
175
- costLimits[provider] = maxDollars;
176
- }
177
-
178
- /**
179
- * Remove a cost limit for a provider.
180
- */
181
- function removeCostLimit(provider) {
182
- delete costLimits[provider];
183
- }
184
-
185
- /**
186
- * Get all cost limits.
187
- * @returns {object}
188
- */
189
- function getCostLimits() {
190
- return { ...costLimits };
191
- }
192
-
193
- /**
194
- * Get total spend for a specific provider in this session.
195
- */
196
- function getProviderSpend(provider) {
197
- let total = 0;
198
- for (const entry of usageLog) {
199
- if (entry.provider === provider) {
200
- total += calcCost(entry);
201
- }
202
- }
203
- return total;
204
- }
205
-
206
- /**
207
- * Check if a provider is within budget.
208
- * @returns {{ allowed: boolean, spent: number, limit: number|null, remaining: number|null }}
209
- */
210
- function checkBudget(provider) {
211
- const spent = getProviderSpend(provider);
212
- const limit = costLimits[provider];
213
-
214
- if (limit === undefined) {
215
- return { allowed: true, spent, limit: null, remaining: null };
216
- }
217
-
218
- const remaining = Math.max(0, limit - spent);
219
- return {
220
- allowed: spent < limit,
221
- spent,
222
- limit,
223
- remaining,
224
- };
225
- }
226
-
227
- /**
228
- * Load cost limits from .nex/config.json
229
- */
230
- function loadCostLimits() {
231
- const configPath = path.join(process.cwd(), '.nex', 'config.json');
232
- if (!fs.existsSync(configPath)) return;
233
- try {
234
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
235
- if (config.costLimits && typeof config.costLimits === 'object') {
236
- costLimits = { ...config.costLimits };
237
- }
238
- } catch {
239
- // ignore corrupt config
240
- }
241
- }
242
-
243
- /**
244
- * Save cost limits to .nex/config.json
245
- */
246
- function saveCostLimits() {
247
- const configDir = path.join(process.cwd(), '.nex');
248
- const configPath = path.join(configDir, 'config.json');
249
-
250
- let config = {};
251
- if (fs.existsSync(configPath)) {
252
- try {
253
- config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
254
- } catch {
255
- config = {};
256
- }
257
- }
258
-
259
- config.costLimits = costLimits;
260
- if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
261
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
262
- }
263
-
264
- /**
265
- * Reset cost limits (for testing).
266
- */
267
- function resetCostLimits() {
268
- costLimits = {};
269
- }
270
-
271
- // Load cost limits on init
272
- loadCostLimits();
273
-
274
- module.exports = {
275
- PRICING,
276
- trackUsage,
277
- getSessionCosts,
278
- formatCosts,
279
- formatCostHint,
280
- resetCosts,
281
- getPricing,
282
- setCostLimit,
283
- removeCostLimit,
284
- getCostLimits,
285
- getProviderSpend,
286
- checkBudget,
287
- loadCostLimits,
288
- saveCostLimits,
289
- resetCostLimits,
290
- };
package/cli/diff.js DELETED
@@ -1,366 +0,0 @@
1
- /**
2
- * cli/diff.js — Diff Display + Confirmation
3
- * Minimal Myers-diff-lite for line-by-line comparison.
4
- */
5
-
6
- const path = require('path');
7
- const { C } = require('./ui');
8
- const { confirm, getAutoConfirm } = require('./safety');
9
-
10
- /**
11
- * Simple LCS-based line diff (no external deps)
12
- * Returns array of { type: 'same'|'add'|'remove', line: string }
13
- */
14
- const DIFF_LINE_LIMIT = 2000;
15
-
16
- function diffLines(oldText, newText) {
17
- const oldLines = oldText.split('\n');
18
- const newLines = newText.split('\n');
19
- const result = [];
20
-
21
- // Guard against OOM on huge files — fall back to simple summary
22
- const m = oldLines.length;
23
- const n = newLines.length;
24
- if (m > DIFF_LINE_LIMIT || n > DIFF_LINE_LIMIT) {
25
- // Produce a simplified diff: all old lines removed, all new lines added
26
- for (const line of oldLines) result.push({ type: 'remove', line });
27
- for (const line of newLines) result.push({ type: 'add', line });
28
- return result;
29
- }
30
- const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
31
-
32
- for (let i = 1; i <= m; i++) {
33
- for (let j = 1; j <= n; j++) {
34
- if (oldLines[i - 1] === newLines[j - 1]) {
35
- dp[i][j] = dp[i - 1][j - 1] + 1;
36
- } else {
37
- dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
38
- }
39
- }
40
- }
41
-
42
- // Backtrack
43
- let i = m,
44
- j = n;
45
- const ops = [];
46
- while (i > 0 || j > 0) {
47
- if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
48
- ops.unshift({ type: 'same', line: oldLines[i - 1] });
49
- i--;
50
- j--;
51
- } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
52
- ops.unshift({ type: 'add', line: newLines[j - 1] });
53
- j--;
54
- } else {
55
- ops.unshift({ type: 'remove', line: oldLines[i - 1] });
56
- i--;
57
- }
58
- }
59
-
60
- return ops;
61
- }
62
-
63
- /**
64
- * Show colored diff for edit_file (old_text → new_text) with context lines
65
- */
66
- function showEditDiff(filePath, oldText, newText, contextLines = 3) {
67
- console.log(`\n${C.bold}${C.cyan} Diff: ${filePath}${C.reset}`);
68
- const ops = diffLines(oldText, newText);
69
-
70
- // Find changed regions and show with context
71
- const changed = [];
72
- ops.forEach((op, idx) => {
73
- if (op.type !== 'same') changed.push(idx);
74
- });
75
-
76
- if (changed.length === 0) {
77
- console.log(`${C.gray} (no changes)${C.reset}`);
78
- return;
79
- }
80
-
81
- const showFrom = Math.max(0, changed[0] - contextLines);
82
- const showTo = Math.min(ops.length, changed[changed.length - 1] + contextLines + 1);
83
-
84
- if (showFrom > 0) console.log(`${C.gray} ...${C.reset}`);
85
-
86
- for (let k = showFrom; k < showTo; k++) {
87
- const op = ops[k];
88
- switch (op.type) {
89
- case 'remove':
90
- console.log(`${C.red} - ${op.line}${C.reset}`);
91
- break;
92
- case 'add':
93
- console.log(`${C.green} + ${op.line}${C.reset}`);
94
- break;
95
- default:
96
- console.log(`${C.gray} ${op.line}${C.reset}`);
97
- }
98
- }
99
-
100
- if (showTo < ops.length) console.log(`${C.gray} ...${C.reset}`);
101
- console.log();
102
- }
103
-
104
- /**
105
- * Show diff for write_file when file already exists
106
- */
107
- function showWriteDiff(filePath, oldContent, newContent) {
108
- console.log(`\n${C.bold}${C.cyan} File exists — showing changes: ${filePath}${C.reset}`);
109
- const ops = diffLines(oldContent, newContent);
110
-
111
- let changes = 0;
112
- for (const op of ops) {
113
- if (op.type !== 'same') changes++;
114
- }
115
-
116
- if (changes === 0) {
117
- console.log(`${C.gray} (identical content)${C.reset}`);
118
- return;
119
- }
120
-
121
- // Show up to 30 diff lines
122
- let shown = 0;
123
- for (const op of ops) {
124
- if (shown >= 30) {
125
- console.log(`${C.gray} ...(${changes - shown} more changes)${C.reset}`);
126
- break;
127
- }
128
- switch (op.type) {
129
- case 'remove':
130
- console.log(`${C.red} - ${op.line}${C.reset}`);
131
- shown++;
132
- break;
133
- case 'add':
134
- console.log(`${C.green} + ${op.line}${C.reset}`);
135
- shown++;
136
- break;
137
- default:
138
- if (shown > 0) {
139
- // Only show context around changes
140
- console.log(`${C.gray} ${op.line}${C.reset}`);
141
- }
142
- }
143
- }
144
- console.log();
145
- }
146
-
147
- /**
148
- * Show preview for new file creation
149
- */
150
- function showNewFilePreview(filePath, content) {
151
- console.log(`\n${C.bold}${C.cyan} New file: ${filePath}${C.reset}`);
152
- const lines = content.split('\n');
153
- const preview = lines.slice(0, 20);
154
- for (const line of preview) {
155
- console.log(`${C.green} + ${line}${C.reset}`);
156
- }
157
- if (lines.length > 20) {
158
- console.log(`${C.gray} ...+${lines.length - 20} more lines${C.reset}`);
159
- }
160
- console.log();
161
- }
162
-
163
- /**
164
- * Confirm file change (edit or write)
165
- */
166
- async function confirmFileChange(action) {
167
- if (getAutoConfirm()) return true;
168
- return confirm(` ${action}?`);
169
- }
170
-
171
- /**
172
- * Show side-by-side diff view for two texts
173
- * @param {string} filePath
174
- * @param {string} oldText
175
- * @param {string} newText
176
- * @param {number} width - total terminal width (default: 80)
177
- */
178
- function showSideBySideDiff(filePath, oldText, newText, width) {
179
- const termWidth = width || process.stdout.columns || 80;
180
- const colWidth = Math.floor((termWidth - 3) / 2); // 3 for separator "│"
181
-
182
- console.log(`\n${C.bold}${C.cyan} Side-by-side: ${filePath}${C.reset}`);
183
- console.log(` ${C.dim}${'─'.repeat(colWidth)}┬${'─'.repeat(colWidth)}${C.reset}`);
184
-
185
- const ops = diffLines(oldText, newText);
186
-
187
- // Pair up removals and additions
188
- const pairs = [];
189
- let i = 0;
190
- while (i < ops.length) {
191
- if (ops[i].type === 'same') {
192
- pairs.push({ left: ops[i].line, right: ops[i].line, type: 'same' });
193
- i++;
194
- } else if (ops[i].type === 'remove') {
195
- // Collect consecutive removes, then matching adds
196
- const removes = [];
197
- while (i < ops.length && ops[i].type === 'remove') {
198
- removes.push(ops[i].line);
199
- i++;
200
- }
201
- const adds = [];
202
- while (i < ops.length && ops[i].type === 'add') {
203
- adds.push(ops[i].line);
204
- i++;
205
- }
206
- const maxLen = Math.max(removes.length, adds.length);
207
- for (let j = 0; j < maxLen; j++) {
208
- pairs.push({
209
- left: j < removes.length ? removes[j] : '',
210
- right: j < adds.length ? adds[j] : '',
211
- type: 'changed',
212
- });
213
- }
214
- } else if (ops[i].type === 'add') {
215
- pairs.push({ left: '', right: ops[i].line, type: 'changed' });
216
- i++;
217
- }
218
- }
219
-
220
- // Show max 40 pairs around changes
221
- const changedIdxs = pairs.map((p, idx) => p.type !== 'same' ? idx : -1).filter(x => x >= 0);
222
- if (changedIdxs.length === 0) {
223
- console.log(` ${C.gray}(no changes)${C.reset}`);
224
- return;
225
- }
226
-
227
- const showFrom = Math.max(0, changedIdxs[0] - 2);
228
- const showTo = Math.min(pairs.length, changedIdxs[changedIdxs.length - 1] + 3);
229
-
230
- const pad = (s, w) => {
231
- const visible = s.replace(/\x1b\[[0-9;]*m/g, '');
232
- return visible.length >= w ? s.substring(0, w) : s + ' '.repeat(w - visible.length);
233
- };
234
-
235
- if (showFrom > 0) console.log(` ${C.dim}${'·'.repeat(colWidth)}┊${'·'.repeat(colWidth)}${C.reset}`);
236
-
237
- for (let k = showFrom; k < showTo; k++) {
238
- const p = pairs[k];
239
- if (p.type === 'same') {
240
- console.log(` ${C.gray}${pad(p.left, colWidth)}${C.reset}│${C.gray}${pad(p.right, colWidth)}${C.reset}`);
241
- } else {
242
- const leftCol = p.left ? `${C.red}${pad(p.left, colWidth)}${C.reset}` : `${pad('', colWidth)}`;
243
- const rightCol = p.right ? `${C.green}${pad(p.right, colWidth)}${C.reset}` : `${pad('', colWidth)}`;
244
- console.log(` ${leftCol}│${rightCol}`);
245
- }
246
- }
247
-
248
- if (showTo < pairs.length) console.log(` ${C.dim}${'·'.repeat(colWidth)}┊${'·'.repeat(colWidth)}${C.reset}`);
249
- console.log(` ${C.dim}${'─'.repeat(colWidth)}┴${'─'.repeat(colWidth)}${C.reset}\n`);
250
- }
251
-
252
- /**
253
- * Claude Code-style diff display
254
- * Header: ⏺ Update(relPath) or ⏺ Create(relPath)
255
- * Summary: ⎿ Added N lines, removed M lines
256
- * Numbered context/change lines with ··· hunk separators
257
- */
258
- function showClaudeDiff(filePath, oldContent, newContent, options = {}) {
259
- const label = options.label || 'Update';
260
- const contextSize = options.context || 3;
261
- const relPath = path.isAbsolute(filePath) ? path.relative(process.cwd(), filePath) : filePath;
262
-
263
- const ops = diffLines(oldContent, newContent);
264
-
265
- // Assign line numbers to each op
266
- let oldLine = 1, newLine = 1;
267
- for (const op of ops) {
268
- if (op.type === 'same') {
269
- op.oldLine = oldLine++;
270
- op.newLine = newLine++;
271
- } else if (op.type === 'remove') {
272
- op.oldLine = oldLine++;
273
- op.newLine = null;
274
- } else {
275
- op.oldLine = null;
276
- op.newLine = newLine++;
277
- }
278
- }
279
-
280
- // Count additions and removals
281
- let added = 0, removed = 0;
282
- for (const op of ops) {
283
- if (op.type === 'add') added++;
284
- else if (op.type === 'remove') removed++;
285
- }
286
-
287
- // Header
288
- console.log(`\n${C.cyan}⏺${C.reset} ${C.bold}${label}(${relPath})${C.reset}`);
289
-
290
- if (added === 0 && removed === 0) {
291
- console.log(` ${C.dim}⎿ (no changes)${C.reset}\n`);
292
- return;
293
- }
294
-
295
- // Summary
296
- const parts = [];
297
- if (added > 0) parts.push(`Added ${added} line${added !== 1 ? 's' : ''}`);
298
- if (removed > 0) parts.push(`removed ${removed} line${removed !== 1 ? 's' : ''}`);
299
- console.log(` ${C.dim}⎿ ${parts.join(', ')}${C.reset}`);
300
-
301
- // Build hunks: groups of changes with surrounding context
302
- const changeIndices = [];
303
- ops.forEach((op, i) => { if (op.type !== 'same') changeIndices.push(i); });
304
-
305
- const hunks = [];
306
- let hunkStart = null, hunkEnd = null;
307
- for (const ci of changeIndices) {
308
- const start = Math.max(0, ci - contextSize);
309
- const end = Math.min(ops.length - 1, ci + contextSize);
310
- if (hunkStart === null) {
311
- hunkStart = start;
312
- hunkEnd = end;
313
- } else if (start <= hunkEnd + 1) {
314
- hunkEnd = end;
315
- } else {
316
- hunks.push([hunkStart, hunkEnd]);
317
- hunkStart = start;
318
- hunkEnd = end;
319
- }
320
- }
321
- if (hunkStart !== null) hunks.push([hunkStart, hunkEnd]);
322
-
323
- const PAD = ' '; // 6-char left padding
324
-
325
- for (let h = 0; h < hunks.length; h++) {
326
- if (h > 0) console.log(`${PAD}${C.dim}···${C.reset}`);
327
- const [from, to] = hunks[h];
328
- for (let i = from; i <= to; i++) {
329
- const op = ops[i];
330
- const lineNum = op.newLine != null ? op.newLine : op.oldLine;
331
- const numStr = String(lineNum).padStart(4);
332
- if (op.type === 'remove') {
333
- console.log(`${PAD}${C.red}${numStr} -${op.line}${C.reset}`);
334
- } else if (op.type === 'add') {
335
- console.log(`${PAD}${C.green}${numStr} +${op.line}${C.reset}`);
336
- } else {
337
- console.log(`${PAD}${C.dim}${numStr}${C.reset} ${op.line}`);
338
- }
339
- }
340
- }
341
- console.log();
342
- }
343
-
344
- /**
345
- * Claude Code-style new file display
346
- */
347
- function showClaudeNewFile(filePath, content) {
348
- const relPath = path.isAbsolute(filePath) ? path.relative(process.cwd(), filePath) : filePath;
349
- const lines = content.split('\n');
350
-
351
- console.log(`\n${C.cyan}⏺${C.reset} ${C.bold}Create(${relPath})${C.reset}`);
352
- console.log(` ${C.dim}⎿ ${lines.length} line${lines.length !== 1 ? 's' : ''}${C.reset}`);
353
-
354
- const PAD = ' ';
355
- const show = Math.min(lines.length, 20);
356
- for (let i = 0; i < show; i++) {
357
- const numStr = String(i + 1).padStart(4);
358
- console.log(`${PAD}${C.green}${numStr} +${lines[i]}${C.reset}`);
359
- }
360
- if (lines.length > 20) {
361
- console.log(`${PAD}${C.dim} ...+${lines.length - 20} more lines${C.reset}`);
362
- }
363
- console.log();
364
- }
365
-
366
- module.exports = { diffLines, showEditDiff, showWriteDiff, showNewFilePreview, confirmFileChange, showSideBySideDiff, showClaudeDiff, showClaudeNewFile };