zames_pro 2.72.1 → 2.72.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/CHANGELOG.md +20 -0
- package/dist/index.js +1 -202
- package/dist/prompt.js +202 -0
- package/package.json +1 -1
- package/scripts/self-smoke.mts +60 -6
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.72.3] - 2026-10-09
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **index:** extract the paste-aware terminal reader into src/prompt.ts
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **index:** extract the paste-aware terminal reader into src/prompt.ts
|
|
19
|
+
|
|
20
|
+
## [2.72.2] - 2026-10-09
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **self-smoke:** add --reuse-profile and a bounded, explicit login failure (exit 2)
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **self-smoke:** add --reuse-profile and a bounded, explicit login failure (exit 2)
|
|
29
|
+
|
|
10
30
|
## [2.72.1] - 2026-10-09
|
|
11
31
|
|
|
12
32
|
### Changed
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { runAgentLoop } from './agent-loop.js';
|
|
|
12
12
|
import { createSubagentRunner } from './subagent.js';
|
|
13
13
|
import { createSpinner } from './spinner.js';
|
|
14
14
|
import { LineEditor } from './input.js';
|
|
15
|
+
import { promptOnce } from './prompt.js';
|
|
15
16
|
import { parseImagePaste, extForMime, saveToTemp, guessMime, formatSize, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, hasClipboardTool, writeClipboardText, contentHash, } from './attachments.js';
|
|
16
17
|
import { loadConfig, DEFAULTS, CONFIG_PATHS, ZAMES_HOME, CONFIG_SCHEMA, getByPath, validateConfigValue, writeConfigValue, resetConfigValue, } from './config.js';
|
|
17
18
|
import { runConfigMenu } from './config-menu.js';
|
|
@@ -567,208 +568,6 @@ async function cleanTmpDir() {
|
|
|
567
568
|
console.error('tmp: cleanup failed:', e.message);
|
|
568
569
|
}
|
|
569
570
|
}
|
|
570
|
-
// Terminal line input with correct paste handling (Shift+Insert,
|
|
571
|
-
// Ctrl+Shift+V, right mouse button, etc.).
|
|
572
|
-
//
|
|
573
|
-
// Why a custom reader instead of readline:
|
|
574
|
-
// 1) readline submits the line on the FIRST newline. When pasting
|
|
575
|
-
// multiline text this caused immediate submission and only the first
|
|
576
|
-
// line went to the chat. Here newlines inside a paste are replaced with
|
|
577
|
-
// spaces, and submission happens only on a single Enter press.
|
|
578
|
-
// 2) We enable bracketed paste mode (\x1b[?2004h): the terminal wraps the
|
|
579
|
-
// pasted text in the markers \x1b[200~ … \x1b[201~, so we know for sure
|
|
580
|
-
// that this is a paste, not keyboard typing, and Enter inside it is not
|
|
581
|
-
// treated as submission.
|
|
582
|
-
async function promptOnce(question) {
|
|
583
|
-
const stdin = process.stdin;
|
|
584
|
-
const stdout = process.stdout;
|
|
585
|
-
// Non-TTY (pipe, redirect): read everything up to EOF as a single string.
|
|
586
|
-
if (!stdin.isTTY || !stdin.setRawMode) {
|
|
587
|
-
const chunks = [];
|
|
588
|
-
return await new Promise((resolve) => {
|
|
589
|
-
const onData = (b) => chunks.push(b);
|
|
590
|
-
const onEnd = () => {
|
|
591
|
-
stdin.removeListener('data', onData);
|
|
592
|
-
stdin.removeListener('end', onEnd);
|
|
593
|
-
resolve(Buffer.concat(chunks).toString('utf-8'));
|
|
594
|
-
};
|
|
595
|
-
stdin.on('data', onData);
|
|
596
|
-
stdin.on('end', onEnd);
|
|
597
|
-
stdin.resume();
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
const wasRaw = stdin.isRaw;
|
|
601
|
-
stdin.setRawMode(true);
|
|
602
|
-
stdin.resume();
|
|
603
|
-
// We enable/disable bracketed paste in pairs.
|
|
604
|
-
stdout.write('\x1b[?2004h');
|
|
605
|
-
stdout.write(question);
|
|
606
|
-
let line = '';
|
|
607
|
-
let cursor = 0;
|
|
608
|
-
let inPaste = false;
|
|
609
|
-
const PASTE_START = '\x1b[200~';
|
|
610
|
-
const PASTE_END = '\x1b[201~';
|
|
611
|
-
const redraw = () => {
|
|
612
|
-
// Return to the start of the line, erase and print again.
|
|
613
|
-
stdout.write(String.fromCharCode(13));
|
|
614
|
-
stdout.write('\x1b[K');
|
|
615
|
-
stdout.write(question + line);
|
|
616
|
-
// Put the cursor in the right position.
|
|
617
|
-
const back = line.length - cursor;
|
|
618
|
-
if (back > 0)
|
|
619
|
-
stdout.write('\x1b[' + back + 'D');
|
|
620
|
-
};
|
|
621
|
-
return await new Promise((resolve) => {
|
|
622
|
-
const finish = (value, submit) => {
|
|
623
|
-
stdin.removeListener('data', onData);
|
|
624
|
-
stdout.write('\x1b[?2004l');
|
|
625
|
-
if (stdin.setRawMode)
|
|
626
|
-
stdin.setRawMode(wasRaw || false);
|
|
627
|
-
if (submit)
|
|
628
|
-
stdout.write(String.fromCharCode(10));
|
|
629
|
-
resolve(value);
|
|
630
|
-
};
|
|
631
|
-
const insertText = (text) => {
|
|
632
|
-
// Normalize newlines: they come from a multiline paste but mean
|
|
633
|
-
// "submit". Inside the message we replace them with a space so the
|
|
634
|
-
// whole paste goes as ONE message.
|
|
635
|
-
const clean = text
|
|
636
|
-
.replace(/\r\n/g, ' ')
|
|
637
|
-
.replace(/\r/g, ' ')
|
|
638
|
-
.replace(/\n/g, ' ');
|
|
639
|
-
line = line.slice(0, cursor) + clean + line.slice(cursor);
|
|
640
|
-
cursor += clean.length;
|
|
641
|
-
};
|
|
642
|
-
const onData = (buf) => {
|
|
643
|
-
let s = buf.toString('utf-8');
|
|
644
|
-
// Fallback for terminals without bracketed paste: if the whole chunk is
|
|
645
|
-
// a "bare" newline (one byte), then Enter was pressed → submit.
|
|
646
|
-
// If newlines came TOGETHER with other text in one chunk — that's a
|
|
647
|
-
// paste; such newlines don't submit the message but are replaced with
|
|
648
|
-
// spaces (see insertText).
|
|
649
|
-
if (!inPaste && (s === '\r' || s === '\n')) {
|
|
650
|
-
return finish(line, true);
|
|
651
|
-
}
|
|
652
|
-
while (s.length) {
|
|
653
|
-
if (inPaste) {
|
|
654
|
-
const end = s.indexOf(PASTE_END);
|
|
655
|
-
if (end === -1) {
|
|
656
|
-
insertText(s);
|
|
657
|
-
s = '';
|
|
658
|
-
}
|
|
659
|
-
else {
|
|
660
|
-
insertText(s.slice(0, end));
|
|
661
|
-
s = s.slice(end + PASTE_END.length);
|
|
662
|
-
inPaste = false;
|
|
663
|
-
}
|
|
664
|
-
redraw();
|
|
665
|
-
continue;
|
|
666
|
-
}
|
|
667
|
-
const start = s.indexOf(PASTE_START);
|
|
668
|
-
if (start !== -1) {
|
|
669
|
-
// Everything before the marker is processed as regular input.
|
|
670
|
-
const before = s.slice(0, start);
|
|
671
|
-
s = s.slice(start + PASTE_START.length);
|
|
672
|
-
inPaste = true;
|
|
673
|
-
if (before) {
|
|
674
|
-
for (const ch of before) {
|
|
675
|
-
if (ch === '\r' || ch === '\n') {
|
|
676
|
-
/* inside a paste — skip */
|
|
677
|
-
}
|
|
678
|
-
else
|
|
679
|
-
insertText(ch);
|
|
680
|
-
}
|
|
681
|
-
redraw();
|
|
682
|
-
}
|
|
683
|
-
continue;
|
|
684
|
-
}
|
|
685
|
-
const ch = s[0];
|
|
686
|
-
const code = s.charCodeAt(0);
|
|
687
|
-
s = s.slice(1);
|
|
688
|
-
if (ch === '\r' || ch === '\n') {
|
|
689
|
-
// A newline inside a chunk with other text (paste without
|
|
690
|
-
// bracketed paste): don't submit, insert a space instead.
|
|
691
|
-
insertText(' ');
|
|
692
|
-
redraw();
|
|
693
|
-
continue;
|
|
694
|
-
}
|
|
695
|
-
if (code === 3) {
|
|
696
|
-
// Ctrl+C — abort input.
|
|
697
|
-
return finish('', true);
|
|
698
|
-
}
|
|
699
|
-
if (code === 4) {
|
|
700
|
-
// Ctrl+D — like submitting an empty line.
|
|
701
|
-
return finish(line, true);
|
|
702
|
-
}
|
|
703
|
-
if (code === 21) {
|
|
704
|
-
// Ctrl+U — erase the line.
|
|
705
|
-
line = '';
|
|
706
|
-
cursor = 0;
|
|
707
|
-
redraw();
|
|
708
|
-
continue;
|
|
709
|
-
}
|
|
710
|
-
if (code === 127 || code === 8) {
|
|
711
|
-
// Backspace.
|
|
712
|
-
if (cursor > 0) {
|
|
713
|
-
line = line.slice(0, cursor - 1) + line.slice(cursor);
|
|
714
|
-
cursor--;
|
|
715
|
-
redraw();
|
|
716
|
-
}
|
|
717
|
-
continue;
|
|
718
|
-
}
|
|
719
|
-
if (ch === '\x1b') {
|
|
720
|
-
// Escape sequences (arrows, Home/End, Delete…).
|
|
721
|
-
const rest = s;
|
|
722
|
-
if (rest.startsWith('[D')) {
|
|
723
|
-
if (cursor > 0)
|
|
724
|
-
cursor--;
|
|
725
|
-
s = s.slice(2);
|
|
726
|
-
redraw();
|
|
727
|
-
continue;
|
|
728
|
-
}
|
|
729
|
-
if (rest.startsWith('[C')) {
|
|
730
|
-
if (cursor < line.length)
|
|
731
|
-
cursor++;
|
|
732
|
-
s = s.slice(2);
|
|
733
|
-
redraw();
|
|
734
|
-
continue;
|
|
735
|
-
}
|
|
736
|
-
if (rest.startsWith('[H') || rest.startsWith('[1~')) {
|
|
737
|
-
cursor = 0;
|
|
738
|
-
s = s.slice(rest.startsWith('[1~') ? 3 : 2);
|
|
739
|
-
redraw();
|
|
740
|
-
continue;
|
|
741
|
-
}
|
|
742
|
-
if (rest.startsWith('[F') || rest.startsWith('[4~')) {
|
|
743
|
-
cursor = line.length;
|
|
744
|
-
s = s.slice(rest.startsWith('[4~') ? 3 : 2);
|
|
745
|
-
redraw();
|
|
746
|
-
continue;
|
|
747
|
-
}
|
|
748
|
-
if (rest.startsWith('[3~')) {
|
|
749
|
-
// Delete.
|
|
750
|
-
if (cursor < line.length) {
|
|
751
|
-
line = line.slice(0, cursor) + line.slice(cursor + 1);
|
|
752
|
-
redraw();
|
|
753
|
-
}
|
|
754
|
-
s = s.slice(3);
|
|
755
|
-
continue;
|
|
756
|
-
}
|
|
757
|
-
// Other ESC sequences are skipped up to a letter/tilde.
|
|
758
|
-
const m = s.match(/^\[[0-9;]*[A-Za-z~]/);
|
|
759
|
-
if (m)
|
|
760
|
-
s = s.slice(m[0].length);
|
|
761
|
-
continue;
|
|
762
|
-
}
|
|
763
|
-
if (code < 32)
|
|
764
|
-
continue; // other control characters are ignored
|
|
765
|
-
insertText(ch);
|
|
766
|
-
redraw();
|
|
767
|
-
}
|
|
768
|
-
};
|
|
769
|
-
stdin.on('data', onData);
|
|
770
|
-
});
|
|
771
|
-
}
|
|
772
571
|
// ---------- workdir resolution ----------
|
|
773
572
|
// The agent works in the directory it was launched from (process.cwd()).
|
|
774
573
|
// This is the sandbox root: tools cannot go above it.
|
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// Terminal line input with correct paste handling (Shift+Insert,
|
|
2
|
+
// Ctrl+Shift+V, right mouse button, etc.).
|
|
3
|
+
//
|
|
4
|
+
// Why a custom reader instead of readline:
|
|
5
|
+
// 1) readline submits the line on the FIRST newline. When pasting
|
|
6
|
+
// multiline text this caused immediate submission and only the first
|
|
7
|
+
// line went to the chat. Here newlines inside a paste are replaced with
|
|
8
|
+
// spaces, and submission happens only on a single Enter press.
|
|
9
|
+
// 2) We enable bracketed paste mode (\x1b[?2004h): the terminal wraps the
|
|
10
|
+
// pasted text in the markers \x1b[200~ … \x1b[201~, so we know for sure
|
|
11
|
+
// that this is a paste, not keyboard typing, and Enter inside it is not
|
|
12
|
+
// treated as submission.
|
|
13
|
+
export async function promptOnce(question) {
|
|
14
|
+
const stdin = process.stdin;
|
|
15
|
+
const stdout = process.stdout;
|
|
16
|
+
// Non-TTY (pipe, redirect): read everything up to EOF as a single string.
|
|
17
|
+
if (!stdin.isTTY || !stdin.setRawMode) {
|
|
18
|
+
const chunks = [];
|
|
19
|
+
return await new Promise((resolve) => {
|
|
20
|
+
const onData = (b) => chunks.push(b);
|
|
21
|
+
const onEnd = () => {
|
|
22
|
+
stdin.removeListener('data', onData);
|
|
23
|
+
stdin.removeListener('end', onEnd);
|
|
24
|
+
resolve(Buffer.concat(chunks).toString('utf-8'));
|
|
25
|
+
};
|
|
26
|
+
stdin.on('data', onData);
|
|
27
|
+
stdin.on('end', onEnd);
|
|
28
|
+
stdin.resume();
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const wasRaw = stdin.isRaw;
|
|
32
|
+
stdin.setRawMode(true);
|
|
33
|
+
stdin.resume();
|
|
34
|
+
// We enable/disable bracketed paste in pairs.
|
|
35
|
+
stdout.write('\x1b[?2004h');
|
|
36
|
+
stdout.write(question);
|
|
37
|
+
let line = '';
|
|
38
|
+
let cursor = 0;
|
|
39
|
+
let inPaste = false;
|
|
40
|
+
const PASTE_START = '\x1b[200~';
|
|
41
|
+
const PASTE_END = '\x1b[201~';
|
|
42
|
+
const redraw = () => {
|
|
43
|
+
// Return to the start of the line, erase and print again.
|
|
44
|
+
stdout.write(String.fromCharCode(13));
|
|
45
|
+
stdout.write('\x1b[K');
|
|
46
|
+
stdout.write(question + line);
|
|
47
|
+
// Put the cursor in the right position.
|
|
48
|
+
const back = line.length - cursor;
|
|
49
|
+
if (back > 0)
|
|
50
|
+
stdout.write('\x1b[' + back + 'D');
|
|
51
|
+
};
|
|
52
|
+
return await new Promise((resolve) => {
|
|
53
|
+
const finish = (value, submit) => {
|
|
54
|
+
stdin.removeListener('data', onData);
|
|
55
|
+
stdout.write('\x1b[?2004l');
|
|
56
|
+
if (stdin.setRawMode)
|
|
57
|
+
stdin.setRawMode(wasRaw || false);
|
|
58
|
+
if (submit)
|
|
59
|
+
stdout.write(String.fromCharCode(10));
|
|
60
|
+
resolve(value);
|
|
61
|
+
};
|
|
62
|
+
const insertText = (text) => {
|
|
63
|
+
// Normalize newlines: they come from a multiline paste but mean
|
|
64
|
+
// "submit". Inside the message we replace them with a space so the
|
|
65
|
+
// whole paste goes as ONE message.
|
|
66
|
+
const clean = text
|
|
67
|
+
.replace(/\r\n/g, ' ')
|
|
68
|
+
.replace(/\r/g, ' ')
|
|
69
|
+
.replace(/\n/g, ' ');
|
|
70
|
+
line = line.slice(0, cursor) + clean + line.slice(cursor);
|
|
71
|
+
cursor += clean.length;
|
|
72
|
+
};
|
|
73
|
+
const onData = (buf) => {
|
|
74
|
+
let s = buf.toString('utf-8');
|
|
75
|
+
// Fallback for terminals without bracketed paste: if the whole chunk is
|
|
76
|
+
// a "bare" newline (one byte), then Enter was pressed → submit.
|
|
77
|
+
// If newlines came TOGETHER with other text in one chunk — that's a
|
|
78
|
+
// paste; such newlines don't submit the message but are replaced with
|
|
79
|
+
// spaces (see insertText).
|
|
80
|
+
if (!inPaste && (s === '\r' || s === '\n')) {
|
|
81
|
+
return finish(line, true);
|
|
82
|
+
}
|
|
83
|
+
while (s.length) {
|
|
84
|
+
if (inPaste) {
|
|
85
|
+
const end = s.indexOf(PASTE_END);
|
|
86
|
+
if (end === -1) {
|
|
87
|
+
insertText(s);
|
|
88
|
+
s = '';
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
insertText(s.slice(0, end));
|
|
92
|
+
s = s.slice(end + PASTE_END.length);
|
|
93
|
+
inPaste = false;
|
|
94
|
+
}
|
|
95
|
+
redraw();
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const start = s.indexOf(PASTE_START);
|
|
99
|
+
if (start !== -1) {
|
|
100
|
+
// Everything before the marker is processed as regular input.
|
|
101
|
+
const before = s.slice(0, start);
|
|
102
|
+
s = s.slice(start + PASTE_START.length);
|
|
103
|
+
inPaste = true;
|
|
104
|
+
if (before) {
|
|
105
|
+
for (const ch of before) {
|
|
106
|
+
if (ch === '\r' || ch === '\n') {
|
|
107
|
+
/* inside a paste — skip */
|
|
108
|
+
}
|
|
109
|
+
else
|
|
110
|
+
insertText(ch);
|
|
111
|
+
}
|
|
112
|
+
redraw();
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const ch = s[0];
|
|
117
|
+
const code = s.charCodeAt(0);
|
|
118
|
+
s = s.slice(1);
|
|
119
|
+
if (ch === '\r' || ch === '\n') {
|
|
120
|
+
// A newline inside a chunk with other text (paste without
|
|
121
|
+
// bracketed paste): don't submit, insert a space instead.
|
|
122
|
+
insertText(' ');
|
|
123
|
+
redraw();
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (code === 3) {
|
|
127
|
+
// Ctrl+C — abort input.
|
|
128
|
+
return finish('', true);
|
|
129
|
+
}
|
|
130
|
+
if (code === 4) {
|
|
131
|
+
// Ctrl+D — like submitting an empty line.
|
|
132
|
+
return finish(line, true);
|
|
133
|
+
}
|
|
134
|
+
if (code === 21) {
|
|
135
|
+
// Ctrl+U — erase the line.
|
|
136
|
+
line = '';
|
|
137
|
+
cursor = 0;
|
|
138
|
+
redraw();
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (code === 127 || code === 8) {
|
|
142
|
+
// Backspace.
|
|
143
|
+
if (cursor > 0) {
|
|
144
|
+
line = line.slice(0, cursor - 1) + line.slice(cursor);
|
|
145
|
+
cursor--;
|
|
146
|
+
redraw();
|
|
147
|
+
}
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (ch === '\x1b') {
|
|
151
|
+
// Escape sequences (arrows, Home/End, Delete…).
|
|
152
|
+
const rest = s;
|
|
153
|
+
if (rest.startsWith('[D')) {
|
|
154
|
+
if (cursor > 0)
|
|
155
|
+
cursor--;
|
|
156
|
+
s = s.slice(2);
|
|
157
|
+
redraw();
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (rest.startsWith('[C')) {
|
|
161
|
+
if (cursor < line.length)
|
|
162
|
+
cursor++;
|
|
163
|
+
s = s.slice(2);
|
|
164
|
+
redraw();
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (rest.startsWith('[H') || rest.startsWith('[1~')) {
|
|
168
|
+
cursor = 0;
|
|
169
|
+
s = s.slice(rest.startsWith('[1~') ? 3 : 2);
|
|
170
|
+
redraw();
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (rest.startsWith('[F') || rest.startsWith('[4~')) {
|
|
174
|
+
cursor = line.length;
|
|
175
|
+
s = s.slice(rest.startsWith('[4~') ? 3 : 2);
|
|
176
|
+
redraw();
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (rest.startsWith('[3~')) {
|
|
180
|
+
// Delete.
|
|
181
|
+
if (cursor < line.length) {
|
|
182
|
+
line = line.slice(0, cursor) + line.slice(cursor + 1);
|
|
183
|
+
redraw();
|
|
184
|
+
}
|
|
185
|
+
s = s.slice(3);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
// Other ESC sequences are skipped up to a letter/tilde.
|
|
189
|
+
const m = s.match(/^\[[0-9;]*[A-Za-z~]/);
|
|
190
|
+
if (m)
|
|
191
|
+
s = s.slice(m[0].length);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (code < 32)
|
|
195
|
+
continue; // other control characters are ignored
|
|
196
|
+
insertText(ch);
|
|
197
|
+
redraw();
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
stdin.on('data', onData);
|
|
201
|
+
});
|
|
202
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.72.
|
|
3
|
+
"version": "2.72.3",
|
|
4
4
|
"description": "Terminal coding agent that drives chat.deepseek.com through Playwright — no API key required. Reads and edits files, runs commands, commits to git, like Claude Code / Codex CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/scripts/self-smoke.mts
CHANGED
|
@@ -7,18 +7,35 @@
|
|
|
7
7
|
// config.json (so auto-login works) and symlinks the Playwright browser cache.
|
|
8
8
|
// Nothing in the operator's profile is written.
|
|
9
9
|
//
|
|
10
|
-
// Usage: npm run self-smoke
|
|
10
|
+
// Usage: npm run self-smoke (headless, empty profile)
|
|
11
11
|
// npm run self-smoke -- --headed
|
|
12
|
-
//
|
|
12
|
+
// npm run self-smoke -- --reuse-profile (logged-in profile, see below)
|
|
13
|
+
// Exit code 0 = all scenarios passed; 2 = login failed (could not start).
|
|
14
|
+
//
|
|
15
|
+
// --reuse-profile: copy the REAL profile (~/.zames/profile) read-only into the
|
|
16
|
+
// throwaway HOME so the browser starts ALREADY SIGNED IN. Reason: DeepSeek's
|
|
17
|
+
// anti-bot increasingly blocks a COLD login on a fresh profile (captcha / rate
|
|
18
|
+
// limit), so a smoke that logs in from scratch fails for reasons unrelated to
|
|
19
|
+
// the agent. Copying (never opening the live profile) keeps isolation intact.
|
|
13
20
|
|
|
14
21
|
import fs from 'fs'
|
|
15
22
|
import path from 'path'
|
|
16
23
|
import os from 'os'
|
|
17
24
|
|
|
25
|
+
// --- flags -----------------------------------------------------------------
|
|
18
26
|
const HEADED = process.argv.includes('--headed')
|
|
27
|
+
const REUSE_PROFILE =
|
|
28
|
+
process.argv.includes('--reuse-profile') ||
|
|
29
|
+
process.env.ZAMES_SMOKE_REUSE_PROFILE === '1'
|
|
30
|
+
// Optional explicit source profile (default: the real ~/.zames/profile).
|
|
31
|
+
const profileArgIdx = process.argv.indexOf('--profile')
|
|
32
|
+
const REAL_HOME = os.homedir()
|
|
33
|
+
const PROFILE_SRC =
|
|
34
|
+
profileArgIdx >= 0 && process.argv[profileArgIdx + 1]
|
|
35
|
+
? path.resolve(process.argv[profileArgIdx + 1])
|
|
36
|
+
: path.join(REAL_HOME, '.zames', 'profile')
|
|
19
37
|
|
|
20
38
|
// --- isolate HOME before importing project modules -------------------------
|
|
21
|
-
const REAL_HOME = os.homedir()
|
|
22
39
|
if (!process.env.ZAMES_SMOKE_HOME) {
|
|
23
40
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'zames-smoke-'))
|
|
24
41
|
process.env.HOME = tmp
|
|
@@ -35,6 +52,16 @@ if (!process.env.ZAMES_SMOKE_HOME) {
|
|
|
35
52
|
if (fs.existsSync(realPw)) {
|
|
36
53
|
fs.symlinkSync(realPw, path.join(tmp, '.cache', 'ms-playwright'))
|
|
37
54
|
}
|
|
55
|
+
// Reuse a logged-in profile: COPY it (the live one is never opened) and drop
|
|
56
|
+
// the Singleton* files — Chrome refuses to start on a profile whose lock is
|
|
57
|
+
// present, and the live browser may hold it. The cookies survive the copy
|
|
58
|
+
// because Playwright launches with --password-store=basic.
|
|
59
|
+
if (REUSE_PROFILE && fs.existsSync(PROFILE_SRC)) {
|
|
60
|
+
fs.cpSync(PROFILE_SRC, path.join(tmp, '.zames', 'profile'), {
|
|
61
|
+
recursive: true,
|
|
62
|
+
filter: (src) => !/^Singleton/.test(path.basename(src)),
|
|
63
|
+
})
|
|
64
|
+
}
|
|
38
65
|
}
|
|
39
66
|
const SMOKE_HOME = process.env.ZAMES_SMOKE_HOME
|
|
40
67
|
const WORK = path.join(SMOKE_HOME, 'work')
|
|
@@ -54,6 +81,9 @@ interface SmokeResult {
|
|
|
54
81
|
note?: string
|
|
55
82
|
}
|
|
56
83
|
const results: SmokeResult[] = []
|
|
84
|
+
// Set when we could not even start (login failed): exit code 2, distinct from a
|
|
85
|
+
// scenario failure (1).
|
|
86
|
+
let loginFailed = false
|
|
57
87
|
const record = (name: string, ok: boolean, note?: string): void => {
|
|
58
88
|
results.push({ name, ok, note })
|
|
59
89
|
console.log((ok ? 'PASS ' : 'FAIL ') + name + (note ? ' — ' + note : ''))
|
|
@@ -76,9 +106,33 @@ const browser = new DeepSeekBrowser({
|
|
|
76
106
|
})
|
|
77
107
|
|
|
78
108
|
async function main() {
|
|
79
|
-
console.log(
|
|
109
|
+
console.log(
|
|
110
|
+
'self-smoke: HOME=' +
|
|
111
|
+
SMOKE_HOME +
|
|
112
|
+
' headed=' +
|
|
113
|
+
HEADED +
|
|
114
|
+
' reuse-profile=' +
|
|
115
|
+
REUSE_PROFILE,
|
|
116
|
+
)
|
|
80
117
|
await browser.launch()
|
|
81
|
-
|
|
118
|
+
// waitForLogin() can block on a manual Enter in a pipe. Bound it so a cold
|
|
119
|
+
// login (anti-bot / captcha) fails fast with a clear hint instead of hanging
|
|
120
|
+
// until the caller's tool timeout. On failure exit code 2 signals "could not
|
|
121
|
+
// even start" — distinct from a scenario failure.
|
|
122
|
+
const loggedIn = await Promise.race([
|
|
123
|
+
browser.waitForLogin().then(() => true),
|
|
124
|
+
new Promise<boolean>((r) => setTimeout(() => r(false), 120_000)),
|
|
125
|
+
])
|
|
126
|
+
if (!loggedIn) {
|
|
127
|
+
console.error(
|
|
128
|
+
'\nself-smoke: LOGIN FAILED — a cold login on a fresh profile is often\n' +
|
|
129
|
+
'blocked by DeepSeek anti-bot/captcha. Re-run against a signed-in profile:\n' +
|
|
130
|
+
' npm run self-smoke -- --reuse-profile\n' +
|
|
131
|
+
'(or sign in once with: npm run self-smoke -- --headed).',
|
|
132
|
+
)
|
|
133
|
+
loginFailed = true
|
|
134
|
+
return
|
|
135
|
+
}
|
|
82
136
|
record('login', true, 'DeepSeek session is active')
|
|
83
137
|
|
|
84
138
|
const tools = createTools(WORK, {})
|
|
@@ -252,4 +306,4 @@ if (!process.env.ZAMES_SMOKE_KEEP) {
|
|
|
252
306
|
fs.rmSync(SMOKE_HOME, { recursive: true, force: true })
|
|
253
307
|
} catch {}
|
|
254
308
|
}
|
|
255
|
-
process.exit(anyFail ? 1 : 0)
|
|
309
|
+
process.exit(loginFailed ? 2 : anyFail ? 1 : 0)
|