zames_pro 2.51.0 → 2.52.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 +22 -1
- package/dist/commands.js +21 -0
- package/dist/i18n.js +20 -0
- package/dist/index.js +37 -6
- package/dist/input.js +49 -0
- package/dist/sessions.js +26 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,6 +76,23 @@ While the agent is working you can keep typing: press Enter to queue a message
|
|
|
76
76
|
abort the current generation. This mirrors typing during generation on the
|
|
77
77
|
DeepSeek website.
|
|
78
78
|
|
|
79
|
+
### Input line
|
|
80
|
+
|
|
81
|
+
The prompt is a small line editor with persistent history:
|
|
82
|
+
|
|
83
|
+
- `↑` / `↓` — walk the message history (saved in `~/.zames/history.json`, so it
|
|
84
|
+
survives a restart); inside a multiline message the arrows move between lines.
|
|
85
|
+
- `Ctrl+R` — incremental reverse search over the history (bash-style): type to
|
|
86
|
+
filter, `Ctrl+R` for older matches, `Enter` to accept, `Esc` to cancel.
|
|
87
|
+
- `Ctrl+_` — undo the last edit in the input line (a fat-fingered `Ctrl+U` /
|
|
88
|
+
`Ctrl+K` is recoverable).
|
|
89
|
+
- `Ctrl+U` — clear the line, `Ctrl+K` — delete to end of line, `Ctrl+W` —
|
|
90
|
+
delete the word before the cursor, `Ctrl+←`/`Ctrl+→` — move by words.
|
|
91
|
+
- `\` + `Enter`, `Ctrl+J`, `Ctrl+Enter` or `Shift+Enter` — insert a newline.
|
|
92
|
+
- `/` + `Tab` — slash-command hints and completion.
|
|
93
|
+
|
|
94
|
+
Set `NO_COLOR=1` to disable colors (a calm default palette is used otherwise).
|
|
95
|
+
|
|
79
96
|
### Images and files
|
|
80
97
|
|
|
81
98
|
You can paste an image or a file into the input line (Ctrl+Shift+V / Shift+Insert
|
|
@@ -149,7 +166,11 @@ Codex CLI:
|
|
|
149
166
|
startup; relaunch with --dir to write there).
|
|
150
167
|
- /resume <n> (after /chats) and /resume-id <id> — open a chat and PRINT its
|
|
151
168
|
dialogue into the terminal, so the restored context is visible. Only the
|
|
152
|
-
last 20 messages are shown (`RESTORED_HISTORY_LIMIT`).
|
|
169
|
+
last 20 messages are shown (`RESTORED_HISTORY_LIMIT`). /last reopens the last
|
|
170
|
+
chat of the current directory with no lookup step; /sessions shows the saved
|
|
171
|
+
sessions with their relative age.
|
|
172
|
+
- /help <command> — the full description of a single command (e.g. `/help diff`)
|
|
173
|
+
instead of the whole list.
|
|
153
174
|
- /review [focus] [--staged] — ask the agent to review uncommitted changes
|
|
154
175
|
and report findings (no code changes).
|
|
155
176
|
- /compact — ask DeepSeek to compress the current chat into a handover
|
package/dist/commands.js
CHANGED
|
@@ -86,6 +86,27 @@ export function formatDuration(ms, units = EN_DURATION_UNITS) {
|
|
|
86
86
|
return m + units.m + ' ' + pad(s) + units.s;
|
|
87
87
|
return s + units.s;
|
|
88
88
|
}
|
|
89
|
+
// Short relative age ("just now", "5m ago", "3h ago", "2d ago") for the
|
|
90
|
+
// /sessions and /chats lists. The operator cares about RECENCY, not a raw
|
|
91
|
+
// ISO timestamp, when picking a session to restore.
|
|
92
|
+
export function formatRelativeTime(iso, t = translate('en'), now = Date.now()) {
|
|
93
|
+
if (!iso)
|
|
94
|
+
return '';
|
|
95
|
+
const then = Date.parse(iso);
|
|
96
|
+
if (!Number.isFinite(then))
|
|
97
|
+
return '';
|
|
98
|
+
const diff = Math.max(0, now - then);
|
|
99
|
+
const min = Math.floor(diff / 60_000);
|
|
100
|
+
if (min < 1)
|
|
101
|
+
return t('time.now');
|
|
102
|
+
if (min < 60)
|
|
103
|
+
return t('time.min_ago', { n: String(min) });
|
|
104
|
+
const hours = Math.floor(min / 60);
|
|
105
|
+
if (hours < 24)
|
|
106
|
+
return t('time.hour_ago', { n: String(hours) });
|
|
107
|
+
const days = Math.floor(hours / 24);
|
|
108
|
+
return t('time.day_ago', { n: String(days) });
|
|
109
|
+
}
|
|
89
110
|
export function renderCost(stats, transcriptFile, tokenUsage = null, t = translate('en')) {
|
|
90
111
|
const lines = [];
|
|
91
112
|
lines.push(t('cost.title'));
|
package/dist/i18n.js
CHANGED
|
@@ -103,6 +103,10 @@ const CATALOG = {
|
|
|
103
103
|
ru: 'Ctrl+R поиск по истории (обратный)',
|
|
104
104
|
en: 'Ctrl+R reverse-search the history',
|
|
105
105
|
},
|
|
106
|
+
'help.key.undo': {
|
|
107
|
+
ru: 'Ctrl+_ отменить правку в строке ввода',
|
|
108
|
+
en: 'Ctrl+_ undo the last edit in the input line',
|
|
109
|
+
},
|
|
106
110
|
'help.key.words': {
|
|
107
111
|
ru: 'Ctrl+← / Ctrl+→ перемещение по словам',
|
|
108
112
|
en: 'Ctrl+← / Ctrl+→ move by words',
|
|
@@ -363,6 +367,10 @@ const CATALOG = {
|
|
|
363
367
|
ru: '/debug-dom сохранить HTML страницы (для отладки)',
|
|
364
368
|
en: '/debug-dom dump page HTML (debug)',
|
|
365
369
|
},
|
|
370
|
+
'help.no_topic': {
|
|
371
|
+
ru: 'Нет команды по запросу «{v}». Попробуй /help без аргумента.',
|
|
372
|
+
en: 'No command matches "{v}". Try /help without arguments.',
|
|
373
|
+
},
|
|
366
374
|
'help.cmd.help': {
|
|
367
375
|
ru: '/help, help справка',
|
|
368
376
|
en: '/help, help this help',
|
|
@@ -455,7 +463,15 @@ const CATALOG = {
|
|
|
455
463
|
'help.files.profile': { ru: 'Профиль:', en: 'Profile:' },
|
|
456
464
|
'help.files.snapshots': { ru: 'Снапшоты:', en: 'Snapshots:' },
|
|
457
465
|
'help.files.tmp': { ru: 'Временные:', en: 'Temp:' },
|
|
466
|
+
'help.files.tmp_note': {
|
|
467
|
+
ru: '(в .gitignore, очищается при старте)',
|
|
468
|
+
en: '(.gitignore, cleaned on start)',
|
|
469
|
+
},
|
|
458
470
|
'help.files.config': { ru: 'Конфиг:', en: 'Config:' },
|
|
471
|
+
'help.toggles_legend': {
|
|
472
|
+
ru: 'Индикаторы в статус-строке: 🧠 — глубокое размышление, 🌐 — поиск в интернете (видны, когда включены).',
|
|
473
|
+
en: 'Status-line icons: 🧠 — Deep thinking, 🌐 — web search (shown when enabled).',
|
|
474
|
+
},
|
|
459
475
|
// ---------- spinner ----------
|
|
460
476
|
'spinner.phrases': {
|
|
461
477
|
ru: 'Анализирую запрос…|Изучаю контекст…|Планирую решение…|Обрабатываю данные…|Формулирую ответ…|Проверяю детали…|Собираю информацию…|Обдумываю варианты…|Структурирую мысли…|Готовлю решение…|Анализирую код…|Уточняю детали…|Выполняю вычисления…|Систематизирую данные…|Прорабатываю задачу…|Проверяю логику…|Оптимизирую подход…|Формирую выводы…',
|
|
@@ -1080,6 +1096,10 @@ const CATALOG = {
|
|
|
1080
1096
|
},
|
|
1081
1097
|
'chats.history_you': { ru: 'Вы', en: 'You' },
|
|
1082
1098
|
'chats.history_agent': { ru: 'Агент', en: 'Agent' },
|
|
1099
|
+
'time.now': { ru: 'только что', en: 'just now' },
|
|
1100
|
+
'time.min_ago': { ru: '{n} мин назад', en: '{n}m ago' },
|
|
1101
|
+
'time.hour_ago': { ru: '{n} ч назад', en: '{n}h ago' },
|
|
1102
|
+
'time.day_ago': { ru: '{n} дн назад', en: '{n}d ago' },
|
|
1083
1103
|
'sessions.dir': { ru: 'Папка сессий: {v}', en: 'Sessions dir: {v}' },
|
|
1084
1104
|
'sessions.none': {
|
|
1085
1105
|
ru: 'Сохранённых сессий нет. Они появятся после первой задачи/чата.',
|
package/dist/index.js
CHANGED
|
@@ -17,12 +17,12 @@ import { translate, normalizeLocale, localeDisplayName, isLocale, } from './i18n
|
|
|
17
17
|
import { Transcript } from './transcript.js';
|
|
18
18
|
import { UndoStore } from './undo.js';
|
|
19
19
|
import { selfReview, selfDiff, selfApply, selfList } from './self-review.js';
|
|
20
|
-
import { formatDiff, diffGitArgs, parseTranscript, summarizeTranscript, renderCost, formatExport, defaultExportPath, renderDoctor, renderPermissions, resolveExtraDir, buildReviewPrompt, formatDuration, trimRestoredMessages, RESTORED_HISTORY_LIMIT, mergeMessages, hasQueuedJob, isSlashCommand, parseQueueCommand, parseLiveToggle, parseGoalCommand, isLiveConfigCommand, withGoal, formatQueueList, ctrlCEscalation, } from './commands.js';
|
|
20
|
+
import { formatDiff, diffGitArgs, parseTranscript, summarizeTranscript, renderCost, formatExport, defaultExportPath, renderDoctor, renderPermissions, resolveExtraDir, buildReviewPrompt, formatDuration, formatRelativeTime, trimRestoredMessages, RESTORED_HISTORY_LIMIT, mergeMessages, hasQueuedJob, isSlashCommand, parseQueueCommand, parseLiveToggle, parseGoalCommand, isLiveConfigCommand, withGoal, formatQueueList, ctrlCEscalation, } from './commands.js';
|
|
21
21
|
import { performCompact } from './compact.js';
|
|
22
22
|
import { Scheduler, parseInterval, formatInterval, formatJobLine, parseCron, } from './scheduler.js';
|
|
23
23
|
import { renderMarkdown } from './markdown.js';
|
|
24
24
|
import { closeWeb } from './web.js';
|
|
25
|
-
import { saveSession, loadLastSession, readSession, listSessions, sessionsDir, } from './sessions.js';
|
|
25
|
+
import { saveSession, loadLastSession, readSession, listSessions, sessionsDir, loadHistory, saveHistory, } from './sessions.js';
|
|
26
26
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
27
27
|
const NL = String.fromCharCode(10);
|
|
28
28
|
// Floor for a /loop interval. A loop faster than the send throttle (15s)
|
|
@@ -299,6 +299,7 @@ ${theme.bold(t('help.while_working'))}
|
|
|
299
299
|
${t('help.key.history')}
|
|
300
300
|
${t('help.key.search')}
|
|
301
301
|
${t('help.key.words')}
|
|
302
|
+
${t('help.key.undo')}
|
|
302
303
|
${t('help.key.slash')}
|
|
303
304
|
${t('help.key.newline')}
|
|
304
305
|
${t('help.key.backslash')}
|
|
@@ -357,9 +358,11 @@ ${theme.bold(t('help.files'))}
|
|
|
357
358
|
${t('help.files.sessions')} ${sessionsDir()}
|
|
358
359
|
${t('help.files.profile')} ~/.zames/profile
|
|
359
360
|
${t('help.files.snapshots')} ~/.zames/snapshots
|
|
360
|
-
${t('help.files.tmp')} <project>/tmp (.
|
|
361
|
+
${t('help.files.tmp')} <project>/tmp ${t('help.files.tmp_note')}
|
|
361
362
|
${t('help.files.config')} ${CONFIG_PATHS.HOME_CONFIG}
|
|
362
363
|
${CONFIG_PATHS.PROJECT_CONFIG}
|
|
364
|
+
|
|
365
|
+
${theme.dim(t('help.toggles_legend'))}
|
|
363
366
|
`);
|
|
364
367
|
}
|
|
365
368
|
function dirLabel(p) {
|
|
@@ -1544,6 +1547,11 @@ async function main() {
|
|
|
1544
1547
|
});
|
|
1545
1548
|
editor = ed;
|
|
1546
1549
|
ed.setTmpDir(TMP_DIR);
|
|
1550
|
+
// Restore the input history from the previous runs and persist every new
|
|
1551
|
+
// entry, so Ctrl+R / arrows can recall older messages after a restart.
|
|
1552
|
+
ed.history = loadHistory();
|
|
1553
|
+
ed._histIndex = ed.history.length;
|
|
1554
|
+
ed.onHistoryChange = (h) => saveHistory(h);
|
|
1547
1555
|
// The token context right-aligned on the status line (above the input).
|
|
1548
1556
|
// The editor pulls the number on every render, so it follows the live
|
|
1549
1557
|
// DeepSeek counter (accumulated_token_usage) without a polling timer.
|
|
@@ -2115,8 +2123,29 @@ async function main() {
|
|
|
2115
2123
|
const lower = trimmed.toLowerCase();
|
|
2116
2124
|
if (['/exit', '/quit', 'exit', 'quit'].includes(lower))
|
|
2117
2125
|
break;
|
|
2118
|
-
if (lower === '/help' ||
|
|
2119
|
-
|
|
2126
|
+
if (lower === '/help' ||
|
|
2127
|
+
lower === 'help' ||
|
|
2128
|
+
lower.startsWith('/help ') ||
|
|
2129
|
+
lower.startsWith('help ')) {
|
|
2130
|
+
const topic = trimmed.replace(/^\/?help\s*/i, '').trim();
|
|
2131
|
+
if (!topic) {
|
|
2132
|
+
printHelp();
|
|
2133
|
+
}
|
|
2134
|
+
else {
|
|
2135
|
+
// /help <command> — print the full description of ONE command instead
|
|
2136
|
+
// of making the operator scroll the whole list.
|
|
2137
|
+
const q = '/' + topic.replace(/^\//, '').toLowerCase();
|
|
2138
|
+
const match = buildSlashCommands().filter((c) => c.name.toLowerCase() === q ||
|
|
2139
|
+
c.name.toLowerCase().startsWith(q + ' '));
|
|
2140
|
+
if (!match.length) {
|
|
2141
|
+
console.error(theme.warn(t('help.no_topic', { v: topic })));
|
|
2142
|
+
}
|
|
2143
|
+
else {
|
|
2144
|
+
for (const c of match) {
|
|
2145
|
+
console.log(theme.assistant(c.name) + theme.dim(' ' + c.description));
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2120
2149
|
continue;
|
|
2121
2150
|
}
|
|
2122
2151
|
if (['/new', '/clear', 'new'].includes(lower)) {
|
|
@@ -2414,7 +2443,9 @@ async function main() {
|
|
|
2414
2443
|
const mark = s.id === currentChatId ? theme.user(' *') : '';
|
|
2415
2444
|
const title = s.title ? ` ${s.title}` : '';
|
|
2416
2445
|
const wd = s.workdir ? theme.dim(` [${dirLabel(s.workdir)}]`) : '';
|
|
2417
|
-
|
|
2446
|
+
const age = formatRelativeTime(s.updatedAt, t);
|
|
2447
|
+
const ageText = age ? theme.dim(' ' + age) : '';
|
|
2448
|
+
console.log(` ${theme.user(n)}. ${s.id.slice(0, 8)}…${title}${wd}${ageText}${mark}`);
|
|
2418
2449
|
});
|
|
2419
2450
|
console.log(theme.system(t('sessions.restore_hint')));
|
|
2420
2451
|
}
|
package/dist/input.js
CHANGED
|
@@ -356,6 +356,16 @@ export class LineEditor {
|
|
|
356
356
|
_searchQuery;
|
|
357
357
|
_searchIndex;
|
|
358
358
|
_searchSavedBuf;
|
|
359
|
+
// Called after every submit with the new full history, so the caller can
|
|
360
|
+
// persist it (the history otherwise dies with the process).
|
|
361
|
+
onHistoryChange;
|
|
362
|
+
// Snapshots of {buf, cursor} taken before each edit, so Ctrl+_ can undo a
|
|
363
|
+
// slip (a fat-fingered Ctrl+U / Ctrl+K used to wipe the whole line with no
|
|
364
|
+
// way back). Capped so a long session cannot grow it without bound.
|
|
365
|
+
_undoStack;
|
|
366
|
+
// True while a run of plain typing is in progress, so one undo snapshot
|
|
367
|
+
// covers the whole run instead of every character.
|
|
368
|
+
_typingRun;
|
|
359
369
|
// Slash-command hints (shown when you type «/»).
|
|
360
370
|
slashCommands;
|
|
361
371
|
_suggestCount;
|
|
@@ -462,6 +472,9 @@ export class LineEditor {
|
|
|
462
472
|
this._searchQuery = '';
|
|
463
473
|
this._searchIndex = -1;
|
|
464
474
|
this._searchSavedBuf = '';
|
|
475
|
+
this.onHistoryChange = null;
|
|
476
|
+
this._undoStack = [];
|
|
477
|
+
this._typingRun = false;
|
|
465
478
|
this.slashCommands = commands;
|
|
466
479
|
this._suggestCount = 0;
|
|
467
480
|
this.pastes = [];
|
|
@@ -1280,6 +1293,9 @@ export class LineEditor {
|
|
|
1280
1293
|
// Add to history only non-empty messages that don't duplicate the previous one.
|
|
1281
1294
|
if (text.trim() && this.history[this.history.length - 1] !== text) {
|
|
1282
1295
|
this.history.push(text);
|
|
1296
|
+
// Persist the new entry: the in-memory history dies with the process.
|
|
1297
|
+
if (this.onHistoryChange)
|
|
1298
|
+
this.onHistoryChange(this.history.slice());
|
|
1283
1299
|
}
|
|
1284
1300
|
this._histIndex = this.history.length;
|
|
1285
1301
|
this._histDraft = '';
|
|
@@ -1304,6 +1320,26 @@ export class LineEditor {
|
|
|
1304
1320
|
if (this.busy)
|
|
1305
1321
|
this._startThinking();
|
|
1306
1322
|
}
|
|
1323
|
+
// Record the current buffer for Ctrl+_ undo. `coalesce` keeps one snapshot
|
|
1324
|
+
// for a whole run of plain typing (so undo removes the run, not one char) —
|
|
1325
|
+
// the run ends as soon as a non-typing key is handled.
|
|
1326
|
+
_pushUndo(coalesce = false) {
|
|
1327
|
+
if (coalesce && this._typingRun)
|
|
1328
|
+
return;
|
|
1329
|
+
this._undoStack.push({ buf: this.buf, cursor: this.cursor });
|
|
1330
|
+
if (this._undoStack.length > 200)
|
|
1331
|
+
this._undoStack.shift();
|
|
1332
|
+
this._typingRun = coalesce;
|
|
1333
|
+
}
|
|
1334
|
+
// Ctrl+_ — restore the buffer to before the last edit.
|
|
1335
|
+
_undoEdit() {
|
|
1336
|
+
const prev = this._undoStack.pop();
|
|
1337
|
+
if (!prev)
|
|
1338
|
+
return;
|
|
1339
|
+
this.buf = prev.buf;
|
|
1340
|
+
this.cursor = Math.min(prev.cursor, Array.from(prev.buf).length);
|
|
1341
|
+
this._typingRun = false;
|
|
1342
|
+
}
|
|
1307
1343
|
_insert(text) {
|
|
1308
1344
|
const chars = Array.from(this.buf);
|
|
1309
1345
|
const ins = Array.from(String(text));
|
|
@@ -1774,12 +1810,19 @@ export class LineEditor {
|
|
|
1774
1810
|
continue;
|
|
1775
1811
|
}
|
|
1776
1812
|
if (code === 21) {
|
|
1813
|
+
this._pushUndo();
|
|
1777
1814
|
this.buf = '';
|
|
1778
1815
|
this.cursor = 0;
|
|
1779
1816
|
this.pastes = [];
|
|
1780
1817
|
this._renderInputOnly();
|
|
1781
1818
|
continue;
|
|
1782
1819
|
}
|
|
1820
|
+
if (code === 31) {
|
|
1821
|
+
// Ctrl+_ — undo the last edit in the input line.
|
|
1822
|
+
this._undoEdit();
|
|
1823
|
+
this._renderInputOnly();
|
|
1824
|
+
continue;
|
|
1825
|
+
}
|
|
1783
1826
|
if (code === 22) {
|
|
1784
1827
|
// Ctrl+V: terminals rarely deliver an image as text here, so we try the
|
|
1785
1828
|
// OS clipboard first; if there is no image we fall back to reading the
|
|
@@ -1793,12 +1836,14 @@ export class LineEditor {
|
|
|
1793
1836
|
continue;
|
|
1794
1837
|
}
|
|
1795
1838
|
if (code === 23) {
|
|
1839
|
+
this._pushUndo();
|
|
1796
1840
|
this._deleteWordLeft();
|
|
1797
1841
|
this._renderInputOnly();
|
|
1798
1842
|
continue;
|
|
1799
1843
|
}
|
|
1800
1844
|
if (code === 11) {
|
|
1801
1845
|
// Ctrl+K — delete from the cursor to the end of the line.
|
|
1846
|
+
this._pushUndo();
|
|
1802
1847
|
const arr = Array.from(this.buf);
|
|
1803
1848
|
let e = this.cursor;
|
|
1804
1849
|
while (e < arr.length && arr[e] !== NL)
|
|
@@ -1809,6 +1854,7 @@ export class LineEditor {
|
|
|
1809
1854
|
continue;
|
|
1810
1855
|
}
|
|
1811
1856
|
if (code === 127 || code === 8) {
|
|
1857
|
+
this._pushUndo(true);
|
|
1812
1858
|
this._backspace();
|
|
1813
1859
|
this._renderInputOnly();
|
|
1814
1860
|
continue;
|
|
@@ -1872,6 +1918,7 @@ export class LineEditor {
|
|
|
1872
1918
|
continue;
|
|
1873
1919
|
}
|
|
1874
1920
|
if (s.startsWith('[3~')) {
|
|
1921
|
+
this._pushUndo(true);
|
|
1875
1922
|
this._delete();
|
|
1876
1923
|
s = s.slice(3);
|
|
1877
1924
|
this._renderInputOnly();
|
|
@@ -1898,6 +1945,8 @@ export class LineEditor {
|
|
|
1898
1945
|
}
|
|
1899
1946
|
if (code < 32)
|
|
1900
1947
|
continue;
|
|
1948
|
+
// Coalesce a run of typing into ONE undo snapshot.
|
|
1949
|
+
this._pushUndo(true);
|
|
1901
1950
|
this._insert(ch);
|
|
1902
1951
|
this._renderInputOnly();
|
|
1903
1952
|
}
|
package/dist/sessions.js
CHANGED
|
@@ -132,3 +132,29 @@ export function listSessions() {
|
|
|
132
132
|
export function sessionsDir() {
|
|
133
133
|
return SESSIONS_DIR;
|
|
134
134
|
}
|
|
135
|
+
// ---------- input history ----------
|
|
136
|
+
// The LineEditor history otherwise dies with the process (it was in-memory
|
|
137
|
+
// only), so the arrow keys / Ctrl+R could not recall anything from a previous
|
|
138
|
+
// run. We persist it in ~/.zames/history.json, capped so the file cannot grow
|
|
139
|
+
// without bound.
|
|
140
|
+
const HISTORY_FILE = path.join(os.homedir(), '.zames', 'history.json');
|
|
141
|
+
const HISTORY_LIMIT = 500;
|
|
142
|
+
export function loadHistory() {
|
|
143
|
+
try {
|
|
144
|
+
const raw = JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf-8'));
|
|
145
|
+
if (!Array.isArray(raw))
|
|
146
|
+
return [];
|
|
147
|
+
return raw.filter((x) => typeof x === 'string');
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export function saveHistory(history) {
|
|
154
|
+
try {
|
|
155
|
+
ensureDir();
|
|
156
|
+
const trimmed = history.slice(-HISTORY_LIMIT);
|
|
157
|
+
atomicWriteJson(HISTORY_FILE, trimmed);
|
|
158
|
+
}
|
|
159
|
+
catch { }
|
|
160
|
+
}
|