zames_pro 2.72.2 → 2.72.4
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/config-commands.js +75 -0
- package/dist/index.js +7 -253
- package/dist/prompt.js +202 -0
- package/package.json +1 -1
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.4] - 2026-10-09
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **index:** extract the pure /config rendering into config-commands.ts (C3)
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **index:** extract the pure /config rendering into config-commands.ts (C3)
|
|
19
|
+
|
|
20
|
+
## [2.72.3] - 2026-10-09
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **index:** extract the paste-aware terminal reader into src/prompt.ts
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- **index:** extract the paste-aware terminal reader into src/prompt.ts
|
|
29
|
+
|
|
10
30
|
## [2.72.2] - 2026-10-09
|
|
11
31
|
|
|
12
32
|
### Added
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// PURE rendering for the /config command's TEXT output (BACKLOG C3). These
|
|
2
|
+
// helpers format the schema into lines; they touch no browser/terminal state,
|
|
3
|
+
// so they are unit-tested without a live agent (the /config dispatch in
|
|
4
|
+
// index.ts only prints what these return). The interactive menu lives in
|
|
5
|
+
// config-menu.ts; the value read/write policy lives in config.ts.
|
|
6
|
+
import { theme } from './theme.js';
|
|
7
|
+
/**
|
|
8
|
+
* The value shown for a field in the LIST: the default marker, on/off for a
|
|
9
|
+
* boolean, a fixed mask for a password, otherwise the JSON form. Mirrors the
|
|
10
|
+
* long-used inline logic so the output is byte-for-byte identical.
|
|
11
|
+
*/
|
|
12
|
+
export function configListValue(field, value, t) {
|
|
13
|
+
if (value === undefined)
|
|
14
|
+
return t('cfg.menu.default');
|
|
15
|
+
if (typeof value === 'boolean')
|
|
16
|
+
return value ? t('common.on') : t('common.off');
|
|
17
|
+
if (/password/i.test(field.path) && String(value).length > 0)
|
|
18
|
+
return '********';
|
|
19
|
+
return JSON.stringify(value);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The value shown for `/config get <path>`: the default marker, a fixed mask
|
|
23
|
+
* for a password, otherwise the JSON form. Note this deliberately does NOT
|
|
24
|
+
* special-case booleans (the get subcommand has always printed true/false).
|
|
25
|
+
*/
|
|
26
|
+
export function configGetValue(path, value, t) {
|
|
27
|
+
if (value === undefined)
|
|
28
|
+
return t('cfg.menu.default');
|
|
29
|
+
if (/password/i.test(path) && String(value).length > 0)
|
|
30
|
+
return '********';
|
|
31
|
+
return JSON.stringify(value);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Human-readable hint of the accepted values for a field, used in the error
|
|
35
|
+
* message when /config set gets a bad value. Enum -> the allowed list,
|
|
36
|
+
* number -> a range, boolean -> "true|false".
|
|
37
|
+
*/
|
|
38
|
+
export function configValueHint(field, t) {
|
|
39
|
+
if (field.values && field.values.length)
|
|
40
|
+
return field.values.join('|');
|
|
41
|
+
if (field.type === 'boolean')
|
|
42
|
+
return 'true|false';
|
|
43
|
+
if (field.type === 'number') {
|
|
44
|
+
const lo = field.min !== undefined ? String(field.min) : '-';
|
|
45
|
+
const hi = field.max !== undefined ? String(field.max) : '-';
|
|
46
|
+
return t('cfg.range_hint', { min: lo, max: hi });
|
|
47
|
+
}
|
|
48
|
+
return field.type;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Render the full settings LIST as an array of lines (grouped by section).
|
|
52
|
+
* `get` reads the current value of a path (undefined = default). Returned as
|
|
53
|
+
* lines so index.ts only has to print them and the formatting is testable.
|
|
54
|
+
*/
|
|
55
|
+
export function formatConfigList(fields, get, t) {
|
|
56
|
+
const out = [theme.system(t('cfg.title'))];
|
|
57
|
+
let lastGroup = '';
|
|
58
|
+
for (const f of fields) {
|
|
59
|
+
const g = t(f.groupKey);
|
|
60
|
+
if (g !== lastGroup) {
|
|
61
|
+
out.push(theme.system('\n ' + g));
|
|
62
|
+
lastGroup = g;
|
|
63
|
+
}
|
|
64
|
+
const shown = configListValue(f, get(f.path), t);
|
|
65
|
+
const extra = f.values ? ' [' + f.values.join('|') + ']' : '';
|
|
66
|
+
out.push(' ' +
|
|
67
|
+
theme.user(f.path) +
|
|
68
|
+
theme.dim(' = ') +
|
|
69
|
+
theme.assistant(shown) +
|
|
70
|
+
theme.dim(extra) +
|
|
71
|
+
theme.dim(' # ' + t(f.labelKey)));
|
|
72
|
+
}
|
|
73
|
+
out.push(theme.dim('\n' + t('cfg.usage')));
|
|
74
|
+
return out;
|
|
75
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -12,9 +12,11 @@ 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';
|
|
19
|
+
import { configGetValue, configValueHint, formatConfigList, } from './config-commands.js';
|
|
18
20
|
import { translate, normalizeLocale, localeDisplayName, isLocale, } from './i18n.js';
|
|
19
21
|
import { Transcript } from './transcript.js';
|
|
20
22
|
import { UndoStore } from './undo.js';
|
|
@@ -567,208 +569,6 @@ async function cleanTmpDir() {
|
|
|
567
569
|
console.error('tmp: cleanup failed:', e.message);
|
|
568
570
|
}
|
|
569
571
|
}
|
|
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
572
|
// ---------- workdir resolution ----------
|
|
773
573
|
// The agent works in the directory it was launched from (process.cwd()).
|
|
774
574
|
// This is the sandbox root: tools cannot go above it.
|
|
@@ -1729,55 +1529,16 @@ async function main() {
|
|
|
1729
1529
|
// Human-readable hint of the accepted values for a field, used in the error
|
|
1730
1530
|
// message when /config set gets a bad value. Enum -> the allowed list,
|
|
1731
1531
|
// number -> a range, boolean -> "true|false".
|
|
1732
|
-
function configValueHint(field) {
|
|
1733
|
-
if (field.values && field.values.length)
|
|
1734
|
-
return field.values.join('|');
|
|
1735
|
-
if (field.type === 'boolean')
|
|
1736
|
-
return 'true|false';
|
|
1737
|
-
if (field.type === 'number') {
|
|
1738
|
-
const lo = field.min !== undefined ? String(field.min) : '-';
|
|
1739
|
-
const hi = field.max !== undefined ? String(field.max) : '-';
|
|
1740
|
-
return t('cfg.range_hint', { min: lo, max: hi });
|
|
1741
|
-
}
|
|
1742
|
-
return field.type;
|
|
1743
|
-
}
|
|
1744
1532
|
function configResetField(field) {
|
|
1745
1533
|
resetConfigValue('home', field.path);
|
|
1746
1534
|
// Reset the runtime value to the default.
|
|
1747
1535
|
const def = getByPath(DEFAULTS, field.path);
|
|
1748
1536
|
setConfigRuntime(field.path, def);
|
|
1749
1537
|
}
|
|
1750
|
-
function configLabel(field) {
|
|
1751
|
-
return t(field.labelKey);
|
|
1752
|
-
}
|
|
1753
1538
|
function configShowList() {
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
const g = t(f.groupKey);
|
|
1758
|
-
if (g !== lastGroup) {
|
|
1759
|
-
console.log(theme.system('\n ' + g));
|
|
1760
|
-
lastGroup = g;
|
|
1761
|
-
}
|
|
1762
|
-
const cur = getByPath(config, f.path);
|
|
1763
|
-
const shown = cur === undefined
|
|
1764
|
-
? t('cfg.menu.default')
|
|
1765
|
-
: typeof cur === 'boolean'
|
|
1766
|
-
? cur
|
|
1767
|
-
? t('common.on')
|
|
1768
|
-
: t('common.off')
|
|
1769
|
-
: /password/i.test(f.path) && String(cur).length > 0
|
|
1770
|
-
? '********'
|
|
1771
|
-
: JSON.stringify(cur);
|
|
1772
|
-
const extra = f.values ? ' [' + f.values.join('|') + ']' : '';
|
|
1773
|
-
console.log(' ' +
|
|
1774
|
-
theme.user(f.path) +
|
|
1775
|
-
theme.dim(' = ') +
|
|
1776
|
-
theme.assistant(shown) +
|
|
1777
|
-
theme.dim(extra) +
|
|
1778
|
-
theme.dim(' # ' + configLabel(f)));
|
|
1779
|
-
}
|
|
1780
|
-
console.log(theme.dim('\n' + t('cfg.usage')));
|
|
1539
|
+
for (const line of formatConfigList(CONFIG_SCHEMA, (p) => getByPath(config, p), t)) {
|
|
1540
|
+
console.log(line);
|
|
1541
|
+
}
|
|
1781
1542
|
}
|
|
1782
1543
|
async function configOpenMenu() {
|
|
1783
1544
|
if (!(process.stdin.isTTY && process.stdout.isTTY)) {
|
|
@@ -1858,14 +1619,7 @@ async function main() {
|
|
|
1858
1619
|
return;
|
|
1859
1620
|
}
|
|
1860
1621
|
const cur = getByPath(config, key);
|
|
1861
|
-
console.log(theme.system(t('cfg.value', {
|
|
1862
|
-
v: key,
|
|
1863
|
-
value: cur === undefined
|
|
1864
|
-
? t('cfg.menu.default')
|
|
1865
|
-
: /password/i.test(key) && String(cur).length > 0
|
|
1866
|
-
? '********'
|
|
1867
|
-
: JSON.stringify(cur),
|
|
1868
|
-
})));
|
|
1622
|
+
console.log(theme.system(t('cfg.value', { v: key, value: configGetValue(key, cur, t) })));
|
|
1869
1623
|
return;
|
|
1870
1624
|
}
|
|
1871
1625
|
if (sub === 'set') {
|
|
@@ -1886,7 +1640,7 @@ async function main() {
|
|
|
1886
1640
|
catch {
|
|
1887
1641
|
console.error(theme.error(t('cfg.bad_value', {
|
|
1888
1642
|
v: key,
|
|
1889
|
-
type: configValueHint(field),
|
|
1643
|
+
type: configValueHint(field, t),
|
|
1890
1644
|
})));
|
|
1891
1645
|
return;
|
|
1892
1646
|
}
|
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.4",
|
|
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": {
|