zames_pro 1.2.0 → 1.2.1
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/package.json +1 -1
- package/src/agent-loop.js +75 -16
- package/src/browser.js +1 -1
- package/src/config.js +1 -1
- package/src/index.js +103 -49
- package/src/sessions.js +110 -0
package/package.json
CHANGED
package/src/agent-loop.js
CHANGED
|
@@ -80,16 +80,21 @@ export async function runAgentLoop({
|
|
|
80
80
|
// (есть tool/invoke/parameter, но JSON/XML битый) — это почти всегда
|
|
81
81
|
// ошибка формата. Не считаем её финальным ответом (иначе агент молча
|
|
82
82
|
// остановится), а просим модель переотправить вызов корректно.
|
|
83
|
-
const looksLikeToolCall =
|
|
83
|
+
const looksLikeToolCall =
|
|
84
|
+
/("tool"\s*:|\btool_calls?\b|\binvoke\b|\bparameter\b|DSML|function_call)/i.test(
|
|
85
|
+
rawResponse,
|
|
86
|
+
) || /\{\s*"?(tool|name)"?\s*:/.test(rawResponse)
|
|
84
87
|
if (looksLikeToolCall && malformedRetries < MAX_MALFORMED_RETRIES) {
|
|
85
88
|
malformedRetries++
|
|
86
89
|
transcript?.log("malformed_toolcall", { attempt: malformedRetries, response: rawResponse })
|
|
87
90
|
if (debugLog) {
|
|
88
91
|
console.error("внимание: ответ похож на tool-call, но не распознан (попытка " + malformedRetries + "/" + MAX_MALFORMED_RETRIES + ")")
|
|
89
92
|
}
|
|
90
|
-
message =
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
message =
|
|
94
|
+
'Твой предыдущий ответ не распознан как вызов инструмента. ' +
|
|
95
|
+
'Ответь РОВНО одним JSON-объектом вызова инструмента, без текста до и после. ' +
|
|
96
|
+
'НЕ используй XML/DSML-теги — только JSON. ' +
|
|
97
|
+
'Например: {"tool": "Read", "args": {"path": "src/index.js"}}'
|
|
93
98
|
continue
|
|
94
99
|
}
|
|
95
100
|
|
|
@@ -162,6 +167,27 @@ export async function runAgentLoop({
|
|
|
162
167
|
|
|
163
168
|
// ============ JSON parsing ============
|
|
164
169
|
|
|
170
|
+
function repairRawControlChars(str) {
|
|
171
|
+
let out = ''
|
|
172
|
+
let inString = false
|
|
173
|
+
let escape = false
|
|
174
|
+
for (let i = 0; i < str.length; i++) {
|
|
175
|
+
const c = str[i]
|
|
176
|
+
if (escape) { out += c; escape = false; continue }
|
|
177
|
+
if (c === '\\') { out += c; escape = true; continue }
|
|
178
|
+
if (c === '"') { inString = !inString; out += c; continue }
|
|
179
|
+
if (inString) {
|
|
180
|
+
if (c === '\n') { out += '\\n'; continue }
|
|
181
|
+
if (c === '\r') { out += '\\r'; continue }
|
|
182
|
+
if (c === '\t') { out += '\\t'; continue }
|
|
183
|
+
const code = c.charCodeAt(0)
|
|
184
|
+
if (code < 0x20) { out += '\\u' + code.toString(16).padStart(4, '0'); continue }
|
|
185
|
+
}
|
|
186
|
+
out += c
|
|
187
|
+
}
|
|
188
|
+
return out
|
|
189
|
+
}
|
|
190
|
+
|
|
165
191
|
function tryParse(str) {
|
|
166
192
|
try {
|
|
167
193
|
const obj = JSON.parse(str)
|
|
@@ -417,17 +443,34 @@ function parseToolCallPermissive(text) {
|
|
|
417
443
|
// а не первый попавшийся '}'. Иначе вложенные объекты/массивы или
|
|
418
444
|
// фигурные скобки внутри строк ломают разбор.
|
|
419
445
|
const endIdx = findMatching(text, openIdx, '{', '}')
|
|
420
|
-
if (endIdx === -1) return null
|
|
421
446
|
|
|
422
|
-
const argsStr = text.slice(openIdx, endIdx + 1)
|
|
423
447
|
if (tool === 'Edit') {
|
|
424
|
-
|
|
425
|
-
|
|
448
|
+
// Edit-аргументы часто содержат сырые кавычки и переводы строк, из-за
|
|
449
|
+
// чего балансировка скобок сбоит. parseEditArgs рассчитан ровно на
|
|
450
|
+
// такой случай, поэтому пробуем его и на «хвосте» до конца текста,
|
|
451
|
+
// а не только на сбалансированном фрагменте.
|
|
452
|
+
const balanced = endIdx === -1 ? null : text.slice(openIdx, endIdx + 1)
|
|
453
|
+
const tailToEnd = text.slice(openIdx)
|
|
454
|
+
for (const frag of [balanced, tailToEnd]) {
|
|
455
|
+
if (!frag) continue
|
|
456
|
+
const e = parseEditArgs(frag)
|
|
457
|
+
if (e) return { tool, args: e }
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Балансировка скобок сбоит, если в строковых значениях есть сырые
|
|
462
|
+
// кавычки/скобки (частый случай для Bash/Write с кодом внутри). Тогда
|
|
463
|
+
// пробуем разобрать args из «хвоста» до конца текста и жадным парсером.
|
|
464
|
+
const frags = []
|
|
465
|
+
if (endIdx !== -1) frags.push(text.slice(openIdx, endIdx + 1))
|
|
466
|
+
frags.push(text.slice(openIdx))
|
|
467
|
+
|
|
468
|
+
for (const argsStr of frags) {
|
|
469
|
+
const args = parseArgsPermissive(argsStr)
|
|
470
|
+
if (args) return { tool, args }
|
|
471
|
+
const greedy = parseArgsGreedy(argsStr)
|
|
472
|
+
if (greedy) return { tool, args: greedy }
|
|
426
473
|
}
|
|
427
|
-
const args = parseArgsPermissive(argsStr)
|
|
428
|
-
if (args) return { tool, args }
|
|
429
|
-
const greedy = parseArgsGreedy(argsStr)
|
|
430
|
-
if (greedy) return { tool, args: greedy }
|
|
431
474
|
return null
|
|
432
475
|
}
|
|
433
476
|
|
|
@@ -511,7 +554,7 @@ function findMatching(text, openIdx, openCh, closeCh) {
|
|
|
511
554
|
return -1
|
|
512
555
|
}
|
|
513
556
|
|
|
514
|
-
function parseToolCall(text) {
|
|
557
|
+
export function parseToolCall(text) {
|
|
515
558
|
if (!text || typeof text !== 'string') return null
|
|
516
559
|
|
|
517
560
|
let cleaned = text.trim()
|
|
@@ -539,9 +582,20 @@ function parseToolCall(text) {
|
|
|
539
582
|
const repaired = raw.replace(/\\(?!["\\/bfnrtu])/g, '\\\\')
|
|
540
583
|
const second = tryParse(repaired)
|
|
541
584
|
if (second) return second
|
|
585
|
+
|
|
586
|
+
// Сырые переводы строк/табы внутри строковых значений (old_string,
|
|
587
|
+
// new_string, content) делают JSON невалидным. Экранируем их и
|
|
588
|
+
// пробуем снова, иначе многострочный вызов не распознаётся.
|
|
589
|
+
const ctrl = repairRawControlChars(raw)
|
|
590
|
+
const third = tryParse(ctrl)
|
|
591
|
+
if (third) return third
|
|
592
|
+
const ctrlArr = tryParseArray(ctrl)
|
|
593
|
+
if (ctrlArr) return ctrlArr
|
|
542
594
|
}
|
|
543
595
|
|
|
544
|
-
const permissive =
|
|
596
|
+
const permissive =
|
|
597
|
+
parseToolCallPermissive(cleaned) ||
|
|
598
|
+
parseToolCallPermissive(repairRawControlChars(cleaned))
|
|
545
599
|
if (permissive) return { ...permissive, _permissive: true }
|
|
546
600
|
|
|
547
601
|
// Fallback: модель могла обернуть JSON в прозу и/или добавить мусорные
|
|
@@ -550,8 +604,13 @@ function parseToolCall(text) {
|
|
|
550
604
|
const toolIdx = cleaned.search(/["']?tool["']?\s:/)
|
|
551
605
|
if (toolIdx > 0) {
|
|
552
606
|
let tail = cleaned.slice(toolIdx)
|
|
553
|
-
|
|
554
|
-
|
|
607
|
+
// Регексп обязан матчить многосимвольные теги (<|DSML|invoke ...>),
|
|
608
|
+
// поэтому <[^>]*>, а не <[^>]> — иначе DSML-хвост не срезается,
|
|
609
|
+
// permissive-разбор падает и агент молча останавливается.
|
|
610
|
+
tail = tail.replace(/<[^>]*>.*$/s, '').trim()
|
|
611
|
+
const tailPermissive =
|
|
612
|
+
parseToolCallPermissive('{"' + tail) ||
|
|
613
|
+
parseToolCallPermissive('{"' + repairRawControlChars(tail))
|
|
555
614
|
if (tailPermissive) return { ...tailPermissive, _permissive: true }
|
|
556
615
|
}
|
|
557
616
|
|
package/src/browser.js
CHANGED
package/src/config.js
CHANGED
package/src/index.js
CHANGED
|
@@ -14,6 +14,12 @@ import { Transcript } from './transcript.js'
|
|
|
14
14
|
import { UndoStore } from './undo.js'
|
|
15
15
|
import { selfReview, selfDiff, selfApply, selfList } from './self-review.js'
|
|
16
16
|
import { closeWeb } from './web.js'
|
|
17
|
+
import {
|
|
18
|
+
saveSession,
|
|
19
|
+
loadLastSession,
|
|
20
|
+
listSessions,
|
|
21
|
+
sessionsDir,
|
|
22
|
+
} from './sessions.js'
|
|
17
23
|
|
|
18
24
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
19
25
|
|
|
@@ -60,40 +66,26 @@ const chatIdArg = getArg('--chat', null)
|
|
|
60
66
|
// переотправляется (он уже есть в начале чата). Флаг --resend-prompt
|
|
61
67
|
// заставляет дослать его заново — например, если промпт обновился.
|
|
62
68
|
const resendPrompt = hasFlag('--resend-prompt')
|
|
63
|
-
// --new-chat: намеренно начать с чистого чата, игнорируя
|
|
64
|
-
// --resume-last:
|
|
65
|
-
// По умолчанию
|
|
66
|
-
|
|
69
|
+
// --new-chat: намеренно начать с чистого чата, игнорируя сохранённую сессию.
|
|
70
|
+
// --resume-last: оставлен для совместимости (восстановление и так по умолчанию).
|
|
71
|
+
// По умолчанию последняя сессия для рабочей директории восстанавливается.
|
|
72
|
+
const newChatFlag = hasFlag('--new-chat')
|
|
67
73
|
const resumeLastFlag = hasFlag('--resume-last')
|
|
68
74
|
|
|
69
|
-
// ----------
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
// ---------- session persistence ----------
|
|
76
|
+
// Сессии (чаты DeepSeek) храним в ~/.zames/.sessions, чтобы они переживали
|
|
77
|
+
// перезапуск процесса. При старте из той же рабочей директории последняя
|
|
78
|
+
// сессия восстанавливается автоматически (если не передан --new-chat).
|
|
79
|
+
// Раньше здесь был один файл last-chat.json, который терялся при смене
|
|
80
|
+
// проекта и не давал списка сессий для восстановления.
|
|
81
|
+
function saveLastChat(id, workdir = '', title = '') {
|
|
76
82
|
if (!id) return
|
|
77
|
-
|
|
78
|
-
await fs.mkdir(ZAMES_HOME, { recursive: true })
|
|
79
|
-
await fs.writeFile(
|
|
80
|
-
LAST_CHAT_FILE,
|
|
81
|
-
JSON.stringify({ id, updatedAt: new Date().toISOString() }, null, 2),
|
|
82
|
-
'utf-8',
|
|
83
|
-
)
|
|
84
|
-
} catch (e) {
|
|
85
|
-
if (debug) console.error('last-chat: не удалось сохранить:', e.message)
|
|
86
|
-
}
|
|
83
|
+
saveSession({ id, workdir, title })
|
|
87
84
|
}
|
|
88
85
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const data = JSON.parse(raw)
|
|
93
|
-
return data && typeof data.id === 'string' ? data.id : null
|
|
94
|
-
} catch {
|
|
95
|
-
return null
|
|
96
|
-
}
|
|
86
|
+
function loadLastChat(workdir = '') {
|
|
87
|
+
const s = loadLastSession(workdir)
|
|
88
|
+
return s ? s.id : null
|
|
97
89
|
}
|
|
98
90
|
|
|
99
91
|
|
|
@@ -190,8 +182,8 @@ ${theme.bold('Опции CLI:')}
|
|
|
190
182
|
--dir <path> рабочая директория агента
|
|
191
183
|
--task <text> задача одной строкой
|
|
192
184
|
--chat <id> продолжить существующий чат по id
|
|
193
|
-
--resume-last вернуться в последний сохранённый чат
|
|
194
|
-
--new-chat начать новый
|
|
185
|
+
--resume-last вернуться в последний сохранённый чат (по умолчанию)
|
|
186
|
+
--new-chat начать новый чат, не восстанавливать сессию
|
|
195
187
|
--resend-prompt дослать system-prompt в существующий чат
|
|
196
188
|
--max-iter <n> лимит итераций (по умолчанию ${config.maxIterations})
|
|
197
189
|
--headless браузер без UI
|
|
@@ -207,6 +199,8 @@ ${theme.bold('Пока агент работает:')}
|
|
|
207
199
|
|
|
208
200
|
${theme.bold('Обычные команды:')}
|
|
209
201
|
/new, /clear новый чат (сброс контекста)
|
|
202
|
+
/sessions список сохранённых сессий (~/.zames/.sessions)
|
|
203
|
+
/resume-id <id> восстановить сессию по полному id
|
|
210
204
|
/chats список последних чатов DeepSeek
|
|
211
205
|
/resume <n> открыть чат №n из /chats
|
|
212
206
|
/chat показать текущий chat id
|
|
@@ -235,6 +229,7 @@ ${theme.bold('Самообзор (отладка агента):')}
|
|
|
235
229
|
${theme.bold('Файлы:')}
|
|
236
230
|
Логи: ${config.transcript.dir}
|
|
237
231
|
Undo: ~/.zames/undo
|
|
232
|
+
Сессии: ${sessionsDir()}
|
|
238
233
|
Профиль: ~/.zames/profile
|
|
239
234
|
Снапшоты: ~/.zames/snapshots
|
|
240
235
|
Временные: <проект>/tmp (в .gitignore, чистится при запуске)
|
|
@@ -771,13 +766,15 @@ async function main() {
|
|
|
771
766
|
let freshChat = true
|
|
772
767
|
let sendSystemPrompt = true
|
|
773
768
|
|
|
774
|
-
// По умолчанию
|
|
775
|
-
//
|
|
776
|
-
// --resume-last вернуться в последний сохранённый чат
|
|
769
|
+
// По умолчанию восстанавливаем последнюю сессию для этой директории.
|
|
770
|
+
// Начать с чистого листа — --new-chat.
|
|
777
771
|
let resumeId = chatIdArg
|
|
778
|
-
if (!resumeId &&
|
|
779
|
-
|
|
780
|
-
if (
|
|
772
|
+
if (!resumeId && !newChatFlag) {
|
|
773
|
+
const last = loadLastSession(currentWorkdir)
|
|
774
|
+
if (last && last.id) {
|
|
775
|
+
resumeId = last.id
|
|
776
|
+
console.log(theme.system(`Восстанавливаю сессию ${resumeId}...`))
|
|
777
|
+
}
|
|
781
778
|
}
|
|
782
779
|
|
|
783
780
|
if (resumeId) {
|
|
@@ -786,6 +783,7 @@ async function main() {
|
|
|
786
783
|
await browser.openChat(resumeId)
|
|
787
784
|
freshChat = false
|
|
788
785
|
sendSystemPrompt = resendPrompt
|
|
786
|
+
saveLastChat(resumeId, currentWorkdir)
|
|
789
787
|
} catch (e) {
|
|
790
788
|
console.error(theme.error(`Не удалось открыть чат: ${e.message}`))
|
|
791
789
|
}
|
|
@@ -842,13 +840,23 @@ async function main() {
|
|
|
842
840
|
process.exit(0)
|
|
843
841
|
})
|
|
844
842
|
|
|
845
|
-
//
|
|
843
|
+
// Восстановление сессии при старте. По умолчанию возвращаемся в последний
|
|
844
|
+
// чат для этой рабочей директории (сессии лежат в ~/.zames/.sessions),
|
|
845
|
+
// чтобы контекст не терялся после перезапуска. Явно начать с чистого листа
|
|
846
|
+
// можно флагом --new-chat.
|
|
846
847
|
// --chat <id> открыть конкретный чат
|
|
847
|
-
// --
|
|
848
|
+
// --new-chat не восстанавливать, начать новый чат
|
|
848
849
|
let resumeId = chatIdArg
|
|
849
|
-
if (!resumeId &&
|
|
850
|
-
|
|
851
|
-
if (
|
|
850
|
+
if (!resumeId && !newChatFlag) {
|
|
851
|
+
const last = loadLastSession(currentWorkdir)
|
|
852
|
+
if (last && last.id) {
|
|
853
|
+
resumeId = last.id
|
|
854
|
+
console.log(
|
|
855
|
+
theme.system(
|
|
856
|
+
`Восстанавливаю сессию ${last.id}${last.title ? ` (${last.title})` : ''}...`,
|
|
857
|
+
),
|
|
858
|
+
)
|
|
859
|
+
}
|
|
852
860
|
}
|
|
853
861
|
|
|
854
862
|
if (resumeId) {
|
|
@@ -858,7 +866,7 @@ async function main() {
|
|
|
858
866
|
currentChatId = resumeId
|
|
859
867
|
freshChatNext = false
|
|
860
868
|
sendSystemPromptNext = resendPrompt
|
|
861
|
-
|
|
869
|
+
saveLastChat(resumeId, currentWorkdir)
|
|
862
870
|
console.log(theme.system(`Чат открыт: ${resumeId}\n`))
|
|
863
871
|
} catch (e) {
|
|
864
872
|
console.error(theme.error(`Не удалось открыть чат: ${e.message}`))
|
|
@@ -980,7 +988,7 @@ async function main() {
|
|
|
980
988
|
freshChatNext = false
|
|
981
989
|
sendSystemPromptNext = true
|
|
982
990
|
currentChatId = await browser.getCurrentChatId()
|
|
983
|
-
|
|
991
|
+
saveLastChat(currentChatId, currentWorkdir)
|
|
984
992
|
transcript.log('new_chat')
|
|
985
993
|
console.log(theme.system('Новый чат.\n'))
|
|
986
994
|
} catch (e) {
|
|
@@ -1020,7 +1028,7 @@ async function main() {
|
|
|
1020
1028
|
freshChatNext = false
|
|
1021
1029
|
sendSystemPromptNext = false
|
|
1022
1030
|
currentChatId = await browser.getCurrentChatId()
|
|
1023
|
-
|
|
1031
|
+
saveLastChat(currentChatId, currentWorkdir)
|
|
1024
1032
|
|
|
1025
1033
|
console.log(
|
|
1026
1034
|
theme.user(
|
|
@@ -1105,7 +1113,7 @@ async function main() {
|
|
|
1105
1113
|
freshChatNext = false
|
|
1106
1114
|
sendSystemPromptNext = false
|
|
1107
1115
|
currentChatId = await browser.getCurrentChatId()
|
|
1108
|
-
|
|
1116
|
+
saveLastChat(currentChatId, currentWorkdir)
|
|
1109
1117
|
|
|
1110
1118
|
console.log(
|
|
1111
1119
|
theme.user(
|
|
@@ -1232,7 +1240,7 @@ async function main() {
|
|
|
1232
1240
|
currentChatId = pick.id
|
|
1233
1241
|
freshChatNext = false
|
|
1234
1242
|
sendSystemPromptNext = resendPrompt
|
|
1235
|
-
|
|
1243
|
+
saveLastChat(pick.id, currentWorkdir, pick.title)
|
|
1236
1244
|
transcript.log('resume_chat', { id: pick.id, title: pick.title })
|
|
1237
1245
|
console.log(
|
|
1238
1246
|
theme.assistant(`Чат открыт.`) +
|
|
@@ -1263,6 +1271,51 @@ async function main() {
|
|
|
1263
1271
|
continue
|
|
1264
1272
|
}
|
|
1265
1273
|
|
|
1274
|
+
if (lower === '/sessions' || lower === '/session') {
|
|
1275
|
+
const all = listSessions()
|
|
1276
|
+
console.log(theme.system(`Папка сессий: ${sessionsDir()}`))
|
|
1277
|
+
if (!all.length) {
|
|
1278
|
+
console.log(
|
|
1279
|
+
theme.system(
|
|
1280
|
+
'Сохранённых сессий нет. Они появятся после первой задачи/чата.',
|
|
1281
|
+
),
|
|
1282
|
+
)
|
|
1283
|
+
} else {
|
|
1284
|
+
all.forEach((s, i) => {
|
|
1285
|
+
const n = String(i + 1).padStart(2, ' ')
|
|
1286
|
+
const mark = s.id === currentChatId ? theme.user(' *') : ''
|
|
1287
|
+
const title = s.title ? ` ${s.title}` : ''
|
|
1288
|
+
const wd = s.workdir ? theme.dim(` [${dirLabel(s.workdir)}]`) : ''
|
|
1289
|
+
console.log(` ${theme.user(n)}. ${s.id.slice(0, 8)}…${title}${wd}${mark}`)
|
|
1290
|
+
})
|
|
1291
|
+
console.log(
|
|
1292
|
+
theme.system('Восстановить: /resume-id <id> (полный id) или /resume <n> после /chats.'),
|
|
1293
|
+
)
|
|
1294
|
+
}
|
|
1295
|
+
continue
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
if (lower.startsWith('/resume-id ')) {
|
|
1299
|
+
const id = trimmed.slice('/resume-id'.length).trim()
|
|
1300
|
+
if (!id) {
|
|
1301
|
+
console.error(theme.error('Использование: /resume-id <chat id>'))
|
|
1302
|
+
continue
|
|
1303
|
+
}
|
|
1304
|
+
try {
|
|
1305
|
+
console.log(theme.system(`Открываю чат ${id}...`))
|
|
1306
|
+
await browser.openChat(id)
|
|
1307
|
+
currentChatId = id
|
|
1308
|
+
freshChatNext = false
|
|
1309
|
+
sendSystemPromptNext = resendPrompt
|
|
1310
|
+
saveLastChat(id, currentWorkdir)
|
|
1311
|
+
transcript.log('resume_chat', { id })
|
|
1312
|
+
console.log(theme.assistant('Чат открыт.') + theme.system(String.fromCharCode(10)))
|
|
1313
|
+
} catch (e) {
|
|
1314
|
+
console.error(theme.error('Не удалось открыть чат:'), e.message)
|
|
1315
|
+
}
|
|
1316
|
+
continue
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1266
1319
|
if (lower === '/pwd') {
|
|
1267
1320
|
console.log(theme.system(currentWorkdir))
|
|
1268
1321
|
continue
|
|
@@ -1313,8 +1366,9 @@ async function main() {
|
|
|
1313
1366
|
theme.system(`Resend prompt (--resend-prompt): ${resendPrompt ? 'да' : 'нет'}`),
|
|
1314
1367
|
)
|
|
1315
1368
|
console.log(
|
|
1316
|
-
theme.system(`Last chat: ${
|
|
1369
|
+
theme.system(`Last chat: ${loadLastChat(currentWorkdir) || 'нет'}`),
|
|
1317
1370
|
)
|
|
1371
|
+
console.log(theme.system(`Сессии: ${sessionsDir()}`))
|
|
1318
1372
|
console.log(
|
|
1319
1373
|
theme.system(`Dev mode (auto-reload): ${devMode ? 'вкл' : 'выкл'}`),
|
|
1320
1374
|
)
|
|
@@ -1445,7 +1499,7 @@ async function main() {
|
|
|
1445
1499
|
if (!currentChatId) {
|
|
1446
1500
|
currentChatId = await browser.getCurrentChatId()
|
|
1447
1501
|
}
|
|
1448
|
-
|
|
1502
|
+
saveLastChat(currentChatId, currentWorkdir)
|
|
1449
1503
|
}
|
|
1450
1504
|
|
|
1451
1505
|
if (editor) editor.dispose()
|
package/src/sessions.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
|
|
5
|
+
// Сессии (чаты DeepSeek) храним в отдельной папке, чтобы они не терялись
|
|
6
|
+
// после перезапуска процесса. Каждая сессия — отдельный JSON-файл
|
|
7
|
+
// <id>.json в ~/.zames/.sessions. last.json указывает на последнюю
|
|
8
|
+
// открытую сессию для конкретной рабочей директории.
|
|
9
|
+
const SESSIONS_DIR = path.join(os.homedir(), '.zames', '.sessions')
|
|
10
|
+
const INDEX_FILE = path.join(SESSIONS_DIR, 'last.json')
|
|
11
|
+
|
|
12
|
+
function ensureDir() {
|
|
13
|
+
fs.mkdirSync(SESSIONS_DIR, { recursive: true })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function safeName(id) {
|
|
17
|
+
return String(id).replace(/[^a-zA-Z0-9_.-]/g, '_')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Сохраняем/обновляем сессию. workdir нужен, чтобы при запуске из того же
|
|
21
|
+
// проекта восстанавливать именно его последний чат.
|
|
22
|
+
export function saveSession({ id, title = '', workdir = '' }) {
|
|
23
|
+
if (!id) return
|
|
24
|
+
try {
|
|
25
|
+
ensureDir()
|
|
26
|
+
const file = path.join(SESSIONS_DIR, safeName(id) + '.json')
|
|
27
|
+
let prev = {}
|
|
28
|
+
try { prev = JSON.parse(fs.readFileSync(file, 'utf-8')) } catch {}
|
|
29
|
+
const now = new Date().toISOString()
|
|
30
|
+
const data = {
|
|
31
|
+
id,
|
|
32
|
+
title: title || prev.title || '',
|
|
33
|
+
workdir: workdir || prev.workdir || '',
|
|
34
|
+
createdAt: prev.createdAt || now,
|
|
35
|
+
updatedAt: now,
|
|
36
|
+
}
|
|
37
|
+
fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf-8')
|
|
38
|
+
writeIndex({ id, workdir: data.workdir })
|
|
39
|
+
return data
|
|
40
|
+
} catch {
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function writeIndex({ id, workdir }) {
|
|
46
|
+
try {
|
|
47
|
+
ensureDir()
|
|
48
|
+
let index = {}
|
|
49
|
+
try { index = JSON.parse(fs.readFileSync(INDEX_FILE, 'utf-8')) } catch {}
|
|
50
|
+
if (!index || typeof index !== 'object') index = {}
|
|
51
|
+
if (!index.byWorkdir || typeof index.byWorkdir !== 'object') index.byWorkdir = {}
|
|
52
|
+
const key = workdir || ''
|
|
53
|
+
index.byWorkdir[key] = id
|
|
54
|
+
index.last = id
|
|
55
|
+
index.updatedAt = new Date().toISOString()
|
|
56
|
+
fs.writeFileSync(INDEX_FILE, JSON.stringify(index, null, 2), 'utf-8')
|
|
57
|
+
} catch {}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Последняя сессия для рабочей директории. Если для неё ничего нет —
|
|
61
|
+
// возвращаем последнюю сессию вообще (полезно при запуске из нового места).
|
|
62
|
+
export function loadLastSession(workdir = '') {
|
|
63
|
+
try {
|
|
64
|
+
const index = JSON.parse(fs.readFileSync(INDEX_FILE, 'utf-8'))
|
|
65
|
+
const byWorkdir = index && index.byWorkdir ? index.byWorkdir : {}
|
|
66
|
+
const candidates = [byWorkdir[workdir], index.last].filter(Boolean)
|
|
67
|
+
for (const id of candidates) {
|
|
68
|
+
// Файл сессии мог быть удалён — индекс тогда устарел, и сессию
|
|
69
|
+
// восстанавливать нельзя. Пробуем следующий кандидат.
|
|
70
|
+
const s = readSession(id)
|
|
71
|
+
if (s) return s
|
|
72
|
+
}
|
|
73
|
+
return null
|
|
74
|
+
} catch {
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function readSession(id) {
|
|
80
|
+
if (!id) return null
|
|
81
|
+
try {
|
|
82
|
+
const file = path.join(SESSIONS_DIR, safeName(id) + '.json')
|
|
83
|
+
return JSON.parse(fs.readFileSync(file, 'utf-8'))
|
|
84
|
+
} catch {
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Список всех сессий, свежие — первыми.
|
|
90
|
+
export function listSessions() {
|
|
91
|
+
try {
|
|
92
|
+
ensureDir()
|
|
93
|
+
const files = fs.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith('.json') && f !== 'last.json')
|
|
94
|
+
const out = []
|
|
95
|
+
for (const f of files) {
|
|
96
|
+
try {
|
|
97
|
+
const data = JSON.parse(fs.readFileSync(path.join(SESSIONS_DIR, f), 'utf-8'))
|
|
98
|
+
if (data && data.id) out.push(data)
|
|
99
|
+
} catch {}
|
|
100
|
+
}
|
|
101
|
+
out.sort((a, b) => String(b.updatedAt || '').localeCompare(String(a.updatedAt || '')))
|
|
102
|
+
return out
|
|
103
|
+
} catch {
|
|
104
|
+
return []
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function sessionsDir() {
|
|
109
|
+
return SESSIONS_DIR
|
|
110
|
+
}
|